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