authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-01-02 15:15:04-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2022-01-02 15:15:04-05:00
log36b069910ef71278ad3aaf059f12144d8b86e856
tree72b5e2a540d94035dd70a0ddc10d442502c330a3
parentae8d6fd7b78a1d41651fc1ab68a973a068f5bd6f
parenta54788ba7af64e59d3fbaf8951e3164f1347a2f4
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #10456 from Aransentin/master

Support parsing tz timezone data

6 files changed, 254 insertions(+), 0 deletions(-)

build.zig+1
...@@ -98,6 +98,7 @@ pub fn build(b: *Builder) !void {...@@ -98,6 +98,7 @@ pub fn build(b: *Builder) !void {
98 ".z.9",98 ".z.9",
99 ".gz",99 ".gz",
100 "rfc1951.txt",100 "rfc1951.txt",
101 ".tzif",
101 },102 },
102 .blank_extensions = &[_][]const u8{103 .blank_extensions = &[_][]const u8{
103 "test.zig",104 "test.zig",
lib/std/std.zig+1
...@@ -39,6 +39,7 @@ pub const StringArrayHashMapUnmanaged = array_hash_map.StringArrayHashMapUnmanag...@@ -39,6 +39,7 @@ pub const StringArrayHashMapUnmanaged = array_hash_map.StringArrayHashMapUnmanag
39pub const TailQueue = @import("linked_list.zig").TailQueue;39pub const TailQueue = @import("linked_list.zig").TailQueue;
40pub const Target = @import("target.zig").Target;40pub const Target = @import("target.zig").Target;
41pub const Thread = @import("Thread.zig");41pub const Thread = @import("Thread.zig");
42pub const Tz = @import("tz.zig").Tz;
4243
43pub const array_hash_map = @import("array_hash_map.zig");44pub const array_hash_map = @import("array_hash_map.zig");
44pub const atomic = @import("atomic.zig");45pub const atomic = @import("atomic.zig");
lib/std/tz.zig created+252
...@@ -0,0 +1,252 @@
1const std = @import("std.zig");
2const builtin = @import("builtin");
3
4pub const Transition = struct {
5 ts: i64,
6 timetype: *Timetype,
7};
8
9pub const Timetype = struct {
10 offset: i32,
11 flags: u8,
12 name_data: [6:0]u8,
13
14 pub fn name(self: Timetype) [:0]const u8 {
15 return std.mem.sliceTo(self.name_data[0..], 0);
16 }
17
18 pub fn isDst(self: Timetype) bool {
19 return (self.flags & 0x01) > 0;
20 }
21
22 pub fn standardTimeIndicator(self: Timetype) bool {
23 return (self.flags & 0x02) > 0;
24 }
25
26 pub fn utIndicator(self: Timetype) bool {
27 return (self.flags & 0x04) > 0;
28 }
29};
30
31pub const Leapsecond = struct {
32 occurrence: i48,
33 correction: i16,
34};
35
36pub const Tz = struct {
37 allocator: std.mem.Allocator,
38 transitions: []const Transition,
39 timetypes: []const Timetype,
40 leapseconds: []const Leapsecond,
41 footer: ?[]const u8,
42
43 const Header = extern struct {
44 magic: [4]u8,
45 version: u8,
46 reserved: [15]u8,
47 counts: extern struct {
48 isutcnt: u32,
49 isstdcnt: u32,
50 leapcnt: u32,
51 timecnt: u32,
52 typecnt: u32,
53 charcnt: u32,
54 },
55 };
56
57 pub fn parse(allocator: std.mem.Allocator, reader: anytype) !Tz {
58 var legacy_header = try reader.readStruct(Header);
59 if (!std.mem.eql(u8, &legacy_header.magic, "TZif")) return error.BadHeader;
60 if (legacy_header.version != 0 and legacy_header.version != '2' and legacy_header.version != '3') return error.BadVersion;
61
62 if (builtin.target.cpu.arch.endian() != std.builtin.Endian.Big) {
63 std.mem.bswapAllFields(@TypeOf(legacy_header.counts), &legacy_header.counts);
64 }
65
66 if (legacy_header.version == 0) {
67 return parseBlock(allocator, reader, legacy_header, true);
68 } else {
69 // If the format is modern, just skip over the legacy data
70 const skipv = legacy_header.counts.timecnt * 5 + legacy_header.counts.typecnt * 6 + legacy_header.counts.charcnt + legacy_header.counts.leapcnt * 8 + legacy_header.counts.isstdcnt + legacy_header.counts.isutcnt;
71 try reader.skipBytes(skipv, .{});
72
73 var header = try reader.readStruct(Header);
74 if (!std.mem.eql(u8, &header.magic, "TZif")) return error.BadHeader;
75 if (header.version != '2' and header.version != '3') return error.BadVersion;
76 if (builtin.target.cpu.arch.endian() != std.builtin.Endian.Big) {
77 std.mem.bswapAllFields(@TypeOf(header.counts), &header.counts);
78 }
79
80 return parseBlock(allocator, reader, header, false);
81 }
82 }
83
84 fn parseBlock(allocator: std.mem.Allocator, reader: anytype, header: Header, legacy: bool) !Tz {
85 if (header.counts.isstdcnt != 0 and header.counts.isstdcnt != header.counts.typecnt) return error.Malformed; // rfc8536: isstdcnt [...] MUST either be zero or equal to "typecnt"
86 if (header.counts.isutcnt != 0 and header.counts.isutcnt != header.counts.typecnt) return error.Malformed; // rfc8536: isutcnt [...] MUST either be zero or equal to "typecnt"
87 if (header.counts.typecnt == 0) return error.Malformed; // rfc8536: typecnt [...] MUST NOT be zero
88 if (header.counts.charcnt == 0) return error.Malformed; // rfc8536: charcnt [...] MUST NOT be zero
89 if (header.counts.charcnt > 256 + 6) return error.Malformed; // Not explicitly banned by rfc8536 but nonsensical
90
91 var leapseconds = try allocator.alloc(Leapsecond, header.counts.leapcnt);
92 errdefer allocator.free(leapseconds);
93 var transitions = try allocator.alloc(Transition, header.counts.timecnt);
94 errdefer allocator.free(transitions);
95 var timetypes = try allocator.alloc(Timetype, header.counts.typecnt);
96 errdefer allocator.free(timetypes);
97
98 // Parse transition types
99 var i: usize = 0;
100 while (i < header.counts.timecnt) : (i += 1) {
101 transitions[i].ts = if (legacy) try reader.readIntBig(i32) else try reader.readIntBig(i64);
102 }
103
104 i = 0;
105 while (i < header.counts.timecnt) : (i += 1) {
106 const tt = try reader.readByte();
107 if (tt >= timetypes.len) return error.Malformed; // rfc8536: Each type index MUST be in the range [0, "typecnt" - 1]
108 transitions[i].timetype = &timetypes[tt];
109 }
110
111 // Parse time types
112 i = 0;
113 while (i < header.counts.typecnt) : (i += 1) {
114 const offset = try reader.readIntBig(i32);
115 if (offset < -2147483648) return error.Malformed; // rfc8536: utoff [...] MUST NOT be -2**31
116 const dst = try reader.readByte();
117 if (dst != 0 and dst != 1) return error.Malformed; // rfc8536: (is)dst [...] The value MUST be 0 or 1.
118 const idx = try reader.readByte();
119 if (idx > header.counts.charcnt - 1) return error.Malformed; // rfc8536: (desig)idx [...] Each index MUST be in the range [0, "charcnt" - 1]
120 timetypes[i] = .{
121 .offset = offset,
122 .flags = dst,
123 .name_data = undefined,
124 };
125
126 // Temporarily cache idx in name_data to be processed after we've read the designator names below
127 timetypes[i].name_data[0] = idx;
128 }
129
130 var designators_data: [256 + 6]u8 = undefined;
131 try reader.readNoEof(designators_data[0..header.counts.charcnt]);
132 const designators = designators_data[0..header.counts.charcnt];
133 if (designators[designators.len - 1] != 0) return error.Malformed; // rfc8536: charcnt [...] includes the trailing NUL (0x00) octet
134
135 // Iterate through the timetypes again, setting the designator names
136 for (timetypes) |*tt| {
137 const name = std.mem.sliceTo(designators[tt.name_data[0]..], 0);
138 // We are mandating the "SHOULD" 6-character limit so we can pack the struct better, and to conform to POSIX.
139 if (name.len > 6) return error.Malformed; // rfc8536: Time zone designations SHOULD consist of at least three (3) and no more than six (6) ASCII characters.
140 std.mem.copy(u8, tt.name_data[0..], name);
141 tt.name_data[name.len] = 0;
142 }
143
144 // Parse leap seconds
145 i = 0;
146 while (i < header.counts.leapcnt) : (i += 1) {
147 const occur: i64 = if (legacy) try reader.readIntBig(i32) else try reader.readIntBig(i64);
148 if (occur < 0) return error.Malformed; // rfc8536: occur [...] MUST be nonnegative
149 if (i > 0 and leapseconds[i - 1].occurrence + 2419199 > occur) return error.Malformed; // rfc8536: occur [...] each later value MUST be at least 2419199 greater than the previous value
150 if (occur > std.math.maxInt(i48)) return error.Malformed; // Unreasonably far into the future
151
152 const corr = try reader.readIntBig(i32);
153 if (i == 0 and corr != -1 and corr != 1) return error.Malformed; // rfc8536: The correction value in the first leap-second record, if present, MUST be either one (1) or minus one (-1)
154 if (i > 0 and leapseconds[i - 1].correction != corr + 1 and leapseconds[i - 1].correction != corr - 1) return error.Malformed; // rfc8536: The correction values in adjacent leap-second records MUST differ by exactly one (1)
155 if (corr > std.math.maxInt(i16)) return error.Malformed; // Unreasonably large correction
156
157 leapseconds[i] = .{
158 .occurrence = @intCast(i48, occur),
159 .correction = @intCast(i16, corr),
160 };
161 }
162
163 // Parse standard/wall indicators
164 i = 0;
165 while (i < header.counts.isstdcnt) : (i += 1) {
166 const stdtime = try reader.readByte();
167 if (stdtime == 1) {
168 timetypes[i].flags |= 0x02;
169 }
170 }
171
172 // Parse UT/local indicators
173 i = 0;
174 while (i < header.counts.isutcnt) : (i += 1) {
175 const ut = try reader.readByte();
176 if (ut == 1) {
177 timetypes[i].flags |= 0x04;
178 if (!timetypes[i].standardTimeIndicator()) return error.Malformed; // rfc8536: standard/wall value MUST be one (1) if the UT/local value is one (1)
179 }
180 }
181
182 // Footer
183 var footer: ?[]u8 = null;
184 if (!legacy) {
185 if ((try reader.readByte()) != '\n') return error.Malformed; // An rfc8536 footer must start with a newline
186 var footerdata_buf: [128]u8 = undefined;
187 const footer_mem = reader.readUntilDelimiter(&footerdata_buf, '\n') catch |err| switch (err) {
188 error.StreamTooLong => return error.OverlargeFooter, // Read more than 128 bytes, much larger than any reasonable POSIX TZ string
189 else => return err,
190 };
191 if (footer_mem.len != 0) {
192 footer = try allocator.dupe(u8, footer_mem);
193 }
194 }
195 errdefer if (footer) |ft| allocator.free(ft);
196
197 return Tz{
198 .allocator = allocator,
199 .transitions = transitions,
200 .timetypes = timetypes,
201 .leapseconds = leapseconds,
202 .footer = footer,
203 };
204 }
205
206 pub fn deinit(self: *Tz) void {
207 if (self.footer) |footer| {
208 self.allocator.free(footer);
209 }
210 self.allocator.free(self.leapseconds);
211 self.allocator.free(self.transitions);
212 self.allocator.free(self.timetypes);
213 }
214};
215
216test "slim" {
217 const data = @embedFile("tz/asia_tokyo.tzif");
218 var in_stream = std.io.fixedBufferStream(data);
219
220 var tz = try std.Tz.parse(std.testing.allocator, in_stream.reader());
221 defer tz.deinit();
222
223 try std.testing.expectEqual(tz.transitions.len, 9);
224 try std.testing.expect(std.mem.eql(u8, tz.transitions[3].timetype.name(), "JDT"));
225 try std.testing.expectEqual(tz.transitions[5].ts, -620298000); // 1950-05-06 15:00:00 UTC
226 try std.testing.expectEqual(tz.leapseconds[13].occurrence, 567993613); // 1988-01-01 00:00:00 UTC (+23s in TAI, and +13 in the data since it doesn't store the initial 10 second offset)
227}
228
229test "fat" {
230 const data = @embedFile("tz/antarctica_davis.tzif");
231 var in_stream = std.io.fixedBufferStream(data);
232
233 var tz = try std.Tz.parse(std.testing.allocator, in_stream.reader());
234 defer tz.deinit();
235
236 try std.testing.expectEqual(tz.transitions.len, 8);
237 try std.testing.expect(std.mem.eql(u8, tz.transitions[3].timetype.name(), "+05"));
238 try std.testing.expectEqual(tz.transitions[4].ts, 1268251224); // 2010-03-10 20:00:00 UTC
239}
240
241test "legacy" {
242 // Taken from Slackware 8.0, from 2001
243 const data = @embedFile("tz/europe_vatican.tzif");
244 var in_stream = std.io.fixedBufferStream(data);
245
246 var tz = try std.Tz.parse(std.testing.allocator, in_stream.reader());
247 defer tz.deinit();
248
249 try std.testing.expectEqual(tz.transitions.len, 170);
250 try std.testing.expect(std.mem.eql(u8, tz.transitions[69].timetype.name(), "CET"));
251 try std.testing.expectEqual(tz.transitions[123].ts, 1414285200); // 2014-10-26 01:00:00 UTC
252}
lib/std/tz/antarctica_davis.tzif created
Binary files /dev/null and b/lib/std/tz/antarctica_davis.tzif differ
lib/std/tz/asia_tokyo.tzif created
Binary files /dev/null and b/lib/std/tz/asia_tokyo.tzif differ
lib/std/tz/europe_vatican.tzif created
Binary files /dev/null and b/lib/std/tz/europe_vatican.tzif differ