2021-12-30 15:12:20 +00:00
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 ,
2022-06-15 16:34:56 +01:00
pub fn name ( self : * const Timetype ) [ : 0 ] const u8 {
2021-12-30 15:12:20 +00:00
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 ,
2022-01-01 12:47:08 +00:00
footer : ? [ ] const u8 ,
2021-12-30 15:12:20 +00:00
2022-01-01 12:47:08 +00:00
const Header = extern struct {
magic : [ 4 ] u8 ,
version : u8 ,
reserved : [ 15 ] u8 ,
counts : extern struct {
2021-12-31 17:17:49 +00:00
isutcnt : u32 ,
isstdcnt : u32 ,
leapcnt : u32 ,
timecnt : u32 ,
typecnt : u32 ,
charcnt : u32 ,
2022-01-01 12:47:08 +00:00
} ,
} ;
2021-12-31 17:17:49 +00:00
2022-01-01 12:47:08 +00:00
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 ;
2021-12-31 17:17:49 +00:00
2022-01-01 12:47:08 +00:00
if ( builtin . target . cpu . arch . endian ( ) ! = std . builtin . Endian . Big ) {
2022-01-24 20:40:19 +00:00
std . mem . byteSwapAllFields ( @TypeOf ( legacy_header . counts ) , & legacy_header . counts ) ;
2022-01-01 12:47:08 +00:00
}
2021-12-31 17:17:49 +00:00
2022-01-01 12:47:08 +00:00
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 ;
2021-12-31 17:17:49 +00:00
try reader . skipBytes ( skipv , . { } ) ;
2022-01-01 12:47:08 +00:00
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 ) {
2022-01-24 20:40:19 +00:00
std . mem . byteSwapAllFields ( @TypeOf ( header . counts ) , & header . counts ) ;
2022-01-01 12:47:08 +00:00
}
2021-12-31 17:17:49 +00:00
2022-01-01 12:47:08 +00:00
return parseBlock ( allocator , reader , header , false ) ;
2021-12-31 17:17:49 +00:00
}
2022-01-01 12:47:08 +00:00
}
2021-12-31 17:17:49 +00:00
2022-01-01 12:47:08 +00:00
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
2021-12-31 17:17:49 +00:00
2022-01-01 12:47:08 +00:00
var leapseconds = try allocator . alloc ( Leapsecond , header . counts . leapcnt ) ;
2021-12-30 15:12:20 +00:00
errdefer allocator . free ( leapseconds ) ;
2022-01-01 12:47:08 +00:00
var transitions = try allocator . alloc ( Transition , header . counts . timecnt ) ;
2021-12-30 15:12:20 +00:00
errdefer allocator . free ( transitions ) ;
2022-01-01 12:47:08 +00:00
var timetypes = try allocator . alloc ( Timetype , header . counts . typecnt ) ;
2021-12-30 15:12:20 +00:00
errdefer allocator . free ( timetypes ) ;
// Parse transition types
var i : usize = 0 ;
2022-01-01 12:47:08 +00:00
while ( i < header . counts . timecnt ) : ( i + = 1 ) {
transitions [ i ] . ts = if ( legacy ) try reader . readIntBig ( i32 ) else try reader . readIntBig ( i64 ) ;
2021-12-30 15:12:20 +00:00
}
i = 0 ;
2022-01-01 12:47:08 +00:00
while ( i < header . counts . timecnt ) : ( i + = 1 ) {
2021-12-31 17:17:49 +00:00
const tt = try reader . readByte ( ) ;
2021-12-30 15:12:20 +00:00
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 ;
2022-01-01 12:47:08 +00:00
while ( i < header . counts . typecnt ) : ( i + = 1 ) {
2021-12-31 17:17:49 +00:00
const offset = try reader . readIntBig ( i32 ) ;
2021-12-30 15:12:20 +00:00
if ( offset < - 2147483648 ) return error . Malformed ; // rfc8536: utoff [...] MUST NOT be -2**31
2021-12-31 17:17:49 +00:00
const dst = try reader . readByte ( ) ;
2021-12-30 15:12:20 +00:00
if ( dst ! = 0 and dst ! = 1 ) return error . Malformed ; // rfc8536: (is)dst [...] The value MUST be 0 or 1.
2021-12-31 17:17:49 +00:00
const idx = try reader . readByte ( ) ;
2022-01-01 12:47:08 +00:00
if ( idx > header . counts . charcnt - 1 ) return error . Malformed ; // rfc8536: (desig)idx [...] Each index MUST be in the range [0, "charcnt" - 1]
2021-12-30 15:12:20 +00:00
timetypes [ i ] = . {
. offset = offset ,
. flags = dst ,
. name_data = undefined ,
} ;
2021-12-31 17:17:49 +00:00
// Temporarily cache idx in name_data to be processed after we've read the designator names below
timetypes [ i ] . name_data [ 0 ] = idx ;
2021-12-30 15:12:20 +00:00
}
2021-12-31 17:17:49 +00:00
var designators_data : [ 256 + 6 ] u8 = undefined ;
2022-01-01 12:47:08 +00:00
try reader . readNoEof ( designators_data [ 0 . . header . counts . charcnt ] ) ;
const designators = designators_data [ 0 . . header . counts . charcnt ] ;
2021-12-31 17:17:49 +00:00
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.
std . mem . copy ( u8 , tt . name_data [ 0 . . ] , name ) ;
tt . name_data [ name . len ] = 0 ;
}
2021-12-30 15:12:20 +00:00
// Parse leap seconds
i = 0 ;
2022-01-01 12:47:08 +00:00
while ( i < header . counts . leapcnt ) : ( i + = 1 ) {
const occur : i64 = if ( legacy ) try reader . readIntBig ( i32 ) else try reader . readIntBig ( i64 ) ;
2021-12-30 15:12:20 +00:00
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
2021-12-31 17:17:49 +00:00
const corr = try reader . readIntBig ( i32 ) ;
2021-12-30 15:12:20 +00:00
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)
2021-12-31 17:17:49 +00:00
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)
2021-12-30 15:12:20 +00:00
if ( corr > std . math . maxInt ( i16 ) ) return error . Malformed ; // Unreasonably large correction
leapseconds [ i ] = . {
. occurrence = @intCast ( i48 , occur ) ,
. correction = @intCast ( i16 , corr ) ,
} ;
}
// Parse standard/wall indicators
i = 0 ;
2022-01-01 12:47:08 +00:00
while ( i < header . counts . isstdcnt ) : ( i + = 1 ) {
2021-12-31 17:17:49 +00:00
const stdtime = try reader . readByte ( ) ;
2021-12-30 15:12:20 +00:00
if ( stdtime = = 1 ) {
timetypes [ i ] . flags | = 0x02 ;
}
}
// Parse UT/local indicators
i = 0 ;
2022-01-01 12:47:08 +00:00
while ( i < header . counts . isutcnt ) : ( i + = 1 ) {
2021-12-31 17:17:49 +00:00
const ut = try reader . readByte ( ) ;
2021-12-30 15:12:20 +00:00
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
2022-01-01 12:47:08 +00:00
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 ) ;
2021-12-30 15:12:20 +00:00
return Tz {
. allocator = allocator ,
. transitions = transitions ,
. timetypes = timetypes ,
. leapseconds = leapseconds ,
2022-01-01 12:47:08 +00:00
. footer = footer ,
2021-12-30 15:12:20 +00:00
} ;
}
pub fn deinit ( self : * Tz ) void {
2022-01-01 12:47:08 +00:00
if ( self . footer ) | footer | {
self . allocator . free ( footer ) ;
}
2021-12-30 15:12:20 +00:00
self . allocator . free ( self . leapseconds ) ;
self . allocator . free ( self . transitions ) ;
self . allocator . free ( self . timetypes ) ;
}
} ;
2021-12-31 17:17:49 +00:00
test " slim " {
2021-12-30 15:12:20 +00:00
const data = @embedFile ( " tz/asia_tokyo.tzif " ) ;
2021-12-31 17:17:49 +00:00
var in_stream = std . io . fixedBufferStream ( data ) ;
var tz = try std . Tz . parse ( std . testing . allocator , in_stream . reader ( ) ) ;
2021-12-30 15:12:20 +00:00
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 " ) ) ;
2021-12-31 17:17:49 +00:00
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
2021-12-30 15:12:20 +00:00
}
2022-01-01 12:47:08 +00:00
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
}