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