Skip to main content

jiff_core/tz/tzif/
parser.rs

1use alloc::{string::String, vec, vec::Vec};
2
3use crate::{
4    tz::{posix, Abbreviation, Dst, Offset},
5    util::{crc32, MaybeStaticSlice},
6};
7
8use super::{
9    DateTime, Indicator, LocalTimeType, TimeZone, Timestamp, TransitionInfo,
10    TransitionKind, Transitions,
11};
12
13// The TZif parser clamps timestamps to this range. It is not ideal, but Jiff
14// can't handle values outside of this range, and completely refusing to use
15// TZif data with pathological timestamps in typically irrelevant transitions
16// is undesirable.
17const TIMESTAMP_MIN: i64 = Timestamp::MIN.as_second();
18const TIMESTAMP_MAX: i64 = Timestamp::MAX.as_second();
19
20// Unlike timestamps, offsets outside Jiff's range are rejected. They could
21// result in incorrect datetimes for actual transitions.
22const OFFSET_MIN: i32 = Offset::MIN.seconds();
23const OFFSET_MAX: i32 = Offset::MAX.seconds();
24
25// When fattening TZif data, this is the year to go up to. This year was
26// chosen because it is what the "fat" TZif data generated by zic uses.
27const FATTEN_UP_TO_YEAR: i16 = 2038;
28
29// A defense-in-depth limit on additional transitions generated from a POSIX
30// time zone. Normal cases should have at most two transitions per year.
31const FATTEN_MAX_TRANSITIONS: usize = 300;
32
33impl TimeZone {
34    /// Parses the given data as a TZif formatted file.
35    ///
36    /// The name given is attached to the returned value, but is otherwise not
37    /// significant.
38    ///
39    /// In general, callers may assume that it is safe to pass arbitrary or
40    /// even untrusted data to this function and count on it not panicking or
41    /// using resources that are not limited to a small constant factor of the
42    /// size of the data itself.
43    ///
44    /// This is only available when the `alloc` feature is enabled because
45    /// parsing TZif data requires heap allocation.
46    pub fn parse(bytes: &[u8]) -> Result<TimeZone, ParseError> {
47        ParsedTimeZone::parse(bytes)
48    }
49}
50
51// We mirror the `TimeZone` type here because a `TimeZone` wants a fixed and
52// immutable set of transitions. This is difficult to make it work with parsing
53// that wants variable length *and* mutable. We could accommodate the fixed
54// length requirement with some care, but requiring mutability makes using
55// a `MaybeStaticSlice` very awkward.
56
57struct ParsedTimeZone {
58    fixed: ParsedFixed,
59    types: Vec<LocalTimeType>,
60    transitions: ParsedTransitions,
61}
62
63struct ParsedFixed {
64    version: u8,
65    checksum: u32,
66    designations: Vec<Abbreviation>,
67    posix_tz: Option<posix::TimeZone>,
68}
69
70struct ParsedTransitions {
71    timestamps: Vec<Timestamp>,
72    civil_starts: Vec<DateTime>,
73    civil_ends: Vec<DateTime>,
74    infos: Vec<TransitionInfo>,
75}
76
77impl ParsedTimeZone {
78    fn parse(bytes: &[u8]) -> Result<TimeZone, ParseError> {
79        let original = bytes;
80        let (header32, rest) =
81            Header::parse(4, bytes).map_err(ParseErrorKind::Header32)?;
82        let (mut tzif, rest) = if header32.version == 0 {
83            ParsedTimeZone::parse32(header32, rest)?
84        } else {
85            ParsedTimeZone::parse64(header32, rest)?
86        };
87        tzif.fatten();
88        // This should come after fattening because fattening may add new
89        // transitions and we want to add civil datetimes to those too.
90        tzif.add_civil_datetimes_to_transitions();
91        tzif.verify_posix_time_zone_consistency()?;
92
93        // Compute the checksum using the portion of the TZif bytes actually
94        // consumed. TZif data may contain superfluous bytes after the parsed
95        // block.
96        let tzif_raw_len = original.len() - rest.len();
97        tzif.fixed.checksum = crc32::sum(&original[..tzif_raw_len]);
98
99        tzif.finish()
100    }
101
102    fn new(version: u8) -> ParsedTimeZone {
103        ParsedTimeZone {
104            fixed: ParsedFixed {
105                version,
106                checksum: 0,
107                designations: vec![],
108                posix_tz: None,
109            },
110            types: vec![],
111            transitions: ParsedTransitions {
112                timestamps: vec![],
113                civil_starts: vec![],
114                civil_ends: vec![],
115                infos: vec![],
116            },
117        }
118    }
119
120    fn finish(self) -> Result<TimeZone, ParseError> {
121        Ok(TimeZone {
122            version: self.fixed.version,
123            checksum: self.fixed.checksum,
124            designations: MaybeStaticSlice::heap(
125                self.fixed.designations.into_boxed_slice(),
126            ),
127            posix_tz: self.fixed.posix_tz,
128            types: MaybeStaticSlice::heap(self.types.into_boxed_slice()),
129            transitions: Transitions {
130                timestamps: MaybeStaticSlice::heap(
131                    self.transitions.timestamps.into_boxed_slice(),
132                ),
133                civil_starts: MaybeStaticSlice::heap(
134                    self.transitions.civil_starts.into_boxed_slice(),
135                ),
136                civil_ends: MaybeStaticSlice::heap(
137                    self.transitions.civil_ends.into_boxed_slice(),
138                ),
139                infos: MaybeStaticSlice::heap(
140                    self.transitions.infos.into_boxed_slice(),
141                ),
142            },
143        })
144    }
145
146    fn parse32<'b>(
147        header32: Header,
148        bytes: &'b [u8],
149    ) -> Result<(ParsedTimeZone, &'b [u8]), ParseError> {
150        let mut tzif = ParsedTimeZone::new(header32.version);
151        let rest = tzif.parse_transitions(&header32, bytes)?;
152        let rest = tzif.parse_transition_types(&header32, rest)?;
153        let rest = tzif.parse_local_time_types(&header32, rest)?;
154        let rest = tzif.parse_time_zone_designations(&header32, rest)?;
155        let rest = tzif.parse_leap_seconds(&header32, rest)?;
156        let rest = tzif.parse_indicators(&header32, rest)?;
157        Ok((tzif, rest))
158    }
159
160    fn parse64<'b>(
161        header32: Header,
162        bytes: &'b [u8],
163    ) -> Result<(ParsedTimeZone, &'b [u8]), ParseError> {
164        let (_, rest) =
165            try_split_at(SplitAtError::V1, bytes, header32.data_block_len()?)?;
166        let (header64, rest) =
167            Header::parse(8, rest).map_err(ParseErrorKind::Header64)?;
168        let mut tzif = ParsedTimeZone::new(header64.version);
169        let rest = tzif.parse_transitions(&header64, rest)?;
170        let rest = tzif.parse_transition_types(&header64, rest)?;
171        let rest = tzif.parse_local_time_types(&header64, rest)?;
172        let rest = tzif.parse_time_zone_designations(&header64, rest)?;
173        let rest = tzif.parse_leap_seconds(&header64, rest)?;
174        let rest = tzif.parse_indicators(&header64, rest)?;
175        let rest = tzif.parse_footer(&header64, rest)?;
176        // Note that we don't check that the TZif data is fully valid. It is
177        // possible for it to contain superfluous information. For example, a
178        // non-zero local time type that is never referenced by a transition.
179        Ok((tzif, rest))
180    }
181
182    fn parse_transitions<'b>(
183        &mut self,
184        header: &Header,
185        bytes: &'b [u8],
186    ) -> Result<&'b [u8], ParseError> {
187        let (bytes, rest) = try_split_at(
188            SplitAtError::TransitionTimes,
189            bytes,
190            header.transition_times_len()?,
191        )?;
192        let mut it = bytes.chunks_exact(header.time_size);
193        // RFC 8536 says: "If there are no transitions, local time for all
194        // timestamps is specified by the TZ string in the footer if present
195        // and nonempty; otherwise, it is specified by time type 0."
196        //
197        // RFC 8536 also says: "Local time for timestamps before the first
198        // transition is specified by the first time type (time type 0)."
199        //
200        // So if there are no transitions, pushing this dummy one will result
201        // in the desired behavior even when it's the only transition.
202        // Similarly, since this is the minimum timestamp value, it will
203        // trigger for any times before the first transition found in the TZif
204        // data.
205        self.transitions.add_with_type_index(Timestamp::MIN, 0);
206        for chunk in &mut it {
207            let unix_timestamp = if header.is_32bit() {
208                i64::from(from_be_bytes_i32(chunk))
209            } else {
210                from_be_bytes_i64(chunk)
211            };
212            let timestamp = match Timestamp::from_second(unix_timestamp) {
213                Ok(timestamp) => timestamp,
214                Err(_) => {
215                    // We really shouldn't error here just because the Unix
216                    // timestamp is outside what Jiff supports. Since what Jiff
217                    // supports is _somewhat_ arbitrary. But Jiff's supported
218                    // range is good enough for all realistic purposes, so we
219                    // just clamp an out-of-range Unix timestamp to the Jiff
220                    // min or max value.
221                    //
222                    // This can't result in the sorting order being wrong, but
223                    // it can result in a transition that is duplicative with
224                    // the dummy transition we inserted above. This should be
225                    // fine.
226                    let clamped =
227                        if unix_timestamp < Timestamp::MIN.as_second() {
228                            Timestamp::MIN
229                        } else {
230                            Timestamp::MAX
231                        };
232                    warn!(
233                        "found Unix timestamp `{unix_timestamp}` that is \
234                         outside Jiff's supported range, \
235                         clamping to `{clamped:?}`",
236                    );
237                    clamped
238                }
239            };
240            self.transitions.add(timestamp);
241        }
242        assert!(it.remainder().is_empty());
243        Ok(rest)
244    }
245
246    fn parse_transition_types<'b>(
247        &mut self,
248        header: &Header,
249        bytes: &'b [u8],
250    ) -> Result<&'b [u8], TransitionTypeError> {
251        let (bytes, rest) = try_split_at(
252            SplitAtError::TransitionTypes,
253            bytes,
254            header.transition_types_len(),
255        )?;
256        // We skip the first transition because it is our minimum dummy
257        // transition.
258        for (transition_index, &type_index) in (1..).zip(bytes) {
259            if usize::from(type_index) >= header.tzh_typecnt {
260                return Err(TransitionTypeError::ExceedsLocalTimeTypes);
261            }
262            self.transitions.infos[transition_index].type_index = type_index;
263        }
264        Ok(rest)
265    }
266
267    fn parse_local_time_types<'b>(
268        &mut self,
269        header: &Header,
270        bytes: &'b [u8],
271    ) -> Result<&'b [u8], ParseError> {
272        let (bytes, rest) = try_split_at(
273            SplitAtError::LocalTimeTypes,
274            bytes,
275            header.local_time_types_len()?,
276        )?;
277        let mut it = bytes.chunks_exact(6);
278        for chunk in &mut it {
279            let offset_seconds = from_be_bytes_i32(&chunk[..4]);
280            let offset =
281                Offset::from_seconds(offset_seconds).map_err(|_| {
282                    LocalTimeTypeError::InvalidOffset {
283                        offset: offset_seconds,
284                    }
285                })?;
286            let dst = Dst::from(chunk[4] == 1);
287            let designation = chunk[5];
288            self.types.push(LocalTimeType {
289                offset,
290                dst,
291                designation,
292                indicator: Indicator::LocalWall,
293            });
294        }
295        assert!(it.remainder().is_empty());
296        Ok(rest)
297    }
298
299    fn parse_time_zone_designations<'b>(
300        &mut self,
301        header: &Header,
302        bytes: &'b [u8],
303    ) -> Result<&'b [u8], TimeZoneDesignatorError> {
304        let (bytes, rest) = try_split_at(
305            SplitAtError::TimeZoneDesignations,
306            bytes,
307            header.time_zone_designations_len(),
308        )?;
309        let designations = String::from_utf8(bytes.to_vec())
310            .map_err(|_| TimeZoneDesignatorError::InvalidUtf8)?;
311        for type_index in 0..self.types.len() {
312            let start = usize::from(self.types[type_index].designation);
313            let suffix = designations
314                .get(start..)
315                .ok_or(TimeZoneDesignatorError::InvalidStart)?;
316            let len = suffix
317                .find('\x00')
318                .ok_or(TimeZoneDesignatorError::MissingNul)?;
319            let end = start
320                .checked_add(len)
321                .ok_or(TimeZoneDesignatorError::InvalidLength)?;
322            u8::try_from(end)
323                .map_err(|_| TimeZoneDesignatorError::InvalidEnd)?;
324            let abbreviation = &designations[start..end];
325            // This unwrap is OK because if this fails, it implies that there
326            // must be an `end` offset greater than 256. But the error check
327            // above handles that case.
328            let designation = self
329                .find_or_create_designation(abbreviation)
330                .expect("there are at most 256 local time types");
331            self.types[type_index].designation = designation;
332        }
333        Ok(rest)
334    }
335
336    /// Parses leap second corrections in TZif data.
337    ///
338    /// Jiff ignores leap seconds, so this only validates that the byte ranges
339    /// described by the header are well-formed.
340    fn parse_leap_seconds<'b>(
341        &mut self,
342        header: &Header,
343        bytes: &'b [u8],
344    ) -> Result<&'b [u8], ParseError> {
345        let (bytes, rest) = try_split_at(
346            SplitAtError::LeapSeconds,
347            bytes,
348            header.leap_second_len()?,
349        )?;
350        // OK because `time_size` is always `4` or `8`.
351        let chunk_len = header.time_size.wrapping_add(4);
352        let mut it = bytes.chunks_exact(chunk_len);
353        for chunk in &mut it {
354            let (occur_bytes, _corr_bytes) = chunk.split_at(header.time_size);
355            let occur = if header.is_32bit() {
356                i64::from(from_be_bytes_i32(occur_bytes))
357            } else {
358                from_be_bytes_i64(occur_bytes)
359            };
360            if !(TIMESTAMP_MIN <= occur && occur <= TIMESTAMP_MAX) {
361                warn!(
362                    "leap second occurrence `{occur}` is \
363                     not in Jiff's supported range"
364                )
365            }
366        }
367        assert!(it.remainder().is_empty());
368        Ok(rest)
369    }
370
371    fn parse_indicators<'b>(
372        &mut self,
373        header: &Header,
374        bytes: &'b [u8],
375    ) -> Result<&'b [u8], IndicatorError> {
376        let (std_wall_bytes, rest) = try_split_at(
377            SplitAtError::StandardWallIndicators,
378            bytes,
379            header.standard_wall_len(),
380        )?;
381        let (ut_local_bytes, rest) = try_split_at(
382            SplitAtError::UTLocalIndicators,
383            rest,
384            header.ut_local_len(),
385        )?;
386        if std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
387            if ut_local_bytes.iter().any(|&byte| byte != 0) {
388                return Err(IndicatorError::UtLocalNonZero);
389            }
390        } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() {
391            for (i, &byte) in std_wall_bytes.iter().enumerate() {
392                self.types[i].indicator = if byte == 0 {
393                    Indicator::LocalWall
394                } else if byte == 1 {
395                    Indicator::LocalStandard
396                } else {
397                    return Err(IndicatorError::InvalidStdWallIndicator);
398                };
399            }
400        } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
401            assert_eq!(std_wall_bytes.len(), ut_local_bytes.len());
402            let it = std_wall_bytes.iter().zip(ut_local_bytes);
403            for (i, (&stdwall, &utlocal)) in it.enumerate() {
404                self.types[i].indicator = match (stdwall, utlocal) {
405                    (0, 0) => Indicator::LocalWall,
406                    (1, 0) => Indicator::LocalStandard,
407                    (1, 1) => Indicator::UTStandard,
408                    (0, 1) => {
409                        return Err(IndicatorError::InvalidUtWallCombination);
410                    }
411                    _ => return Err(IndicatorError::InvalidCombination),
412                };
413            }
414        } else {
415            debug_assert!(std_wall_bytes.is_empty());
416            debug_assert!(ut_local_bytes.is_empty());
417        }
418        Ok(rest)
419    }
420
421    fn parse_footer<'b>(
422        &mut self,
423        _header: &Header,
424        bytes: &'b [u8],
425    ) -> Result<&'b [u8], FooterError> {
426        if bytes.is_empty() {
427            return Err(FooterError::UnexpectedEnd);
428        }
429        if bytes[0] != b'\n' {
430            return Err(FooterError::MismatchEnd);
431        }
432        let bytes = &bytes[1..];
433        // Only scan up to 1KB for a newline terminator in case we somehow got
434        // passed a huge block of bytes.
435        let toscan = &bytes[..bytes.len().min(1024)];
436        let nlat = toscan
437            .iter()
438            .position(|&b| b == b'\n')
439            .ok_or(FooterError::TerminatorNotFound)?;
440        let (bytes, rest) = bytes.split_at(nlat);
441        if !bytes.is_empty() {
442            // We could in theory limit TZ strings to their strict POSIX
443            // definition here for TZif V2, but I don't think there is any
444            // harm in allowing the extensions in V2 formatted TZif data. Note
445            // that GNU tooling allows it via the `TZ` environment variable
446            // even though POSIX doesn't specify it. This all seems okay to me
447            // because the V3+ extension is a strict superset of functionality.
448            let posix_tz = posix::TimeZone::parse(bytes)
449                .map_err(FooterError::InvalidPosixTz)?;
450            self.fixed.posix_tz = Some(posix_tz);
451        }
452        Ok(&rest[1..])
453    }
454
455    /// Validates that the POSIX TZ string we parsed, if present, is
456    /// consistent with the last transition in this time zone.
457    fn verify_posix_time_zone_consistency(
458        &self,
459    ) -> Result<(), InconsistentPosixTimeZoneError> {
460        if self.transitions.timestamps.len() <= 1 {
461            return Ok(());
462        }
463        let Some(ref tz) = self.fixed.posix_tz else {
464            return Ok(());
465        };
466        let last = self
467            .transitions
468            .timestamps
469            .last()
470            .expect("last transition timestamp");
471        let type_index = self
472            .transitions
473            .infos
474            .last()
475            .expect("last transition info")
476            .type_index;
477        let typ = &self.types[usize::from(type_index)];
478        let timestamp = crate::Timestamp::from_second(last.as_second())
479            .expect("TZif timestamps are in range");
480        let info = tz.to_offset_info(timestamp);
481        if info.offset() != typ.offset {
482            return Err(InconsistentPosixTimeZoneError::Offset);
483        }
484        if info.dst() != typ.dst {
485            return Err(InconsistentPosixTimeZoneError::Dst);
486        }
487        if info.abbreviation().as_ref() != self.designation(typ) {
488            return Err(InconsistentPosixTimeZoneError::Designation);
489        }
490        Ok(())
491    }
492
493    /// Add civil datetimes to our transitions.
494    ///
495    /// This speeds up time zone lookups when the input is a civil datetime.
496    fn add_civil_datetimes_to_transitions(&mut self) {
497        let trans = &mut self.transitions;
498        trans.infos[0].kind = TransitionKind::Unambiguous;
499        trans.civil_starts[0] = DateTime::MIN;
500        trans.civil_ends[0] = DateTime::MIN;
501        for i in 1..trans.timestamps.len() {
502            let timestamp = trans.timestamps[i];
503            let offset = {
504                let type_index = trans.infos[i].type_index;
505                self.types[usize::from(type_index)].offset
506            };
507            let prev_offset = {
508                let type_index = trans.infos[i.saturating_sub(1)].type_index;
509                self.types[usize::from(type_index)].offset
510            };
511
512            if prev_offset == offset {
513                let start = timestamp.to_datetime(prev_offset);
514                trans.infos[i].kind = TransitionKind::Unambiguous;
515                trans.civil_starts[i] = start;
516                trans.civil_ends[i] = start;
517            } else if prev_offset < offset {
518                trans.infos[i].kind = TransitionKind::Gap;
519                trans.civil_starts[i] = timestamp.to_datetime(prev_offset);
520                trans.civil_ends[i] = timestamp.to_datetime(offset);
521            } else {
522                debug_assert!(prev_offset > offset);
523                trans.infos[i].kind = TransitionKind::Fold;
524                trans.civil_starts[i] = timestamp.to_datetime(offset);
525                trans.civil_ends[i] = timestamp.to_datetime(prev_offset);
526            }
527        }
528    }
529
530    /// Fatten up this TZif data with additional transitions from the POSIX
531    /// time zone footer.
532    fn fatten(&mut self) {
533        // We only fatten up TZif data when requested.
534        if !cfg!(feature = "tz-fat") {
535            return;
536        }
537        let Some(posix_tz) = self.fixed.posix_tz.clone() else {
538            return;
539        };
540        let Some(&last) = self.transitions.timestamps.last() else { return };
541        let mut i = 0;
542        let mut next = last;
543        loop {
544            if i > FATTEN_MAX_TRANSITIONS {
545                warn!(
546                    "fattening TZif data for somehow generated more than \
547                     {max} transitions, so giving up to avoid \
548                     doing too much work",
549                    max = FATTEN_MAX_TRANSITIONS,
550                );
551                return;
552            }
553            i += 1;
554            next = match self.fatten_add_transition(&posix_tz, next) {
555                None => break,
556                Some(next) => next,
557            };
558        }
559    }
560
561    fn fatten_add_transition(
562        &mut self,
563        posix_tz: &posix::TimeZone,
564        prev: Timestamp,
565    ) -> Option<Timestamp> {
566        let prev = crate::Timestamp::from_second(prev.as_second()).ok()?;
567        let trans = posix_tz.next_transition(prev)?;
568        if trans.timestamp().to_datetime(Offset::UTC).date().year()
569            >= FATTEN_UP_TO_YEAR
570        {
571            return None;
572        }
573        let type_index = self.find_or_create_local_time_type(
574            trans.offset(),
575            trans.abbreviation().as_ref(),
576            trans.dst(),
577        )?;
578        self.transitions.add_with_type_index(
579            Timestamp::new(trans.timestamp()),
580            type_index,
581        );
582        Some(Timestamp::new(trans.timestamp()))
583    }
584
585    fn find_or_create_local_time_type(
586        &mut self,
587        offset: Offset,
588        abbrev: &str,
589        dst: Dst,
590    ) -> Option<u8> {
591        for (i, typ) in self.types.iter().enumerate() {
592            if offset == typ.offset
593                && abbrev == self.designation(typ)
594                && dst == typ.dst
595            {
596                return u8::try_from(i).ok();
597            }
598        }
599        let i = u8::try_from(self.types.len()).ok()?;
600        let designation = self.find_or_create_designation(abbrev)?;
601        self.types.push(LocalTimeType {
602            offset,
603            dst,
604            designation,
605            indicator: Indicator::LocalWall,
606        });
607        Some(i)
608    }
609
610    /// Looks for a designation matching `needle` and returns its offset.
611    ///
612    /// If one is not found, then a new designation is added and its new offset
613    /// is returned.
614    ///
615    /// If the offset of the designation exceeds `u8::MAX`, then this returns
616    /// `None`. This is not possible for designations in a RFC 9636 TZif file,
617    /// since their offsets are always a single byte big. However, it is
618    /// theoretically possible for this to return `None` on conforming data
619    /// when fattening is enabled. In which case, it's possible for new local
620    /// time types to be added beyond the maximum. But this would require its
621    /// POSIX time zone to use a local time type not found in the file already.
622    fn find_or_create_designation(&mut self, needle: &str) -> Option<u8> {
623        if let Some(i) =
624            self.fixed.designations.iter().position(|abbrev| abbrev == needle)
625        {
626            return u8::try_from(i).ok();
627        }
628        let i = u8::try_from(self.fixed.designations.len()).ok()?;
629        self.fixed.designations.push(Abbreviation::new_or_heap(needle));
630        Some(i)
631    }
632
633    fn designation(&self, typ: &LocalTimeType) -> &str {
634        &self.fixed.designations[usize::from(typ.designation)]
635    }
636}
637
638impl ParsedTransitions {
639    fn add(&mut self, timestamp: Timestamp) {
640        self.add_with_type_index(timestamp, 0);
641    }
642
643    fn add_with_type_index(&mut self, timestamp: Timestamp, type_index: u8) {
644        self.timestamps.push(timestamp);
645        self.civil_starts.push(DateTime::MIN);
646        self.civil_ends.push(DateTime::MIN);
647        self.infos.push(TransitionInfo {
648            type_index,
649            kind: TransitionKind::Unambiguous,
650        });
651    }
652}
653
654/// The header for a TZif formatted file.
655///
656/// V2+ TZif files have two headers: one for V1 data, and then a second
657/// following the V1 data block that describes another data block which uses
658/// 64-bit timestamps.
659#[derive(Debug)]
660struct Header {
661    time_size: usize,
662    version: u8,
663    tzh_ttisutcnt: usize,
664    tzh_ttisstdcnt: usize,
665    tzh_leapcnt: usize,
666    tzh_timecnt: usize,
667    tzh_typecnt: usize,
668    tzh_charcnt: usize,
669}
670
671impl Header {
672    fn parse(
673        time_size: usize,
674        bytes: &[u8],
675    ) -> Result<(Header, &[u8]), HeaderError> {
676        assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8");
677        if bytes.len() < 44 {
678            return Err(HeaderError::TooShort);
679        }
680        let (magic, rest) = bytes.split_at(4);
681        if magic != b"TZif" {
682            return Err(HeaderError::MismatchMagic);
683        }
684        let (version, rest) = rest.split_at(1);
685        let (_reserved, rest) = rest.split_at(15);
686
687        let (tzh_ttisutcnt_bytes, rest) = rest.split_at(4);
688        let (tzh_ttisstdcnt_bytes, rest) = rest.split_at(4);
689        let (tzh_leapcnt_bytes, rest) = rest.split_at(4);
690        let (tzh_timecnt_bytes, rest) = rest.split_at(4);
691        let (tzh_typecnt_bytes, rest) = rest.split_at(4);
692        let (tzh_charcnt_bytes, rest) = rest.split_at(4);
693
694        let tzh_ttisutcnt =
695            from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes).map_err(|e| {
696                HeaderError::ParseCount { kind: CountKind::Ut, convert: e }
697            })?;
698        let tzh_ttisstdcnt =
699            from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes).map_err(|e| {
700                HeaderError::ParseCount { kind: CountKind::Std, convert: e }
701            })?;
702        let tzh_leapcnt =
703            from_be_bytes_u32_to_usize(tzh_leapcnt_bytes).map_err(|e| {
704                HeaderError::ParseCount { kind: CountKind::Leap, convert: e }
705            })?;
706        let tzh_timecnt =
707            from_be_bytes_u32_to_usize(tzh_timecnt_bytes).map_err(|e| {
708                HeaderError::ParseCount { kind: CountKind::Time, convert: e }
709            })?;
710        let tzh_typecnt =
711            from_be_bytes_u32_to_usize(tzh_typecnt_bytes).map_err(|e| {
712                HeaderError::ParseCount { kind: CountKind::Type, convert: e }
713            })?;
714        let tzh_charcnt =
715            from_be_bytes_u32_to_usize(tzh_charcnt_bytes).map_err(|e| {
716                HeaderError::ParseCount { kind: CountKind::Char, convert: e }
717            })?;
718
719        if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt {
720            return Err(HeaderError::MismatchUtType);
721        }
722        if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt {
723            return Err(HeaderError::MismatchStdType);
724        }
725        if tzh_typecnt < 1 {
726            return Err(HeaderError::ZeroType);
727        }
728        if tzh_charcnt < 1 {
729            return Err(HeaderError::ZeroChar);
730        }
731
732        let header = Header {
733            time_size,
734            version: version[0],
735            tzh_ttisutcnt,
736            tzh_ttisstdcnt,
737            tzh_leapcnt,
738            tzh_timecnt,
739            tzh_typecnt,
740            tzh_charcnt,
741        };
742        Ok((header, rest))
743    }
744
745    fn is_32bit(&self) -> bool {
746        self.time_size == 4
747    }
748
749    fn data_block_len(&self) -> Result<usize, HeaderError> {
750        let a = self.transition_times_len()?;
751        let b = self.transition_types_len();
752        let c = self.local_time_types_len()?;
753        let d = self.time_zone_designations_len();
754        let e = self.leap_second_len()?;
755        let f = self.standard_wall_len();
756        let g = self.ut_local_len();
757        a.checked_add(b)
758            .and_then(|z| z.checked_add(c))
759            .and_then(|z| z.checked_add(d))
760            .and_then(|z| z.checked_add(e))
761            .and_then(|z| z.checked_add(f))
762            .and_then(|z| z.checked_add(g))
763            .ok_or(HeaderError::InvalidDataBlock { version: self.version })
764    }
765
766    fn transition_times_len(&self) -> Result<usize, HeaderError> {
767        self.tzh_timecnt
768            .checked_mul(self.time_size)
769            .ok_or(HeaderError::InvalidTimeCount)
770    }
771
772    fn transition_types_len(&self) -> usize {
773        self.tzh_timecnt
774    }
775
776    fn local_time_types_len(&self) -> Result<usize, HeaderError> {
777        self.tzh_typecnt.checked_mul(6).ok_or(HeaderError::InvalidTypeCount)
778    }
779
780    fn time_zone_designations_len(&self) -> usize {
781        self.tzh_charcnt
782    }
783
784    fn leap_second_len(&self) -> Result<usize, HeaderError> {
785        let record_len = self
786            .time_size
787            .checked_add(4)
788            .expect("4-or-8 plus 4 always fits in usize");
789        self.tzh_leapcnt
790            .checked_mul(record_len)
791            .ok_or(HeaderError::InvalidLeapSecondCount)
792    }
793
794    fn standard_wall_len(&self) -> usize {
795        self.tzh_ttisstdcnt
796    }
797
798    fn ut_local_len(&self) -> usize {
799        self.tzh_ttisutcnt
800    }
801}
802
803/// An error that can occur when parsing TZif data.
804///
805/// This is only available when the `alloc` feature is enabled because parsing
806/// TZif data requires heap allocation.
807#[derive(Clone, Debug, Eq, PartialEq)]
808#[cfg_attr(feature = "defmt", derive(defmt::Format))]
809pub struct ParseError {
810    kind: ParseErrorKind,
811}
812
813#[derive(Clone, Debug, Eq, PartialEq)]
814#[cfg_attr(feature = "defmt", derive(defmt::Format))]
815enum ParseErrorKind {
816    Footer(FooterError),
817    Header(HeaderError),
818    Header32(HeaderError),
819    Header64(HeaderError),
820    InconsistentPosixTimeZone(InconsistentPosixTimeZoneError),
821    Indicator(IndicatorError),
822    LocalTimeType(LocalTimeTypeError),
823    SplitAt(SplitAtError),
824    TimeZoneDesignator(TimeZoneDesignatorError),
825    TransitionType(TransitionTypeError),
826}
827
828#[cfg(feature = "std")]
829impl std::error::Error for ParseError {}
830
831impl core::fmt::Display for ParseError {
832    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
833        use self::ParseErrorKind::*;
834        match self.kind {
835            Footer(ref err) => {
836                f.write_str("invalid TZif footer: ")?;
837                err.fmt(f)
838            }
839            Header(ref err) => {
840                f.write_str("invalid TZif header: ")?;
841                err.fmt(f)
842            }
843            Header32(ref err) => {
844                f.write_str("invalid 32-bit TZif header: ")?;
845                err.fmt(f)
846            }
847            Header64(ref err) => {
848                f.write_str("invalid 64-bit TZif header: ")?;
849                err.fmt(f)
850            }
851            InconsistentPosixTimeZone(ref err) => {
852                f.write_str(
853                    "found inconsistency with POSIX time zone transition \
854                     rule in TZif file footer: ",
855                )?;
856                err.fmt(f)
857            }
858            Indicator(ref err) => {
859                f.write_str("failed to parse indicators: ")?;
860                err.fmt(f)
861            }
862            LocalTimeType(ref err) => {
863                f.write_str("failed to parse local time types: ")?;
864                err.fmt(f)
865            }
866            SplitAt(ref err) => err.fmt(f),
867            TimeZoneDesignator(ref err) => {
868                f.write_str("failed to parse time zone designators: ")?;
869                err.fmt(f)
870            }
871            TransitionType(ref err) => {
872                f.write_str("failed to parse time zone transition types: ")?;
873                err.fmt(f)
874            }
875        }
876    }
877}
878
879impl From<ParseErrorKind> for ParseError {
880    fn from(kind: ParseErrorKind) -> ParseError {
881        ParseError { kind }
882    }
883}
884
885impl From<HeaderError> for ParseError {
886    fn from(err: HeaderError) -> ParseError {
887        ParseErrorKind::Header(err).into()
888    }
889}
890
891impl From<FooterError> for ParseError {
892    fn from(err: FooterError) -> ParseError {
893        ParseErrorKind::Footer(err).into()
894    }
895}
896
897impl From<InconsistentPosixTimeZoneError> for ParseError {
898    fn from(err: InconsistentPosixTimeZoneError) -> ParseError {
899        ParseErrorKind::InconsistentPosixTimeZone(err).into()
900    }
901}
902
903impl From<IndicatorError> for ParseError {
904    fn from(err: IndicatorError) -> ParseError {
905        ParseErrorKind::Indicator(err).into()
906    }
907}
908
909impl From<LocalTimeTypeError> for ParseError {
910    fn from(err: LocalTimeTypeError) -> ParseError {
911        ParseErrorKind::LocalTimeType(err).into()
912    }
913}
914
915impl From<SplitAtError> for ParseError {
916    fn from(err: SplitAtError) -> ParseError {
917        ParseErrorKind::SplitAt(err).into()
918    }
919}
920
921impl From<TimeZoneDesignatorError> for ParseError {
922    fn from(err: TimeZoneDesignatorError) -> ParseError {
923        ParseErrorKind::TimeZoneDesignator(err).into()
924    }
925}
926
927impl From<TransitionTypeError> for ParseError {
928    fn from(err: TransitionTypeError) -> ParseError {
929        ParseErrorKind::TransitionType(err).into()
930    }
931}
932
933#[derive(Clone, Debug, Eq, PartialEq)]
934#[cfg_attr(feature = "defmt", derive(defmt::Format))]
935enum TransitionTypeError {
936    ExceedsLocalTimeTypes,
937    Split(SplitAtError),
938}
939
940impl From<SplitAtError> for TransitionTypeError {
941    fn from(err: SplitAtError) -> TransitionTypeError {
942        TransitionTypeError::Split(err)
943    }
944}
945
946impl core::fmt::Display for TransitionTypeError {
947    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
948        use self::TransitionTypeError::*;
949        match *self {
950            ExceedsLocalTimeTypes => f.write_str(
951                "found time zone transition type index that exceeds the \
952                 number of local time types",
953            ),
954            Split(ref err) => err.fmt(f),
955        }
956    }
957}
958
959#[derive(Clone, Debug, Eq, PartialEq)]
960#[cfg_attr(feature = "defmt", derive(defmt::Format))]
961enum LocalTimeTypeError {
962    InvalidOffset { offset: i32 },
963}
964
965impl core::fmt::Display for LocalTimeTypeError {
966    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
967        use self::LocalTimeTypeError::*;
968        match *self {
969            InvalidOffset { offset } => write!(
970                f,
971                "found local time type with out-of-bounds time zone offset: \
972                 {offset}, Jiff's allowed range is \
973                 `{OFFSET_MIN}..={OFFSET_MAX}`"
974            ),
975        }
976    }
977}
978
979#[derive(Clone, Debug, Eq, PartialEq)]
980#[cfg_attr(feature = "defmt", derive(defmt::Format))]
981enum TimeZoneDesignatorError {
982    InvalidEnd,
983    InvalidLength,
984    InvalidStart,
985    InvalidUtf8,
986    MissingNul,
987    Split(SplitAtError),
988}
989
990impl From<SplitAtError> for TimeZoneDesignatorError {
991    fn from(err: SplitAtError) -> TimeZoneDesignatorError {
992        TimeZoneDesignatorError::Split(err)
993    }
994}
995
996impl core::fmt::Display for TimeZoneDesignatorError {
997    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
998        use self::TimeZoneDesignatorError::*;
999        match *self {
1000            InvalidEnd => f.write_str(
1001                "found invalid end of time zone designator for local time type",
1002            ),
1003            InvalidLength => f.write_str(
1004                "found invalid length of time zone designator for local time \
1005                 type",
1006            ),
1007            InvalidStart => f.write_str(
1008                "found invalid start of time zone designator for local time \
1009                 type",
1010            ),
1011            InvalidUtf8 => {
1012                f.write_str("found invalid UTF-8 in time zone designators")
1013            }
1014            MissingNul => f.write_str(
1015                "could not find NUL terminator for time zone designator",
1016            ),
1017            Split(ref err) => err.fmt(f),
1018        }
1019    }
1020}
1021
1022#[derive(Clone, Debug, Eq, PartialEq)]
1023#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1024enum IndicatorError {
1025    InvalidCombination,
1026    InvalidStdWallIndicator,
1027    InvalidUtWallCombination,
1028    Split(SplitAtError),
1029    UtLocalNonZero,
1030}
1031
1032impl From<SplitAtError> for IndicatorError {
1033    fn from(err: SplitAtError) -> IndicatorError {
1034        IndicatorError::Split(err)
1035    }
1036}
1037
1038impl core::fmt::Display for IndicatorError {
1039    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1040        use self::IndicatorError::*;
1041        match *self {
1042            InvalidCombination => f.write_str(
1043                "found invalid std/wall or UT/local value for local time type, \
1044                 each must be 0 or 1",
1045            ),
1046            InvalidStdWallIndicator => f.write_str(
1047                "found invalid std/wall indicator, expected it to be 0 or 1",
1048            ),
1049            InvalidUtWallCombination => f.write_str(
1050                "found invalid UT-wall combination for local time type, only \
1051                 local-wall, local-standard and UT-standard are allowed",
1052            ),
1053            Split(ref err) => err.fmt(f),
1054            UtLocalNonZero => f.write_str(
1055                "found non-zero UT/local indicator, but all such indicators \
1056                 should be zero",
1057            ),
1058        }
1059    }
1060}
1061
1062#[derive(Clone, Debug, Eq, PartialEq)]
1063#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1064enum InconsistentPosixTimeZoneError {
1065    Designation,
1066    Dst,
1067    Offset,
1068}
1069
1070impl core::fmt::Display for InconsistentPosixTimeZoneError {
1071    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1072        use self::InconsistentPosixTimeZoneError::*;
1073        match *self {
1074            Designation => f.write_str(
1075                "expected last transition in TZif file to have a time zone \
1076                 abbreviation matching the abbreviation derived from the POSIX \
1077                 time zone transition rule",
1078            ),
1079            Dst => f.write_str(
1080                "expected last transition in TZif file to have a DST status \
1081                 matching the status derived from the POSIX time zone \
1082                 transition rule",
1083            ),
1084            Offset => f.write_str(
1085                "expected last transition in TZif file to have DST offset \
1086                 matching the offset derived from the POSIX time zone \
1087                 transition rule",
1088            ),
1089        }
1090    }
1091}
1092
1093#[derive(Clone, Debug, Eq, PartialEq)]
1094#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1095enum FooterError {
1096    InvalidPosixTz(posix::ParseError),
1097    MismatchEnd,
1098    TerminatorNotFound,
1099    UnexpectedEnd,
1100}
1101
1102impl core::fmt::Display for FooterError {
1103    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1104        use self::FooterError::*;
1105        match *self {
1106            InvalidPosixTz(ref err) => {
1107                f.write_str("invalid POSIX time zone transition rule")?;
1108                core::fmt::Display::fmt(err, f)
1109            }
1110            MismatchEnd => f.write_str(
1111                "expected to find `\\n` at the beginning of the TZif file \
1112                 footer, but found something else instead",
1113            ),
1114            TerminatorNotFound => f.write_str(
1115                "expected to find `\\n` terminating the TZif file footer, but \
1116                 no line terminator could be found",
1117            ),
1118            UnexpectedEnd => f.write_str(
1119                "expected to find `\\n` at the beginning of the TZif file \
1120                 footer, but found unexpected end of data",
1121            ),
1122        }
1123    }
1124}
1125
1126#[derive(Clone, Debug, Eq, PartialEq)]
1127#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1128enum HeaderError {
1129    InvalidDataBlock { version: u8 },
1130    InvalidLeapSecondCount,
1131    InvalidTimeCount,
1132    InvalidTypeCount,
1133    MismatchMagic,
1134    MismatchStdType,
1135    MismatchUtType,
1136    ParseCount { kind: CountKind, convert: U32UsizeError },
1137    TooShort,
1138    ZeroChar,
1139    ZeroType,
1140}
1141
1142impl core::fmt::Display for HeaderError {
1143    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1144        use self::HeaderError::*;
1145        match *self {
1146            InvalidDataBlock { version } => write!(
1147                f,
1148                "length of data block in V{version} TZif file is too big",
1149            ),
1150            InvalidLeapSecondCount => {
1151                f.write_str("number of leap seconds is too big")
1152            }
1153            InvalidTimeCount => {
1154                f.write_str("number of transition times is too big")
1155            }
1156            InvalidTypeCount => {
1157                f.write_str("number of local time types is too big")
1158            }
1159            MismatchMagic => f.write_str("magic bytes mismatch"),
1160            MismatchStdType => f.write_str(
1161                "expected number of standard/wall indicators to be zero or \
1162                 equal to the number of local time types",
1163            ),
1164            MismatchUtType => f.write_str(
1165                "expected number of UT/local indicators to be zero or equal \
1166                 to the number of local time types",
1167            ),
1168            ParseCount { ref kind, ref convert } => {
1169                write!(f, "failed to parse `{kind}`: {convert}")
1170            }
1171            TooShort => f.write_str("too short"),
1172            ZeroChar => f.write_str(
1173                "expected number of time zone abbreviations to be at least 1",
1174            ),
1175            ZeroType => f.write_str(
1176                "expected number of local time types to be at least 1",
1177            ),
1178        }
1179    }
1180}
1181
1182#[derive(Clone, Debug, Eq, PartialEq)]
1183#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1184enum CountKind {
1185    Ut,
1186    Std,
1187    Leap,
1188    Time,
1189    Type,
1190    Char,
1191}
1192
1193impl core::fmt::Display for CountKind {
1194    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1195        use self::CountKind::*;
1196        match *self {
1197            Ut => f.write_str("tzh_ttisutcnt"),
1198            Std => f.write_str("tzh_ttisstdcnt"),
1199            Leap => f.write_str("tzh_leapcnt"),
1200            Time => f.write_str("tzh_timecnt"),
1201            Type => f.write_str("tzh_typecnt"),
1202            Char => f.write_str("tzh_charcnt"),
1203        }
1204    }
1205}
1206
1207#[derive(Clone, Debug, Eq, PartialEq)]
1208#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1209enum SplitAtError {
1210    V1,
1211    LeapSeconds,
1212    LocalTimeTypes,
1213    StandardWallIndicators,
1214    TimeZoneDesignations,
1215    TransitionTimes,
1216    TransitionTypes,
1217    UTLocalIndicators,
1218}
1219
1220impl core::fmt::Display for SplitAtError {
1221    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1222        use self::SplitAtError::*;
1223        f.write_str("expected bytes for '")?;
1224        f.write_str(match *self {
1225            V1 => "v1 TZif",
1226            LeapSeconds => "leap seconds",
1227            LocalTimeTypes => "local time types",
1228            StandardWallIndicators => "standard/wall indicators",
1229            TimeZoneDesignations => "time zone designations",
1230            TransitionTimes => "transition times",
1231            TransitionTypes => "transition types",
1232            UTLocalIndicators => "UT/local indicators",
1233        })?;
1234        f.write_str("data block', but did not find enough bytes")?;
1235        Ok(())
1236    }
1237}
1238
1239#[derive(Clone, Debug, Eq, PartialEq)]
1240#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1241struct U32UsizeError;
1242
1243impl core::fmt::Display for U32UsizeError {
1244    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1245        write!(
1246            f,
1247            "failed to parse integer because it is bigger than `{max}`",
1248            max = usize::MAX,
1249        )
1250    }
1251}
1252
1253fn try_split_at<'b>(
1254    what: SplitAtError,
1255    bytes: &'b [u8],
1256    at: usize,
1257) -> Result<(&'b [u8], &'b [u8]), SplitAtError> {
1258    if at > bytes.len() {
1259        Err(what)
1260    } else {
1261        Ok(bytes.split_at(at))
1262    }
1263}
1264
1265fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result<usize, U32UsizeError> {
1266    let n = from_be_bytes_u32(bytes);
1267    usize::try_from(n).map_err(|_| U32UsizeError)
1268}
1269
1270fn from_be_bytes_u32(bytes: &[u8]) -> u32 {
1271    u32::from_be_bytes(bytes.try_into().unwrap())
1272}
1273
1274fn from_be_bytes_i32(bytes: &[u8]) -> i32 {
1275    i32::from_be_bytes(bytes.try_into().unwrap())
1276}
1277
1278fn from_be_bytes_i64(bytes: &[u8]) -> i64 {
1279    i64::from_be_bytes(bytes.try_into().unwrap())
1280}
1281
1282// These are some basic sanity tests on the parser. The real tests come in the
1283// parent module where we test the transitions themselves.
1284#[cfg(test)]
1285mod tests {
1286    use alloc::vec::Vec;
1287
1288    use super::*;
1289
1290    fn push_u32(bytes: &mut Vec<u8>, n: u32) {
1291        bytes.extend_from_slice(&n.to_be_bytes());
1292    }
1293
1294    fn push_header(bytes: &mut Vec<u8>, version: u8) {
1295        bytes.extend_from_slice(b"TZif");
1296        bytes.push(version);
1297        bytes.extend_from_slice(&[0; 15]);
1298        push_u32(bytes, 0); // tzh_ttisutcnt
1299        push_u32(bytes, 0); // tzh_ttisstdcnt
1300        push_u32(bytes, 0); // tzh_leapcnt
1301        push_u32(bytes, 0); // tzh_timecnt
1302        push_u32(bytes, 1); // tzh_typecnt
1303        push_u32(bytes, 4); // tzh_charcnt
1304    }
1305
1306    fn push_utc_type_and_designation(bytes: &mut Vec<u8>) {
1307        bytes.extend_from_slice(&0i32.to_be_bytes());
1308        bytes.push(0); // isdst
1309        bytes.push(0); // designation index
1310        bytes.extend_from_slice(b"UTC\0");
1311    }
1312
1313    fn minimal_v1() -> Vec<u8> {
1314        let mut bytes = vec![];
1315        push_header(&mut bytes, 0);
1316        push_utc_type_and_designation(&mut bytes);
1317        bytes
1318    }
1319
1320    fn minimal_v2(footer: &[u8]) -> Vec<u8> {
1321        let mut bytes = vec![];
1322        push_header(&mut bytes, b'2');
1323        push_utc_type_and_designation(&mut bytes);
1324        push_header(&mut bytes, b'2');
1325        push_utc_type_and_designation(&mut bytes);
1326        bytes.push(b'\n');
1327        bytes.extend_from_slice(footer);
1328        bytes.push(b'\n');
1329        bytes
1330    }
1331
1332    #[test]
1333    fn parse_minimal_v1() {
1334        let tzif = TimeZone::parse(&minimal_v1()).unwrap();
1335        assert_eq!(tzif.version, 0);
1336        assert_eq!(tzif.designations.as_ref(), &[Abbreviation::array("UTC")]);
1337        assert_eq!(tzif.types.len(), 1);
1338        assert_eq!(tzif.types[0].offset, Offset::UTC);
1339        assert_eq!(tzif.transitions.timestamps.len(), 1);
1340        assert_eq!(tzif.transitions.timestamps[0], Timestamp::MIN);
1341        assert_eq!(tzif.transitions.civil_starts[0], DateTime::MIN);
1342    }
1343
1344    #[test]
1345    fn parse_minimal_v2() {
1346        let tzif = TimeZone::parse(&minimal_v2(b"UTC0")).unwrap();
1347        assert_eq!(tzif.version, b'2');
1348        assert!(tzif.posix_tz.is_some());
1349        assert_eq!(tzif.designations.as_ref(), &[Abbreviation::array("UTC")]);
1350        assert_eq!(tzif.types.len(), 1);
1351        assert_eq!(tzif.transitions.timestamps.len(), 1);
1352    }
1353
1354    #[test]
1355    fn parse_minimal_v2_with_and_without_tz_fat() {
1356        let bytes = minimal_v2(b"EST5EDT,M3.2.0,M11.1.0");
1357        let tzif = TimeZone::parse(&bytes).unwrap();
1358        let expected = if cfg!(feature = "tz-fat") { 302 } else { 1 };
1359        assert_eq!(tzif.transitions.timestamps.len(), expected);
1360    }
1361}