/src/serenity/Userland/Libraries/LibTimeZone/TimeZone.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/ByteString.h> |
8 | | #include <AK/Debug.h> |
9 | | #include <AK/ScopeGuard.h> |
10 | | #include <LibTimeZone/TimeZone.h> |
11 | | #include <limits.h> |
12 | | #include <stdio.h> |
13 | | #include <stdlib.h> |
14 | | #include <string.h> |
15 | | #include <time.h> |
16 | | #include <unistd.h> |
17 | | |
18 | | namespace TimeZone { |
19 | | |
20 | | // NOTE: Without ENABLE_TIME_ZONE_DATA LibTimeZone operates in a UTC-only mode and only recognizes |
21 | | // the 'UTC' time zone, which is slightly more useful than a bunch of dummy functions that |
22 | | // can't do anything. When we build with time zone data, these weakly linked functions are |
23 | | // replaced with their proper counterparts. |
24 | | |
25 | | #if !ENABLE_TIME_ZONE_DATA |
26 | | enum class TimeZone : u16 { |
27 | | UTC, |
28 | | }; |
29 | | #endif |
30 | | |
31 | | class TimeZoneFile { |
32 | | public: |
33 | | TimeZoneFile(char const* mode) |
34 | 0 | : m_file(fopen("/etc/timezone", mode)) |
35 | 0 | { |
36 | 0 | if (m_file) |
37 | 0 | flockfile(m_file); |
38 | 0 | } |
39 | | |
40 | | ~TimeZoneFile() |
41 | 0 | { |
42 | 0 | if (m_file) { |
43 | 0 | funlockfile(m_file); |
44 | 0 | fclose(m_file); |
45 | 0 | } |
46 | 0 | } |
47 | | |
48 | | ErrorOr<ByteString> read_time_zone() |
49 | 0 | { |
50 | 0 | if (!m_file) |
51 | 0 | return Error::from_string_literal("Could not open /etc/timezone"); |
52 | | |
53 | 0 | Array<u8, 128> buffer; |
54 | 0 | size_t bytes = fread(buffer.data(), 1, buffer.size(), m_file); |
55 | |
|
56 | 0 | if (bytes == 0) |
57 | 0 | return Error::from_string_literal("Could not read time zone from /etc/timezone"); |
58 | | |
59 | 0 | return ByteString(buffer.span().slice(0, bytes)).trim_whitespace(); |
60 | 0 | } |
61 | | |
62 | | ErrorOr<void> write_time_zone(StringView time_zone) |
63 | 0 | { |
64 | 0 | if (!m_file) |
65 | 0 | return Error::from_string_literal("Could not open /etc/timezone"); |
66 | 0 |
|
67 | 0 | auto bytes = fwrite(time_zone.characters_without_null_termination(), 1, time_zone.length(), m_file); |
68 | 0 | if (bytes != time_zone.length()) |
69 | 0 | return Error::from_string_literal("Could not write new time zone to /etc/timezone"); |
70 | 0 |
|
71 | 0 | return {}; |
72 | 0 | } |
73 | | |
74 | | private: |
75 | | FILE* m_file { nullptr }; |
76 | | }; |
77 | | |
78 | | StringView system_time_zone() |
79 | 0 | { |
80 | 0 | TimeZoneFile time_zone_file("r"); |
81 | 0 | auto time_zone = time_zone_file.read_time_zone(); |
82 | | |
83 | | // FIXME: Propagate the error to existing callers. |
84 | 0 | if (time_zone.is_error()) { |
85 | 0 | dbgln_if(TIME_ZONE_DEBUG, "{}", time_zone.error()); |
86 | 0 | return "UTC"sv; |
87 | 0 | } |
88 | | |
89 | 0 | return canonicalize_time_zone(time_zone.value()).value_or("UTC"sv); |
90 | 0 | } |
91 | | |
92 | | StringView current_time_zone() |
93 | 0 | { |
94 | 0 | if (char* tz = getenv("TZ"); tz != nullptr) { |
95 | | // FIXME: Actually parse the TZ environment variable, described here: |
96 | | // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08 |
97 | 0 | StringView time_zone { tz, strlen(tz) }; |
98 | |
|
99 | 0 | if (auto maybe_time_zone = canonicalize_time_zone(time_zone); maybe_time_zone.has_value()) |
100 | 0 | return *maybe_time_zone; |
101 | | |
102 | 0 | dbgln_if(TIME_ZONE_DEBUG, "Could not determine time zone from TZ environment: {}", time_zone); |
103 | 0 | return "UTC"sv; |
104 | 0 | } |
105 | | |
106 | 0 | static constexpr auto zoneinfo = "/zoneinfo"sv; |
107 | |
|
108 | 0 | char* real_path = realpath("/etc/localtime", nullptr); |
109 | 0 | ScopeGuard free_path = [real_path]() { free(real_path); }; |
110 | |
|
111 | 0 | if (real_path) { |
112 | 0 | auto time_zone = StringView { real_path, strlen(real_path) }; |
113 | | |
114 | | // The zoneinfo file may be located in paths like /usr/share/zoneinfo/ or /usr/share/zoneinfo.default/. |
115 | | // We want to strip any such prefix from the path to arrive at the time zone name. |
116 | 0 | if (auto index = time_zone.find(zoneinfo); index.has_value()) |
117 | 0 | time_zone = time_zone.substring_view(*index + zoneinfo.length()); |
118 | 0 | if (auto index = time_zone.find('/'); index.has_value()) |
119 | 0 | time_zone = time_zone.substring_view(*index + 1); |
120 | |
|
121 | 0 | if (auto maybe_time_zone = canonicalize_time_zone(time_zone); maybe_time_zone.has_value()) |
122 | 0 | return *maybe_time_zone; |
123 | | |
124 | 0 | dbgln_if(TIME_ZONE_DEBUG, "Could not determine time zone from /etc/localtime: {}", time_zone); |
125 | 0 | } else { |
126 | 0 | dbgln_if(TIME_ZONE_DEBUG, "Could not read the /etc/localtime link: {}", strerror(errno)); |
127 | 0 | } |
128 | | |
129 | | // Read the system timezone file /etc/timezone |
130 | 0 | return system_time_zone(); |
131 | 0 | } |
132 | | |
133 | | ErrorOr<void> change_time_zone([[maybe_unused]] StringView time_zone) |
134 | 0 | { |
135 | | #ifdef AK_OS_SERENITY |
136 | | TimeZoneFile time_zone_file("w"); |
137 | | |
138 | | if (auto new_time_zone = canonicalize_time_zone(time_zone); new_time_zone.has_value()) |
139 | | return time_zone_file.write_time_zone(*new_time_zone); |
140 | | |
141 | | return Error::from_string_literal("Provided time zone is not supported"); |
142 | | #else |
143 | | // Do not even attempt to change the time zone of someone's host machine. |
144 | 0 | return {}; |
145 | 0 | #endif |
146 | 0 | } |
147 | | |
148 | | ReadonlySpan<TimeZoneIdentifier> __attribute__((weak)) all_time_zones() |
149 | 0 | { |
150 | | #if !ENABLE_TIME_ZONE_DATA |
151 | | static constexpr auto utc = Array { TimeZoneIdentifier { "UTC"sv, IsLink::No } }; |
152 | | return utc; |
153 | | #else |
154 | 0 | return {}; |
155 | 0 | #endif |
156 | 0 | } |
157 | | |
158 | | Optional<TimeZone> __attribute__((weak)) time_zone_from_string([[maybe_unused]] StringView time_zone) |
159 | 0 | { |
160 | | #if !ENABLE_TIME_ZONE_DATA |
161 | | if (time_zone.equals_ignoring_ascii_case("UTC"sv)) |
162 | | return TimeZone::UTC; |
163 | | #endif |
164 | 0 | return {}; |
165 | 0 | } |
166 | | |
167 | | StringView __attribute__((weak)) time_zone_to_string([[maybe_unused]] TimeZone time_zone) |
168 | 0 | { |
169 | | #if !ENABLE_TIME_ZONE_DATA |
170 | | VERIFY(time_zone == TimeZone::UTC); |
171 | | return "UTC"sv; |
172 | | #else |
173 | 0 | return {}; |
174 | 0 | #endif |
175 | 0 | } |
176 | | |
177 | | Optional<StringView> canonicalize_time_zone(StringView time_zone) |
178 | 0 | { |
179 | 0 | auto maybe_time_zone = time_zone_from_string(time_zone); |
180 | 0 | if (!maybe_time_zone.has_value()) |
181 | 0 | return {}; |
182 | | |
183 | 0 | auto canonical_time_zone = time_zone_to_string(*maybe_time_zone); |
184 | 0 | if (canonical_time_zone.is_one_of("Etc/UTC"sv, "Etc/GMT"sv, "GMT"sv)) |
185 | 0 | return "UTC"sv; |
186 | | |
187 | 0 | return canonical_time_zone; |
188 | 0 | } |
189 | | |
190 | 0 | Optional<DaylightSavingsRule> __attribute__((weak)) daylight_savings_rule_from_string(StringView) { return {}; } |
191 | 0 | StringView __attribute__((weak)) daylight_savings_rule_to_string(DaylightSavingsRule) { return {}; } |
192 | | |
193 | | Optional<Offset> __attribute__((weak)) get_time_zone_offset([[maybe_unused]] TimeZone time_zone, AK::UnixDateTime) |
194 | 0 | { |
195 | | #if !ENABLE_TIME_ZONE_DATA |
196 | | VERIFY(time_zone == TimeZone::UTC); |
197 | | return Offset {}; |
198 | | #else |
199 | 0 | return {}; |
200 | 0 | #endif |
201 | 0 | } |
202 | | |
203 | | Optional<Offset> get_time_zone_offset(StringView time_zone, AK::UnixDateTime time) |
204 | 0 | { |
205 | 0 | if (auto maybe_time_zone = time_zone_from_string(time_zone); maybe_time_zone.has_value()) |
206 | 0 | return get_time_zone_offset(*maybe_time_zone, time); |
207 | 0 | return {}; |
208 | 0 | } |
209 | | |
210 | | Optional<Array<NamedOffset, 2>> __attribute__((weak)) get_named_time_zone_offsets([[maybe_unused]] TimeZone time_zone, AK::UnixDateTime) |
211 | 0 | { |
212 | | #if !ENABLE_TIME_ZONE_DATA |
213 | | VERIFY(time_zone == TimeZone::UTC); |
214 | | |
215 | | NamedOffset utc_offset {}; |
216 | | utc_offset.name = "UTC"sv; |
217 | | |
218 | | return Array { utc_offset, utc_offset }; |
219 | | #else |
220 | 0 | return {}; |
221 | 0 | #endif |
222 | 0 | } |
223 | | |
224 | | Optional<Array<NamedOffset, 2>> get_named_time_zone_offsets(StringView time_zone, AK::UnixDateTime time) |
225 | 0 | { |
226 | 0 | if (auto maybe_time_zone = time_zone_from_string(time_zone); maybe_time_zone.has_value()) |
227 | 0 | return get_named_time_zone_offsets(*maybe_time_zone, time); |
228 | 0 | return {}; |
229 | 0 | } |
230 | | |
231 | 0 | Optional<Location> __attribute__((weak)) get_time_zone_location(TimeZone) { return {}; } |
232 | | |
233 | | Optional<Location> get_time_zone_location(StringView time_zone) |
234 | 0 | { |
235 | 0 | if (auto maybe_time_zone = time_zone_from_string(time_zone); maybe_time_zone.has_value()) |
236 | 0 | return get_time_zone_location(*maybe_time_zone); |
237 | 0 | return {}; |
238 | 0 | } |
239 | | |
240 | 0 | Optional<Region> __attribute__((weak)) region_from_string(StringView) { return {}; } |
241 | 0 | StringView __attribute__((weak)) region_to_string(Region) { return {}; } |
242 | 0 | Vector<StringView> __attribute__((weak)) time_zones_in_region(StringView) { return {}; } |
243 | | |
244 | | } |