package timezone import "base:intrinsics" import "core:slice" import "core:strings" import "core:strconv" import "core:time/datetime" // Implementing RFC8536 [https://datatracker.ietf.org/doc/html/rfc8536] TZIF_MAGIC :: u32be(0x545A6966) // 'TZif' TZif_Version :: enum u8 { V1 = 0, V2 = '2', V3 = '3', V4 = '4', } BIG_BANG_ISH :: -0x800000000000000 TZif_Header :: struct #packed { magic: u32be, version: TZif_Version, reserved: [15]u8, isutcnt: u32be, isstdcnt: u32be, leapcnt: u32be, timecnt: u32be, typecnt: u32be, charcnt: u32be, } Sun_Shift :: enum u8 { Standard = 0, DST = 1, } Local_Time_Type :: struct #packed { utoff: i32be, dst: Sun_Shift, idx: u8, } Leapsecond_Record :: struct #packed { occur: i64be, corr: i32be, } @private tzif_data_block_size :: proc(hdr: ^TZif_Header, version: TZif_Version) -> (block_size: int, ok: bool) { time_size : int if version == .V1 { time_size = 4 } else if version == .V2 || version == .V3 || version == .V4 { time_size = 8 } else { return } return (int(hdr.timecnt) * time_size) + int(hdr.timecnt) + int(hdr.typecnt * size_of(Local_Time_Type)) + int(hdr.charcnt) + (int(hdr.leapcnt) * (time_size + 4)) + int(hdr.isstdcnt) + int(hdr.isutcnt), true } @private is_alphabetic :: proc(ch: u8) -> bool { // ('A' -> 'Z') || ('a' -> 'z') return (ch > 0x40 && ch < 0x5B) || (ch > 0x60 && ch < 0x7B) } @private is_numeric :: proc(ch: u8) -> bool { // ('0' -> '9') return (ch > 0x2F && ch < 0x3A) } @private is_alphanumeric :: proc(ch: u8) -> bool { return is_alphabetic(ch) || is_numeric(ch) } @private is_valid_quoted_char :: proc(ch: u8) -> bool { return is_alphabetic(ch) || is_numeric(ch) || ch == '+' || ch == '-' } @private parse_posix_tz_shortname :: proc(str: string) -> (out: string, idx: int, ok: bool) { was_quoted := false quoted := false i := 0 for ; i < len(str); i += 1 { ch := str[i] if !quoted && ch == '<' { quoted = true was_quoted = true continue } if quoted && ch == '>' { quoted = false break } if !is_valid_quoted_char(ch) && ch != ',' { return } if !quoted && !is_alphabetic(ch) { break } } // If we didn't see the trailing quote if was_quoted && quoted { return } out_str: string end_idx := i if was_quoted { end_idx += 1 out_str = str[1:i] } else { out_str = str[:i] } return out_str, end_idx, true } @private parse_posix_tz_offset :: proc(str: string) -> (out_sec: i64, idx: int, ok: bool) { str := str sign : i64 = 1 start_idx := 0 i := 0 if str[i] == '+' { i += 1 sign = 1 start_idx = 1 } else if str[i] == '-' { i += 1 sign = -1 start_idx = 1 } got_more_time := false for ; i < len(str); i += 1 { if is_numeric(str[i]) { continue } if str[i] == ':' { got_more_time = true break } break } ret_sec : i64 = 0 hours := strconv.parse_int(str[start_idx:i], 10) or_return if hours > 167 || hours < -167 { return } ret_sec += i64(hours) * (60 * 60) if !got_more_time { return ret_sec * sign, i, true } i += 1 start_idx = i got_more_time = false for ; i < len(str); i += 1 { if is_numeric(str[i]) { continue } if str[i] == ':' { got_more_time = true break } break } mins_str := str[start_idx:i] if len(mins_str) != 2 { return } mins := strconv.parse_int(mins_str, 10) or_return if mins > 59 || mins < 0 { return } ret_sec += i64(mins) * 60 if !got_more_time { return ret_sec * sign, i, true } i += 1 start_idx = i for ; i < len(str); i += 1 { if !is_numeric(str[i]) { break } } secs_str := str[start_idx:i] if len(secs_str) != 2 { return } secs := strconv.parse_int(secs_str, 10) or_return if secs > 59 || secs < 0 { return } ret_sec += i64(secs) return ret_sec * sign, i, true } @private skim_digits :: proc(str: string) -> (out: string, idx: int, ok: bool) { i := 0 for ; i < len(str); i += 1 { ch := str[i] if ch == '.' || ch == '/' || ch == ',' { break } if !is_numeric(ch) { return } } return str[:i], i, true } TWO_AM :: 2 * 60 * 60 parse_posix_rrule :: proc(str: string) -> (out: datetime.TZ_Transition_Date, idx: int, ok: bool) { str := str if len(str) < 2 { return } i := 0 // No leap if str[i] == 'J' { i += 1 day_str, off := skim_digits(str[i:]) or_return i += off day := strconv.parse_int(day_str, 10) or_return if day < 1 || day > 365 { return } offset : i64 = TWO_AM if len(str) != i && str[i] == '/' { i += 1 offset, off = parse_posix_tz_offset(str[i:]) or_return i += off } if len(str) != i && str[i] == ',' { i += 1 } return datetime.TZ_Transition_Date{ type = .No_Leap, day = u16(day), time = offset, }, i, true // Leap } else if is_numeric(str[i]) { day_str, off := skim_digits(str[i:]) or_return i += off day := strconv.parse_int(day_str, 10) or_return if day < 0 || day > 365 { return } offset : i64 = TWO_AM if len(str) != i && str[i] == '/' { i += 1 offset, off = parse_posix_tz_offset(str[i:]) or_return i += off } if len(str) != i && str[i] == ',' { i += 1 } return datetime.TZ_Transition_Date{ type = .Leap, day = u16(day), time = offset, }, i, true } else if str[i] == 'M' { i += 1 month_str, week_str, day_str: string off := 0 month_str, off = skim_digits(str[i:]) or_return i += off + 1 week_str, off = skim_digits(str[i:]) or_return i += off + 1 day_str, off = skim_digits(str[i:]) or_return i += off month := strconv.parse_int(month_str, 10) or_return if month < 1 || month > 12 { return } week := strconv.parse_int(week_str, 10) or_return if week < 1 || week > 5 { return } day := strconv.parse_int(day_str, 10) or_return if day < 0 || day > 6 { return } offset : i64 = TWO_AM if len(str) != i && str[i] == '/' { i += 1 offset, off = parse_posix_tz_offset(str[i:]) or_return i += off } if len(str) != i && str[i] == ',' { i += 1 } return datetime.TZ_Transition_Date{ type = .Month_Week_Day, month = u8(month), week = u8(week), day = u16(day), time = offset, }, i, true } return } parse_posix_tz :: proc(posix_tz: string, allocator := context.allocator) -> (out: datetime.TZ_RRule, ok: bool) { // TZ string contain at least 3 characters for the STD name, and 1 for the offset if len(posix_tz) < 4 { return } str := posix_tz std_name, idx := parse_posix_tz_shortname(str) or_return str = str[idx:] std_offset, idx2 := parse_posix_tz_offset(str) or_return std_offset *= -1 str = str[idx2:] std_name_str, err := strings.clone(std_name, allocator) if err != nil { return } defer if !ok { delete(std_name_str, allocator) } if len(str) == 0 { return datetime.TZ_RRule{ has_dst = false, std_name = std_name_str, std_offset = std_offset, std_date = datetime.TZ_Transition_Date{ type = .Leap, day = 0, time = TWO_AM, }, }, true } dst_name: string dst_offset := std_offset + (1 * 60 * 60) if str[0] != ',' { dst_name, idx = parse_posix_tz_shortname(str) or_return str = str[idx:] if str[0] != ',' { dst_offset, idx = parse_posix_tz_offset(str) or_return dst_offset *= -1 str = str[idx:] } } if str[0] != ',' { return } str = str[1:] std_td, idx3 := parse_posix_rrule(str) or_return str = str[idx3:] dst_td, idx4 := parse_posix_rrule(str) or_return str = str[idx4:] dst_name_str: string dst_name_str, err = strings.clone(dst_name, allocator) if err != nil { return } return datetime.TZ_RRule{ has_dst = true, std_name = std_name_str, std_offset = std_offset, std_date = std_td, dst_name = dst_name_str, dst_offset = dst_offset, dst_date = dst_td, }, true } parse_tzif :: proc(_buffer: []u8, region_name: string, allocator := context.allocator) -> (out: ^datetime.TZ_Region, ok: bool) { context.allocator = allocator buffer := _buffer // TZif is crufty. Skip the initial header. v1_hdr := slice.to_type(buffer, TZif_Header) or_return if v1_hdr.magic != TZIF_MAGIC { return } if v1_hdr.typecnt == 0 || v1_hdr.charcnt == 0 { return } if v1_hdr.isutcnt != 0 && v1_hdr.isutcnt != v1_hdr.typecnt { return } if v1_hdr.isstdcnt != 0 && v1_hdr.isstdcnt != v1_hdr.typecnt { return } // We don't bother supporting v1, it uses u32 timestamps if v1_hdr.version == .V1 { return } // We only support v2 and v3 if v1_hdr.version != .V2 && v1_hdr.version != .V3 { return } // Skip the initial v1 block too. first_block_size, _ := tzif_data_block_size(&v1_hdr, .V1) if len(buffer) <= size_of(v1_hdr) + first_block_size { return } buffer = buffer[size_of(v1_hdr)+first_block_size:] // Ok, time to parse real things real_hdr := slice.to_type(buffer, TZif_Header) or_return if real_hdr.magic != TZIF_MAGIC { return } if real_hdr.typecnt == 0 || real_hdr.charcnt == 0 { return } if real_hdr.isutcnt != 0 && real_hdr.isutcnt != real_hdr.typecnt { return } if real_hdr.isstdcnt != 0 && real_hdr.isstdcnt != real_hdr.typecnt { return } // Grab the real data block real_block_size, _ := tzif_data_block_size(&real_hdr, v1_hdr.version) if len(buffer) <= size_of(real_hdr) + real_block_size { return } buffer = buffer[size_of(real_hdr):] time_size := 8 transition_times := slice.reinterpret([]i64be, buffer[:int(real_hdr.timecnt)*size_of(i64be)]) for time in transition_times { if time < BIG_BANG_ISH { return } } buffer = buffer[int(real_hdr.timecnt)*time_size:] transition_types := buffer[:int(real_hdr.timecnt)] for type in transition_types { if int(type) > int(real_hdr.typecnt - 1) { return } } buffer = buffer[int(real_hdr.timecnt):] local_time_types := slice.reinterpret([]Local_Time_Type, buffer[:int(real_hdr.typecnt)*size_of(Local_Time_Type)]) for ltt in local_time_types { // UT offset should be > -25 hours and < 26 hours if int(ltt.utoff) < -89999 || int(ltt.utoff) > 93599 { return } if ltt.dst != .DST && ltt.dst != .Standard { return } if int(ltt.idx) > int(real_hdr.charcnt - 1) { return } } buffer = buffer[int(real_hdr.typecnt) * size_of(Local_Time_Type):] timezone_string_table := buffer[:real_hdr.charcnt] buffer = buffer[real_hdr.charcnt:] leapsecond_records := slice.reinterpret([]Leapsecond_Record, buffer[:int(real_hdr.leapcnt)*size_of(Leapsecond_Record)]) if len(leapsecond_records) > 0 { if leapsecond_records[0].occur < 0 { return } } buffer = buffer[(int(real_hdr.leapcnt) * size_of(Leapsecond_Record)):] standard_wall_tags := buffer[:int(real_hdr.isstdcnt)] for stdwall_tag, _ in standard_wall_tags { if (stdwall_tag != 0 && stdwall_tag != 1) { return } } buffer = buffer[int(real_hdr.isstdcnt):] ut_tags := buffer[:int(real_hdr.isutcnt)] for ut_tag, _ in ut_tags { if (ut_tag != 0 && ut_tag != 1) { return } } buffer = buffer[int(real_hdr.isutcnt):] // Start of footer if buffer[0] != '\n' { return } buffer = buffer[1:] if buffer[0] == ':' { return } end_idx := 0 for ch in buffer { if ch == '\n' { break } if ch == 0 { return } end_idx += 1 } footer_str := string(buffer[:end_idx]) // UTC is a special case, we don't need to alloc if len(local_time_types) == 1 && local_time_types[0].utoff == 0 { return nil, true } ltt_names, err := make([dynamic]string, 0, len(local_time_types), allocator) if err != nil { return } defer if err != nil { for name in ltt_names { delete(name, allocator) } delete(ltt_names) } for ltt in local_time_types { name := cstring(raw_data(timezone_string_table[ltt.idx:])) ltt_name: string ltt_name, err = strings.clone_from_cstring_bounded(name, len(timezone_string_table), allocator) if err != nil { return } append(<t_names, ltt_name) } records: []datetime.TZ_Record records, err = make([]datetime.TZ_Record, len(transition_times), allocator) if err != nil { return } defer if err != nil { delete(records, allocator) } for trans_time, idx in transition_times { trans_idx := transition_types[idx] ltt := local_time_types[trans_idx] records[idx] = datetime.TZ_Record{ time = i64(trans_time), utc_offset = i64(ltt.utoff), shortname = ltt_names[trans_idx], dst = bool(ltt.dst), } } rrule, ok2 := parse_posix_tz(footer_str, allocator) if !ok2 { return } defer if err != nil { delete(rrule.std_name, allocator) delete(rrule.dst_name, allocator) } region_name_out: string region_name_out, err = strings.clone(region_name, allocator) if err != nil { return } defer if err != nil { delete(region_name_out, allocator) } region: ^datetime.TZ_Region region, err = new_clone(datetime.TZ_Region{ records = records, shortnames = ltt_names[:], name = region_name_out, rrule = rrule, }, allocator) if err != nil { return } return region, true }