Skip to main content

jiff/shared/
tzif.rs

1use alloc::{string::String, vec};
2
3use super::{
4    util::{
5        array_str::Abbreviation,
6        itime::{IOffset, ITimestamp},
7    },
8    PosixTimeZone, TzifDateTime, TzifFixed, TzifIndicator, TzifLocalTimeType,
9    TzifOwned, TzifTransitionInfo, TzifTransitionKind, TzifTransitions,
10    TzifTransitionsOwned,
11};
12
13// These are Jiff min and max timestamp (in seconds) values.
14//
15// The TZif parser will clamp timestamps to this range. It's
16// not ideal, but Jiff can't handle values outside of this range
17// and completely refusing to use TZif data with pathological
18// timestamps in typically irrelevant transitions is bad juju.
19//
20// Ref: https://github.com/BurntSushi/jiff/issues/163
21// Ref: https://github.com/BurntSushi/jiff/pull/164
22const TIMESTAMP_MIN: i64 = -377705023201;
23const TIMESTAMP_MAX: i64 = 253402207200;
24
25// Similarly for offsets, although in this case, if we find
26// an offset outside of this range, we do actually error. This
27// is because it could result in true incorrect datetimes for
28// actual transitions.
29//
30// But our supported offset range is `-25:59:59..=+25:59:59`.
31// There's no real time zone with offsets even close to those
32// boundaries.
33//
34// If there is pathological data that we should ignore, then
35// we should wait for a real bug report in order to determine
36// the right way to ignore/clamp it.
37const OFFSET_MIN: i32 = -93599;
38const OFFSET_MAX: i32 = 93599;
39
40// When fattening TZif data, this is the year to go up to.
41//
42// This year was chosen because it's what the "fat" TZif data generated
43// by `zic` uses.
44const FATTEN_UP_TO_YEAR: i16 = 2038;
45
46// This is a "sanity" limit on the maximum number of transitions we'll
47// add to TZif data when fattening them up.
48//
49// This is mostly just a defense-in-depth limit to avoid weird cases
50// where a pathological POSIX time zone could be defined to create
51// many transitions. It's not clear that this is actually possible,
52// but I felt a little uneasy doing unbounded work that isn't linearly
53// proportional to the input data. So, this limit is put into place for
54// reasons of "good sense."
55//
56// For "normal" cases, there should be at most two transitions per
57// year. So this limit permits 300/2=150 years of transition data.
58// (Although we won't go above `FATTEN_UP_TO_YEAR`. See above.)
59const FATTEN_MAX_TRANSITIONS: usize = 300;
60
61impl TzifOwned {
62    /// Parses the given data as a TZif formatted file.
63    ///
64    /// The name given is attached to the `Tzif` value returned, but is
65    /// otherwise not significant.
66    ///
67    /// If the given data is not recognized to be valid TZif, then an error is
68    /// returned.
69    ///
70    /// In general, callers may assume that it is safe to pass arbitrary or
71    /// even untrusted data to this function and count on it not panicking
72    /// or using resources that aren't limited to a small constant factor of
73    /// the size of the data itself. That is, callers can reliably limit the
74    /// resources used by limiting the size of the data given to this parse
75    /// function.
76    pub(crate) fn parse(
77        name: Option<String>,
78        bytes: &[u8],
79    ) -> Result<TzifOwned, TzifError> {
80        let original = bytes;
81        let name = name.into();
82        let (header32, rest) =
83            Header::parse(4, bytes).map_err(TzifErrorKind::Header32)?;
84        let (mut tzif, rest) = if header32.version == 0 {
85            TzifOwned::parse32(name, header32, rest)?
86        } else {
87            TzifOwned::parse64(name, header32, rest)?
88        };
89        tzif.fatten();
90        // This should come after fattening, because fattening may add new
91        // transitions and we want to add civil datetimes to those.
92        tzif.add_civil_datetimes_to_transitions();
93        tzif.verify_posix_time_zone_consistency()?;
94        // Compute the checksum using the entire contents of the TZif data.
95        let tzif_raw_len = (rest.as_ptr() as usize)
96            .checked_sub(original.as_ptr() as usize)
97            .unwrap();
98        let tzif_raw_bytes = &original[..tzif_raw_len];
99        tzif.fixed.checksum = super::crc32::sum(tzif_raw_bytes);
100
101        // Shrink all of our allocs so we don't keep excess capacity around.
102        tzif.fixed.designations.shrink_to_fit();
103        tzif.types.shrink_to_fit();
104        tzif.transitions.timestamps.shrink_to_fit();
105        tzif.transitions.civil_starts.shrink_to_fit();
106        tzif.transitions.civil_ends.shrink_to_fit();
107        tzif.transitions.infos.shrink_to_fit();
108
109        Ok(tzif)
110    }
111
112    fn parse32<'b>(
113        name: Option<String>,
114        header32: Header,
115        bytes: &'b [u8],
116    ) -> Result<(TzifOwned, &'b [u8]), TzifError> {
117        let mut tzif = TzifOwned {
118            fixed: TzifFixed {
119                name,
120                version: header32.version,
121                // filled in later
122                checksum: 0,
123                designations: String::new(),
124                posix_tz: None,
125            },
126            types: vec![],
127            transitions: TzifTransitions {
128                timestamps: vec![],
129                civil_starts: vec![],
130                civil_ends: vec![],
131                infos: vec![],
132            },
133        };
134        let rest = tzif.parse_transitions(&header32, bytes)?;
135        let rest = tzif.parse_transition_types(&header32, rest)?;
136        let rest = tzif.parse_local_time_types(&header32, rest)?;
137        let rest = tzif.parse_time_zone_designations(&header32, rest)?;
138        let rest = tzif.parse_leap_seconds(&header32, rest)?;
139        let rest = tzif.parse_indicators(&header32, rest)?;
140        Ok((tzif, rest))
141    }
142
143    fn parse64<'b>(
144        name: Option<String>,
145        header32: Header,
146        bytes: &'b [u8],
147    ) -> Result<(TzifOwned, &'b [u8]), TzifError> {
148        let (_, rest) =
149            try_split_at(SplitAtError::V1, bytes, header32.data_block_len()?)?;
150        let (header64, rest) =
151            Header::parse(8, rest).map_err(TzifErrorKind::Header64)?;
152        let mut tzif = TzifOwned {
153            fixed: TzifFixed {
154                name,
155                version: header64.version,
156                // filled in later
157                checksum: 0,
158                designations: String::new(),
159                posix_tz: None,
160            },
161            types: vec![],
162            transitions: TzifTransitions {
163                timestamps: vec![],
164                civil_starts: vec![],
165                civil_ends: vec![],
166                infos: vec![],
167            },
168        };
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], TzifError> {
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
199        // 0)."
200        //
201        // So if there are no transitions, pushing this dummy one will result
202        // in the desired behavior even when it's the only transition.
203        // Similarly, since this is the minimum timestamp value, it will
204        // trigger for any times before the first transition found in the TZif
205        // data.
206        self.transitions.add_with_type_index(TIMESTAMP_MIN, 0);
207        while let Some(chunk) = it.next() {
208            let mut timestamp = if header.is_32bit() {
209                i64::from(from_be_bytes_i32(chunk))
210            } else {
211                from_be_bytes_i64(chunk)
212            };
213            if !(TIMESTAMP_MIN <= timestamp && timestamp <= TIMESTAMP_MAX) {
214                // We really shouldn't error here just because the Unix
215                // timestamp is outside what Jiff supports. Since what Jiff
216                // supports is _somewhat_ arbitrary. But Jiff's supported
217                // range is good enough for all realistic purposes, so we
218                // just clamp an out-of-range Unix timestamp to the Jiff
219                // min or max value.
220                //
221                // This can't result in the sorting order being wrong, but
222                // it can result in a transition that is duplicative with
223                // the dummy transition we inserted above. This should be
224                // fine.
225                let clamped = timestamp.clamp(TIMESTAMP_MIN, TIMESTAMP_MAX);
226                // only-jiff-start
227                warn!(
228                    "found Unix timestamp `{timestamp}` that is outside \
229                     Jiff's supported range, clamping to `{clamped}`",
230                );
231                // only-jiff-end
232                timestamp = clamped;
233            }
234            self.transitions.add(timestamp);
235        }
236        assert!(it.remainder().is_empty());
237        Ok(rest)
238    }
239
240    fn parse_transition_types<'b>(
241        &mut self,
242        header: &Header,
243        bytes: &'b [u8],
244    ) -> Result<&'b [u8], TransitionTypeError> {
245        let (bytes, rest) = try_split_at(
246            SplitAtError::TransitionTypes,
247            bytes,
248            header.transition_types_len(),
249        )?;
250        // We skip the first transition because it is our minimum dummy
251        // transition.
252        for (transition_index, &type_index) in (1..).zip(bytes) {
253            if usize::from(type_index) >= header.tzh_typecnt {
254                return Err(TransitionTypeError::ExceedsLocalTimeTypes);
255            }
256            self.transitions.infos[transition_index].type_index = type_index;
257        }
258        Ok(rest)
259    }
260
261    fn parse_local_time_types<'b>(
262        &mut self,
263        header: &Header,
264        bytes: &'b [u8],
265    ) -> Result<&'b [u8], TzifError> {
266        let (bytes, rest) = try_split_at(
267            SplitAtError::LocalTimeTypes,
268            bytes,
269            header.local_time_types_len()?,
270        )?;
271        let mut it = bytes.chunks_exact(6);
272        while let Some(chunk) = it.next() {
273            let offset = from_be_bytes_i32(&chunk[..4]);
274            if !(OFFSET_MIN <= offset && offset <= OFFSET_MAX) {
275                return Err(TzifError::from(
276                    LocalTimeTypeError::InvalidOffset { offset },
277                ));
278            }
279            let is_dst = chunk[4] == 1;
280            let designation = (chunk[5], chunk[5]);
281            self.types.push(TzifLocalTimeType {
282                offset,
283                is_dst,
284                designation,
285                indicator: TzifIndicator::LocalWall,
286            });
287        }
288        assert!(it.remainder().is_empty());
289        Ok(rest)
290    }
291
292    fn parse_time_zone_designations<'b>(
293        &mut self,
294        header: &Header,
295        bytes: &'b [u8],
296    ) -> Result<&'b [u8], TimeZoneDesignatorError> {
297        let (bytes, rest) = try_split_at(
298            SplitAtError::TimeZoneDesignations,
299            bytes,
300            header.time_zone_designations_len(),
301        )?;
302        self.fixed.designations = String::from_utf8(bytes.to_vec())
303            .map_err(|_| TimeZoneDesignatorError::InvalidUtf8)?;
304        // Holy hell, this is brutal. The boundary conditions are crazy.
305        for typ in self.types.iter_mut() {
306            let start = usize::from(typ.designation.0);
307            let suffix = self
308                .fixed
309                .designations
310                .get(start..)
311                .ok_or(TimeZoneDesignatorError::InvalidStart)?;
312            let len = suffix
313                .find('\x00')
314                .ok_or(TimeZoneDesignatorError::MissingNul)?;
315            let end = start
316                .checked_add(len)
317                .ok_or(TimeZoneDesignatorError::InvalidLength)?;
318            typ.designation.1 = u8::try_from(end)
319                .map_err(|_| TimeZoneDesignatorError::InvalidEnd)?;
320        }
321        Ok(rest)
322    }
323
324    /// This parses the leap second corrections in the TZif data.
325    ///
326    /// Note that we only parse and verify them. We don't actually use them.
327    /// Jiff effectively ignores leap seconds.
328    fn parse_leap_seconds<'b>(
329        &mut self,
330        header: &Header,
331        bytes: &'b [u8],
332    ) -> Result<&'b [u8], TzifError> {
333        let (bytes, rest) = try_split_at(
334            SplitAtError::LeapSeconds,
335            bytes,
336            header.leap_second_len()?,
337        )?;
338        let chunk_len = header
339            .time_size
340            .checked_add(4)
341            .expect("time_size plus 4 fits in usize");
342        let mut it = bytes.chunks_exact(chunk_len);
343        while let Some(chunk) = it.next() {
344            let (occur_bytes, _corr_bytes) = chunk.split_at(header.time_size);
345            let occur = if header.is_32bit() {
346                i64::from(from_be_bytes_i32(occur_bytes))
347            } else {
348                from_be_bytes_i64(occur_bytes)
349            };
350            if !(TIMESTAMP_MIN <= occur && occur <= TIMESTAMP_MAX) {
351                // only-jiff-start
352                warn!(
353                    "leap second occurrence `{occur}` is \
354                     not in Jiff's supported range"
355                )
356                // only-jiff-end
357            }
358        }
359        assert!(it.remainder().is_empty());
360        Ok(rest)
361    }
362
363    fn parse_indicators<'b>(
364        &mut self,
365        header: &Header,
366        bytes: &'b [u8],
367    ) -> Result<&'b [u8], IndicatorError> {
368        let (std_wall_bytes, rest) = try_split_at(
369            SplitAtError::StandardWallIndicators,
370            bytes,
371            header.standard_wall_len(),
372        )?;
373        let (ut_local_bytes, rest) = try_split_at(
374            SplitAtError::UTLocalIndicators,
375            rest,
376            header.ut_local_len(),
377        )?;
378        if std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
379            // This is a weird case, but technically possible only if all
380            // UT/local indicators are 0. If any are 1, then it's an error,
381            // because it would require the corresponding std/wall indicator
382            // to be 1 too. Which it can't be, because there aren't any. So
383            // we just check that they're all zeros.
384            if ut_local_bytes.iter().any(|&byte| byte != 0) {
385                return Err(IndicatorError::UtLocalNonZero);
386            }
387        } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() {
388            for (i, &byte) in std_wall_bytes.iter().enumerate() {
389                // Indexing is OK because Header guarantees that the number of
390                // indicators is 0 or equal to the number of types.
391                self.types[i].indicator = if byte == 0 {
392                    TzifIndicator::LocalWall
393                } else if byte == 1 {
394                    TzifIndicator::LocalStandard
395                } else {
396                    return Err(IndicatorError::InvalidStdWallIndicator);
397                };
398            }
399        } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
400            assert_eq!(std_wall_bytes.len(), ut_local_bytes.len());
401            let it = std_wall_bytes.iter().zip(ut_local_bytes);
402            for (i, (&stdwall, &utlocal)) in it.enumerate() {
403                // Indexing is OK because Header guarantees that the number of
404                // indicators is 0 or equal to the number of types.
405                self.types[i].indicator = match (stdwall, utlocal) {
406                    (0, 0) => TzifIndicator::LocalWall,
407                    (1, 0) => TzifIndicator::LocalStandard,
408                    (1, 1) => TzifIndicator::UTStandard,
409                    (0, 1) => {
410                        return Err(IndicatorError::InvalidUtWallCombination);
411                    }
412                    _ => return Err(IndicatorError::InvalidCombination),
413                };
414            }
415        } else {
416            // If they're both empty then we don't need to do anything. Every
417            // local time type record already has the correct default for this
418            // case set.
419            debug_assert!(std_wall_bytes.is_empty());
420            debug_assert!(ut_local_bytes.is_empty());
421        }
422        Ok(rest)
423    }
424
425    fn parse_footer<'b>(
426        &mut self,
427        _header: &Header,
428        bytes: &'b [u8],
429    ) -> Result<&'b [u8], FooterError> {
430        if bytes.is_empty() {
431            return Err(FooterError::UnexpectedEnd);
432        }
433        if bytes[0] != b'\n' {
434            return Err(FooterError::MismatchEnd);
435        }
436        let bytes = &bytes[1..];
437        // Only scan up to 1KB for a NUL terminator in case we somehow got
438        // passed a huge block of bytes.
439        let toscan = &bytes[..bytes.len().min(1024)];
440        let nlat = toscan
441            .iter()
442            .position(|&b| b == b'\n')
443            .ok_or(FooterError::TerminatorNotFound)?;
444        let (bytes, rest) = bytes.split_at(nlat);
445        if !bytes.is_empty() {
446            // We could in theory limit TZ strings to their strict POSIX
447            // definition here for TZif V2, but I don't think there is any
448            // harm in allowing the extensions in V2 formatted TZif data. Note
449            // that GNU tooling allows it via the `TZ` environment variable
450            // even though POSIX doesn't specify it. This all seems okay to me
451            // because the V3+ extension is a strict superset of functionality.
452            let posix_tz = PosixTimeZone::parse(bytes)
453                .map_err(FooterError::InvalidPosixTz)?;
454            self.fixed.posix_tz = Some(posix_tz);
455        }
456        Ok(&rest[1..])
457    }
458
459    /// Validates that the POSIX TZ string we parsed (if one exists) is
460    /// consistent with the last transition in this time zone. This is
461    /// required by RFC 8536.
462    ///
463    /// RFC 8536 says, "If the string is nonempty and one or more
464    /// transitions appear in the version 2+ data, the string MUST be
465    /// consistent with the last version 2+ transition."
466    fn verify_posix_time_zone_consistency(
467        &self,
468    ) -> Result<(), InconsistentPosixTimeZoneError> {
469        // We need to be a little careful, since we always have at least one
470        // transition (accounting for the dummy `Timestamp::MIN` transition).
471        // So if we only have 1 transition and a POSIX TZ string, then we
472        // should not validate it since it's equivalent to the case of 0
473        // transitions and a POSIX TZ string.
474        if self.transitions.timestamps.len() <= 1 {
475            return Ok(());
476        }
477        let Some(ref tz) = self.fixed.posix_tz else {
478            return Ok(());
479        };
480        let last = self
481            .transitions
482            .timestamps
483            .last()
484            .expect("last transition timestamp");
485        let type_index = self
486            .transitions
487            .infos
488            .last()
489            .expect("last transition info")
490            .type_index;
491        let typ = &self.types[usize::from(type_index)];
492        let (ioff, abbrev, is_dst) =
493            tz.to_offset_info(ITimestamp::from_second(*last));
494        if ioff.second != typ.offset {
495            return Err(InconsistentPosixTimeZoneError::Offset);
496        }
497        if is_dst != typ.is_dst {
498            return Err(InconsistentPosixTimeZoneError::Dst);
499        }
500        if abbrev != self.designation(&typ) {
501            return Err(InconsistentPosixTimeZoneError::Designation);
502        }
503        Ok(())
504    }
505
506    /// Add civil datetimes to our transitions.
507    ///
508    /// This isn't strictly necessary, but it speeds up time zone lookups when
509    /// the input is a civil datetime. It lets us do comparisons directly on
510    /// the civil datetime as given, instead of needing to convert the civil
511    /// datetime given to a timestamp first. (Even if we didn't do this, I
512    /// believe we'd still need at least one additional timestamp that is
513    /// offset, because TZ lookups for a civil datetime are done in local time,
514    /// and the timestamps in TZif data are, of course, all in UTC.)
515    fn add_civil_datetimes_to_transitions(&mut self) {
516        fn to_datetime(timestamp: i64, offset: i32) -> TzifDateTime {
517            let its = ITimestamp { second: timestamp, nanosecond: 0 };
518            let ioff = IOffset { second: offset };
519            let dt = its.to_datetime(ioff);
520            TzifDateTime::new(
521                dt.date.year,
522                dt.date.month,
523                dt.date.day,
524                dt.time.hour,
525                dt.time.minute,
526                dt.time.second,
527            )
528        }
529
530        let trans = &mut self.transitions;
531        // Special case the first transition, which is always a dummy
532        // transition establishing the lower bound. The loop below doesn't
533        // handle this correctly because it isn't guaranteed to get a
534        // `DateTime::MIN` value.
535        trans.infos[0].kind = TzifTransitionKind::Unambiguous;
536        trans.civil_starts[0] = TzifDateTime::MIN;
537        for i in 1..trans.timestamps.len() {
538            let timestamp = trans.timestamps[i];
539            let offset = {
540                let type_index = trans.infos[i].type_index;
541                self.types[usize::from(type_index)].offset
542            };
543            let prev_offset = {
544                let type_index = trans.infos[i.saturating_sub(1)].type_index;
545                self.types[usize::from(type_index)].offset
546            };
547
548            if prev_offset == offset {
549                // Equivalent offsets means there can never be any ambiguity.
550                let start = to_datetime(timestamp, prev_offset);
551                trans.infos[i].kind = TzifTransitionKind::Unambiguous;
552                trans.civil_starts[i] = start;
553            } else if prev_offset < offset {
554                // When the offset of the previous transition is less, that
555                // means there is some non-zero amount of time that is
556                // "skipped" when moving to the next transition. Thus, we have
557                // a gap. The start of the gap is the offset which gets us the
558                // earliest time, i.e., the smaller of the two offsets.
559                trans.infos[i].kind = TzifTransitionKind::Gap;
560                trans.civil_starts[i] = to_datetime(timestamp, prev_offset);
561                trans.civil_ends[i] = to_datetime(timestamp, offset);
562            } else {
563                // When the offset of the previous transition is greater, that
564                // means there is some non-zero amount of time that will be
565                // replayed on a wall clock in this time zone. Thus, we have
566                // a fold. The start of the gold is the offset which gets us
567                // the earliest time, i.e., the smaller of the two offsets.
568                assert!(prev_offset > offset);
569                trans.infos[i].kind = TzifTransitionKind::Fold;
570                trans.civil_starts[i] = to_datetime(timestamp, offset);
571                trans.civil_ends[i] = to_datetime(timestamp, prev_offset);
572            }
573        }
574    }
575
576    /// Fatten up this TZif data with additional transitions.
577    ///
578    /// These additional transitions often make time zone lookups faster, and
579    /// they smooth out the performance difference between using "slim" and
580    /// "fat" tzdbs.
581    fn fatten(&mut self) {
582        // Note that this is a crate feature for *both* `jiff` and
583        // `jiff-static`.
584        if !cfg!(feature = "tz-fat") {
585            return;
586        }
587        let Some(posix_tz) = self.fixed.posix_tz.clone() else { return };
588        let last =
589            self.transitions.timestamps.last().expect("last transition");
590        let mut i = 0;
591        let mut prev = ITimestamp::from_second(*last);
592        loop {
593            if i > FATTEN_MAX_TRANSITIONS {
594                // only-jiff-start
595                warn!(
596                    "fattening TZif data for `{name:?}` somehow generated \
597                     more than {max} transitions, so giving up to avoid \
598                     doing too much work",
599                    name = self.fixed.name,
600                    max = FATTEN_MAX_TRANSITIONS,
601                );
602                // only-jiff-end
603                return;
604            }
605            i += 1;
606            prev = match self.add_transition(&posix_tz, prev) {
607                None => break,
608                Some(next) => next,
609            };
610        }
611    }
612
613    /// If there's a transition strictly after the given timestamp for the
614    /// given POSIX time zone, then add it to this TZif data.
615    fn add_transition(
616        &mut self,
617        posix_tz: &PosixTimeZone<Abbreviation>,
618        prev: ITimestamp,
619    ) -> Option<ITimestamp> {
620        let (its, ioff, abbrev, is_dst) = posix_tz.next_transition(prev)?;
621        if its.to_datetime(IOffset::UTC).date.year >= FATTEN_UP_TO_YEAR {
622            return None;
623        }
624        let type_index =
625            self.find_or_create_local_time_type(ioff, abbrev, is_dst)?;
626        self.transitions.add_with_type_index(its.second, type_index);
627        Some(its)
628    }
629
630    /// Look for a local time type matching the data given.
631    ///
632    /// If one could not be found, then one is created and its index is
633    /// returned.
634    ///
635    /// If one could not be found and one could not be created (e.g., the index
636    /// would overflow `u8`), then `None` is returned.
637    fn find_or_create_local_time_type(
638        &mut self,
639        offset: IOffset,
640        abbrev: &str,
641        is_dst: bool,
642    ) -> Option<u8> {
643        for (i, typ) in self.types.iter().enumerate() {
644            if offset.second == typ.offset
645                && abbrev == self.designation(typ)
646                && is_dst == typ.is_dst
647            {
648                return u8::try_from(i).ok();
649            }
650        }
651        let i = u8::try_from(self.types.len()).ok()?;
652        let designation = self.find_or_create_designation(abbrev)?;
653        self.types.push(TzifLocalTimeType {
654            offset: offset.second,
655            is_dst,
656            designation,
657            // Not really clear if this is correct, but Jiff
658            // ignores this anyway, so ¯\_(ツ)_/¯.
659            indicator: TzifIndicator::LocalWall,
660        });
661        Some(i)
662    }
663
664    /// Look for a designation (i.e., time zone abbreviation) matching the data
665    /// given, and return its range into `self.fixed.designations`.
666    ///
667    /// If one could not be found, then one is created and its range is
668    /// returned.
669    ///
670    /// If one could not be found and one could not be created (e.g., the range
671    /// would overflow `u8`), then `None` is returned.
672    fn find_or_create_designation(
673        &mut self,
674        needle: &str,
675    ) -> Option<(u8, u8)> {
676        let mut start = 0;
677        while let Some(offset) = self.fixed.designations[start..].find('\0') {
678            let end = start + offset;
679            let abbrev = &self.fixed.designations[start..end];
680            if needle == abbrev {
681                return Some((start.try_into().ok()?, end.try_into().ok()?));
682            }
683            start = end + 1;
684        }
685
686        // Now we need to add a new abbreviation. This
687        // should generally only happen for malformed TZif
688        // data. i.e., TZif data with a POSIX time zone that
689        // contains an TZ abbreviation that isn't found in
690        // the TZif's designation list.
691        //
692        // And since we're guarding against malformed data,
693        // the designation list might not end with NUL. If
694        // not, add one.
695        if !self.fixed.designations.ends_with('\0') {
696            self.fixed.designations.push('\0');
697        }
698        let start = self.fixed.designations.len();
699        self.fixed.designations.push_str(needle);
700        self.fixed.designations.push('\0');
701        let end = self.fixed.designations.len();
702        Some((start.try_into().ok()?, end.try_into().ok()?))
703    }
704
705    fn designation(&self, typ: &TzifLocalTimeType) -> &str {
706        let range =
707            usize::from(typ.designation.0)..usize::from(typ.designation.1);
708        // OK because we verify that the designation range on every local
709        // time type is a valid range into `self.designations`.
710        &self.fixed.designations[range]
711    }
712}
713
714impl TzifTransitionsOwned {
715    /// Add a single transition with the given timestamp.
716    ///
717    /// This also fills in the other columns (civil starts, civil ends and
718    /// infos) with sensible default values. It is expected that callers will
719    /// later fill them in.
720    fn add(&mut self, timestamp: i64) {
721        self.add_with_type_index(timestamp, 0);
722    }
723
724    /// Like `TzifTransitionsOwned::add`, but let's the caller provide a type
725    /// index if it is known.
726    fn add_with_type_index(&mut self, timestamp: i64, type_index: u8) {
727        self.timestamps.push(timestamp);
728        self.civil_starts.push(TzifDateTime::ZERO);
729        self.civil_ends.push(TzifDateTime::ZERO);
730        self.infos.push(TzifTransitionInfo {
731            type_index,
732            kind: TzifTransitionKind::Unambiguous,
733        });
734    }
735}
736
737/// The header for a TZif formatted file.
738///
739/// V2+ TZif format have two headers: one for V1 data, and then a second
740/// following the V1 data block that describes another data block which uses
741/// 64-bit timestamps. The two headers both have the same format and both
742/// use 32-bit big-endian encoded integers.
743#[derive(Debug)]
744struct Header {
745    /// The size of the timestamps encoded in the data block.
746    ///
747    /// This is guaranteed to be either 4 (for V1) or 8 (for the 64-bit header
748    /// block in V2+).
749    time_size: usize,
750    /// The file format version.
751    ///
752    /// Note that this is either a NUL byte (for version 1), or an ASCII byte
753    /// corresponding to the version number. That is, `0x32` for `2`, `0x33`
754    /// for `3` or `0x34` for `4`. Note also that just because zoneinfo might
755    /// have been recently generated does not mean it uses the latest format
756    /// version. It seems like newer versions are only compiled by `zic` when
757    /// they are needed. For example, `America/New_York` on my system (as of
758    /// `2024-03-25`) has version `0x32`, but `Asia/Jerusalem` has version
759    /// `0x33`.
760    version: u8,
761    /// Number of UT/local indicators stored in the file.
762    ///
763    /// This is checked to be either equal to `0` or equal to `tzh_typecnt`.
764    tzh_ttisutcnt: usize,
765    /// The number of standard/wall indicators stored in the file.
766    ///
767    /// This is checked to be either equal to `0` or equal to `tzh_typecnt`.
768    tzh_ttisstdcnt: usize,
769    /// The number of leap seconds for which data entries are stored in the
770    /// file.
771    tzh_leapcnt: usize,
772    /// The number of transition times for which data entries are stored in
773    /// the file.
774    tzh_timecnt: usize,
775    /// The number of local time types for which data entries are stored in the
776    /// file.
777    ///
778    /// This is checked to be at least `1`.
779    tzh_typecnt: usize,
780    /// The number of bytes of time zone abbreviation strings stored in the
781    /// file.
782    ///
783    /// This is checked to be at least `1`.
784    tzh_charcnt: usize,
785}
786
787impl Header {
788    /// Parse the header record from the given bytes.
789    ///
790    /// Upon success, return the header and all bytes after the header.
791    ///
792    /// The given `time_size` must be 4 or 8, corresponding to either the
793    /// V1 header block or the V2+ header block, respectively.
794    fn parse(
795        time_size: usize,
796        bytes: &[u8],
797    ) -> Result<(Header, &[u8]), HeaderError> {
798        assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8");
799        if bytes.len() < 44 {
800            return Err(HeaderError::TooShort);
801        }
802        let (magic, rest) = bytes.split_at(4);
803        if magic != b"TZif" {
804            return Err(HeaderError::MismatchMagic);
805        }
806        let (version, rest) = rest.split_at(1);
807        let (_reserved, rest) = rest.split_at(15);
808
809        let (tzh_ttisutcnt_bytes, rest) = rest.split_at(4);
810        let (tzh_ttisstdcnt_bytes, rest) = rest.split_at(4);
811        let (tzh_leapcnt_bytes, rest) = rest.split_at(4);
812        let (tzh_timecnt_bytes, rest) = rest.split_at(4);
813        let (tzh_typecnt_bytes, rest) = rest.split_at(4);
814        let (tzh_charcnt_bytes, rest) = rest.split_at(4);
815
816        let tzh_ttisutcnt =
817            from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes).map_err(|e| {
818                HeaderError::ParseCount { kind: CountKind::Ut, convert: e }
819            })?;
820        let tzh_ttisstdcnt =
821            from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes).map_err(|e| {
822                HeaderError::ParseCount { kind: CountKind::Std, convert: e }
823            })?;
824        let tzh_leapcnt =
825            from_be_bytes_u32_to_usize(tzh_leapcnt_bytes).map_err(|e| {
826                HeaderError::ParseCount { kind: CountKind::Leap, convert: e }
827            })?;
828        let tzh_timecnt =
829            from_be_bytes_u32_to_usize(tzh_timecnt_bytes).map_err(|e| {
830                HeaderError::ParseCount { kind: CountKind::Time, convert: e }
831            })?;
832        let tzh_typecnt =
833            from_be_bytes_u32_to_usize(tzh_typecnt_bytes).map_err(|e| {
834                HeaderError::ParseCount { kind: CountKind::Type, convert: e }
835            })?;
836        let tzh_charcnt =
837            from_be_bytes_u32_to_usize(tzh_charcnt_bytes).map_err(|e| {
838                HeaderError::ParseCount { kind: CountKind::Char, convert: e }
839            })?;
840
841        if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt {
842            return Err(HeaderError::MismatchUtType);
843        }
844        if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt {
845            return Err(HeaderError::MismatchStdType);
846        }
847        if tzh_typecnt < 1 {
848            return Err(HeaderError::ZeroType);
849        }
850        if tzh_charcnt < 1 {
851            return Err(HeaderError::ZeroChar);
852        }
853
854        let header = Header {
855            time_size,
856            version: version[0],
857            tzh_ttisutcnt,
858            tzh_ttisstdcnt,
859            tzh_leapcnt,
860            tzh_timecnt,
861            tzh_typecnt,
862            tzh_charcnt,
863        };
864        Ok((header, rest))
865    }
866
867    /// Returns true if this header is for a 32-bit data block.
868    ///
869    /// When false, it is guaranteed that this header is for a 64-bit data
870    /// block.
871    fn is_32bit(&self) -> bool {
872        self.time_size == 4
873    }
874
875    /// Returns the size of the data block, in bytes, for this header.
876    ///
877    /// This returns an error if the arithmetic required to compute the
878    /// length would overflow.
879    ///
880    /// This is useful for, e.g., skipping over the 32-bit V1 data block in
881    /// V2+ TZif formatted files.
882    fn data_block_len(&self) -> Result<usize, HeaderError> {
883        let a = self.transition_times_len()?;
884        let b = self.transition_types_len();
885        let c = self.local_time_types_len()?;
886        let d = self.time_zone_designations_len();
887        let e = self.leap_second_len()?;
888        let f = self.standard_wall_len();
889        let g = self.ut_local_len();
890        a.checked_add(b)
891            .and_then(|z| z.checked_add(c))
892            .and_then(|z| z.checked_add(d))
893            .and_then(|z| z.checked_add(e))
894            .and_then(|z| z.checked_add(f))
895            .and_then(|z| z.checked_add(g))
896            .ok_or(HeaderError::InvalidDataBlock { version: self.version })
897    }
898
899    fn transition_times_len(&self) -> Result<usize, HeaderError> {
900        self.tzh_timecnt
901            .checked_mul(self.time_size)
902            .ok_or(HeaderError::InvalidTimeCount)
903    }
904
905    fn transition_types_len(&self) -> usize {
906        self.tzh_timecnt
907    }
908
909    fn local_time_types_len(&self) -> Result<usize, HeaderError> {
910        self.tzh_typecnt.checked_mul(6).ok_or(HeaderError::InvalidTypeCount)
911    }
912
913    fn time_zone_designations_len(&self) -> usize {
914        self.tzh_charcnt
915    }
916
917    fn leap_second_len(&self) -> Result<usize, HeaderError> {
918        let record_len = self
919            .time_size
920            .checked_add(4)
921            .expect("4-or-8 plus 4 always fits in usize");
922        self.tzh_leapcnt
923            .checked_mul(record_len)
924            .ok_or(HeaderError::InvalidLeapSecondCount)
925    }
926
927    fn standard_wall_len(&self) -> usize {
928        self.tzh_ttisstdcnt
929    }
930
931    fn ut_local_len(&self) -> usize {
932        self.tzh_ttisutcnt
933    }
934}
935
936#[derive(Clone, Debug, Eq, PartialEq)]
937pub(crate) struct TzifError {
938    kind: TzifErrorKind,
939}
940
941#[derive(Clone, Debug, Eq, PartialEq)]
942enum TzifErrorKind {
943    Footer(FooterError),
944    Header(HeaderError),
945    Header32(HeaderError),
946    Header64(HeaderError),
947    InconsistentPosixTimeZone(InconsistentPosixTimeZoneError),
948    Indicator(IndicatorError),
949    LocalTimeType(LocalTimeTypeError),
950    SplitAt(SplitAtError),
951    TimeZoneDesignator(TimeZoneDesignatorError),
952    TransitionType(TransitionTypeError),
953}
954
955#[cfg(feature = "std")]
956impl std::error::Error for TzifError {}
957
958impl core::fmt::Display for TzifError {
959    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
960        use self::TzifErrorKind::*;
961        match self.kind {
962            Footer(ref err) => {
963                f.write_str("invalid TZif footer: ")?;
964                err.fmt(f)
965            }
966            Header(ref err) => {
967                f.write_str("invalid TZif header: ")?;
968                err.fmt(f)
969            }
970            Header32(ref err) => {
971                f.write_str("invalid 32-bit TZif header: ")?;
972                err.fmt(f)
973            }
974            Header64(ref err) => {
975                f.write_str("invalid 64-bit TZif header: ")?;
976                err.fmt(f)
977            }
978            InconsistentPosixTimeZone(ref err) => {
979                f.write_str(
980                    "found inconsistency with \
981                     POSIX time zone transition rule \
982                     in TZif file footer: ",
983                )?;
984                err.fmt(f)
985            }
986            Indicator(ref err) => {
987                f.write_str("failed to parse indicators: ")?;
988                err.fmt(f)
989            }
990            LocalTimeType(ref err) => {
991                f.write_str("failed to parse local time types: ")?;
992                err.fmt(f)
993            }
994            SplitAt(ref err) => err.fmt(f),
995            TimeZoneDesignator(ref err) => {
996                f.write_str("failed to parse time zone designators: ")?;
997                err.fmt(f)
998            }
999            TransitionType(ref err) => {
1000                f.write_str("failed to parse time zone transition types: ")?;
1001                err.fmt(f)
1002            }
1003        }
1004    }
1005}
1006
1007impl From<TzifErrorKind> for TzifError {
1008    fn from(kind: TzifErrorKind) -> TzifError {
1009        TzifError { kind }
1010    }
1011}
1012
1013#[derive(Clone, Debug, Eq, PartialEq)]
1014enum TransitionTypeError {
1015    ExceedsLocalTimeTypes,
1016    Split(SplitAtError),
1017}
1018
1019impl From<TransitionTypeError> for TzifError {
1020    fn from(err: TransitionTypeError) -> TzifError {
1021        TzifErrorKind::TransitionType(err).into()
1022    }
1023}
1024
1025impl From<SplitAtError> for TransitionTypeError {
1026    fn from(err: SplitAtError) -> TransitionTypeError {
1027        TransitionTypeError::Split(err)
1028    }
1029}
1030
1031impl core::fmt::Display for TransitionTypeError {
1032    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1033        use self::TransitionTypeError::*;
1034
1035        match *self {
1036            ExceedsLocalTimeTypes => f.write_str(
1037                "found time zone transition type index \
1038                 that exceeds the number of local time types",
1039            ),
1040            Split(ref err) => err.fmt(f),
1041        }
1042    }
1043}
1044
1045#[derive(Clone, Debug, Eq, PartialEq)]
1046enum LocalTimeTypeError {
1047    InvalidOffset { offset: i32 },
1048    Split(SplitAtError),
1049}
1050
1051impl From<LocalTimeTypeError> for TzifError {
1052    fn from(err: LocalTimeTypeError) -> TzifError {
1053        TzifErrorKind::LocalTimeType(err).into()
1054    }
1055}
1056
1057impl From<SplitAtError> for LocalTimeTypeError {
1058    fn from(err: SplitAtError) -> LocalTimeTypeError {
1059        LocalTimeTypeError::Split(err)
1060    }
1061}
1062
1063impl core::fmt::Display for LocalTimeTypeError {
1064    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1065        use self::LocalTimeTypeError::*;
1066
1067        match *self {
1068            InvalidOffset { offset } => write!(
1069                f,
1070                "found local time type with \
1071                 out-of-bounds time zone offset: {offset}, \
1072                 Jiff's allowed range is `{OFFSET_MIN}..={OFFSET_MAX}`"
1073            ),
1074            Split(ref err) => err.fmt(f),
1075        }
1076    }
1077}
1078
1079#[derive(Clone, Debug, Eq, PartialEq)]
1080enum TimeZoneDesignatorError {
1081    InvalidEnd,
1082    InvalidLength,
1083    InvalidStart,
1084    InvalidUtf8,
1085    MissingNul,
1086    Split(SplitAtError),
1087}
1088
1089impl From<TimeZoneDesignatorError> for TzifError {
1090    fn from(err: TimeZoneDesignatorError) -> TzifError {
1091        TzifErrorKind::TimeZoneDesignator(err).into()
1092    }
1093}
1094
1095impl From<SplitAtError> for TimeZoneDesignatorError {
1096    fn from(err: SplitAtError) -> TimeZoneDesignatorError {
1097        TimeZoneDesignatorError::Split(err)
1098    }
1099}
1100
1101impl core::fmt::Display for TimeZoneDesignatorError {
1102    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1103        use self::TimeZoneDesignatorError::*;
1104
1105        match *self {
1106            InvalidEnd => f.write_str(
1107                "found invalid end of time zone designator \
1108                 for local time type",
1109            ),
1110            InvalidLength => f.write_str(
1111                "found invalid length of time zone designator \
1112                 for local time type",
1113            ),
1114            InvalidStart => f.write_str(
1115                "found invalid start of time zone designator \
1116                 for local time type",
1117            ),
1118            InvalidUtf8 => {
1119                f.write_str("found invalid UTF-8 in time zone designators")
1120            }
1121            MissingNul => f.write_str(
1122                "could not find NUL terminator for time zone designator",
1123            ),
1124            Split(ref err) => err.fmt(f),
1125        }
1126    }
1127}
1128
1129#[derive(Clone, Debug, Eq, PartialEq)]
1130enum IndicatorError {
1131    InvalidCombination,
1132    InvalidStdWallIndicator,
1133    InvalidUtWallCombination,
1134    Split(SplitAtError),
1135    UtLocalNonZero,
1136}
1137
1138impl From<IndicatorError> for TzifError {
1139    fn from(err: IndicatorError) -> TzifError {
1140        TzifErrorKind::Indicator(err).into()
1141    }
1142}
1143
1144impl From<SplitAtError> for IndicatorError {
1145    fn from(err: SplitAtError) -> IndicatorError {
1146        IndicatorError::Split(err)
1147    }
1148}
1149
1150impl core::fmt::Display for IndicatorError {
1151    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1152        use self::IndicatorError::*;
1153
1154        match *self {
1155            InvalidCombination => f.write_str(
1156                "found invalid std/wall or UT/local value for \
1157                 local time type, each must be 0 or 1",
1158            ),
1159            InvalidStdWallIndicator => f.write_str(
1160                "found invalid std/wall indicator, \
1161                 expected it to be 0 or 1",
1162            ),
1163            InvalidUtWallCombination => f.write_str(
1164                "found invalid UT-wall combination for \
1165                 local time type, only local-wall, \
1166                 local-standard and UT-standard are allowed",
1167            ),
1168            Split(ref err) => err.fmt(f),
1169            UtLocalNonZero => f.write_str(
1170                "found non-zero UT/local indicator, \
1171                 but all such indicators should be zero",
1172            ),
1173        }
1174    }
1175}
1176
1177#[derive(Clone, Debug, Eq, PartialEq)]
1178enum InconsistentPosixTimeZoneError {
1179    Designation,
1180    Dst,
1181    Offset,
1182}
1183
1184impl From<InconsistentPosixTimeZoneError> for TzifError {
1185    fn from(err: InconsistentPosixTimeZoneError) -> TzifError {
1186        TzifErrorKind::InconsistentPosixTimeZone(err).into()
1187    }
1188}
1189
1190impl core::fmt::Display for InconsistentPosixTimeZoneError {
1191    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1192        use self::InconsistentPosixTimeZoneError::*;
1193
1194        match *self {
1195            Designation => f.write_str(
1196                "expected last transition in TZif file to have \
1197                 a time zone abbreviation matching the abbreviation \
1198                 derived from the POSIX time zone transition rule",
1199            ),
1200            Dst => f.write_str(
1201                "expected last transition in TZif file to have \
1202                 a DST status matching the status derived from the \
1203                 POSIX time zone transition rule",
1204            ),
1205            Offset => f.write_str(
1206                "expected last transition in TZif file to have \
1207                 DST offset matching the offset derived from the \
1208                 POSIX time zone transition rule",
1209            ),
1210        }
1211    }
1212}
1213
1214#[derive(Clone, Debug, Eq, PartialEq)]
1215enum FooterError {
1216    InvalidPosixTz(crate::shared::posix::PosixTimeZoneError),
1217    MismatchEnd,
1218    TerminatorNotFound,
1219    UnexpectedEnd,
1220}
1221
1222impl From<FooterError> for TzifError {
1223    fn from(err: FooterError) -> TzifError {
1224        TzifErrorKind::Footer(err).into()
1225    }
1226}
1227
1228impl core::fmt::Display for FooterError {
1229    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1230        use self::FooterError::*;
1231
1232        match *self {
1233            InvalidPosixTz(ref err) => {
1234                f.write_str("invalid POSIX time zone transition rule")?;
1235                core::fmt::Display::fmt(err, f)
1236            }
1237            MismatchEnd => f.write_str(
1238                "expected to find `\\n` at the beginning of \
1239                 the TZif file footer, \
1240                 but found something else instead",
1241            ),
1242            TerminatorNotFound => f.write_str(
1243                "expected to find `\\n` terminating \
1244                 the TZif file footer, \
1245                 but no line terminator could be found",
1246            ),
1247            UnexpectedEnd => f.write_str(
1248                "expected to find `\\n` at the beginning of \
1249                 the TZif file footer, \
1250                 but found unexpected end of data",
1251            ),
1252        }
1253    }
1254}
1255
1256#[derive(Clone, Debug, Eq, PartialEq)]
1257enum HeaderError {
1258    InvalidDataBlock { version: u8 },
1259    InvalidLeapSecondCount,
1260    InvalidTimeCount,
1261    InvalidTypeCount,
1262    MismatchMagic,
1263    MismatchStdType,
1264    MismatchUtType,
1265    ParseCount { kind: CountKind, convert: U32UsizeError },
1266    TooShort,
1267    ZeroChar,
1268    ZeroType,
1269}
1270
1271impl From<HeaderError> for TzifError {
1272    fn from(err: HeaderError) -> TzifError {
1273        TzifErrorKind::Header(err).into()
1274    }
1275}
1276
1277impl core::fmt::Display for HeaderError {
1278    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1279        use self::HeaderError::*;
1280
1281        match *self {
1282            InvalidDataBlock { version } => write!(
1283                f,
1284                "length of data block in V{version} TZif file is too big",
1285            ),
1286            InvalidLeapSecondCount => {
1287                f.write_str("number of leap seconds is too big")
1288            }
1289            InvalidTimeCount => {
1290                f.write_str("number of transition times is too big")
1291            }
1292            InvalidTypeCount => {
1293                f.write_str("number of local time types is too big")
1294            }
1295            MismatchMagic => f.write_str("magic bytes mismatch"),
1296            MismatchStdType => f.write_str(
1297                "expected number of standard/wall indicators to be zero \
1298                 or equal to the number of local time types",
1299            ),
1300            MismatchUtType => f.write_str(
1301                "expected number of UT/local indicators to be zero \
1302                 or equal to the number of local time types",
1303            ),
1304            ParseCount { ref kind, ref convert } => {
1305                write!(f, "failed to parse `{kind}`: {convert}")
1306            }
1307            TooShort => f.write_str("too short"),
1308            ZeroChar => f.write_str(
1309                "expected number of time zone abbreviations fo be at least 1",
1310            ),
1311            ZeroType => f.write_str(
1312                "expected number of local time types fo be at least 1",
1313            ),
1314        }
1315    }
1316}
1317
1318#[derive(Clone, Debug, Eq, PartialEq)]
1319enum CountKind {
1320    Ut,
1321    Std,
1322    Leap,
1323    Time,
1324    Type,
1325    Char,
1326}
1327
1328impl core::fmt::Display for CountKind {
1329    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1330        use self::CountKind::*;
1331        match *self {
1332            Ut => f.write_str("tzh_ttisutcnt"),
1333            Std => f.write_str("tzh_ttisstdcnt"),
1334            Leap => f.write_str("tzh_leapcnt"),
1335            Time => f.write_str("tzh_timecnt"),
1336            Type => f.write_str("tzh_typecnt"),
1337            Char => f.write_str("tzh_charcnt"),
1338        }
1339    }
1340}
1341
1342#[derive(Clone, Debug, Eq, PartialEq)]
1343pub(crate) enum SplitAtError {
1344    V1,
1345    LeapSeconds,
1346    LocalTimeTypes,
1347    StandardWallIndicators,
1348    TimeZoneDesignations,
1349    TransitionTimes,
1350    TransitionTypes,
1351    UTLocalIndicators,
1352}
1353
1354impl From<SplitAtError> for TzifError {
1355    fn from(err: SplitAtError) -> TzifError {
1356        TzifErrorKind::SplitAt(err).into()
1357    }
1358}
1359
1360impl core::fmt::Display for SplitAtError {
1361    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1362        use self::SplitAtError::*;
1363
1364        f.write_str("expected bytes for '")?;
1365        f.write_str(match *self {
1366            V1 => "v1 TZif",
1367            LeapSeconds => "leap seconds",
1368            LocalTimeTypes => "local time types",
1369            StandardWallIndicators => "standard/wall indicators",
1370            TimeZoneDesignations => "time zone designations",
1371            TransitionTimes => "transition times",
1372            TransitionTypes => "transition types",
1373            UTLocalIndicators => "UT/local indicators",
1374        })?;
1375        f.write_str("data block', but did not find enough bytes")?;
1376        Ok(())
1377    }
1378}
1379
1380#[derive(Clone, Debug, Eq, PartialEq)]
1381struct U32UsizeError;
1382
1383impl core::fmt::Display for U32UsizeError {
1384    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1385        write!(
1386            f,
1387            "failed to parse integer because it is bigger than `{max}`",
1388            max = usize::MAX,
1389        )
1390    }
1391}
1392
1393/// Splits the given slice of bytes at the index given.
1394///
1395/// If the index is out of range (greater than `bytes.len()`) then an error is
1396/// returned. The error message will include the `what` string given, which is
1397/// meant to describe the thing being split.
1398fn try_split_at<'b>(
1399    what: SplitAtError,
1400    bytes: &'b [u8],
1401    at: usize,
1402) -> Result<(&'b [u8], &'b [u8]), SplitAtError> {
1403    if at > bytes.len() {
1404        Err(what)
1405    } else {
1406        Ok(bytes.split_at(at))
1407    }
1408}
1409
1410/// Interprets the given slice as an unsigned 32-bit big endian integer,
1411/// attempts to convert it to a `usize` and returns it.
1412///
1413/// # Panics
1414///
1415/// When `bytes.len() != 4`.
1416///
1417/// # Errors
1418///
1419/// This errors if the `u32` parsed from the given bytes cannot fit in a
1420/// `usize`.
1421fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result<usize, U32UsizeError> {
1422    let n = from_be_bytes_u32(bytes);
1423    usize::try_from(n).map_err(|_| U32UsizeError)
1424}
1425
1426/// Interprets the given slice as an unsigned 32-bit big endian integer and
1427/// returns it.
1428///
1429/// # Panics
1430///
1431/// When `bytes.len() != 4`.
1432fn from_be_bytes_u32(bytes: &[u8]) -> u32 {
1433    u32::from_be_bytes(bytes.try_into().unwrap())
1434}
1435
1436/// Interprets the given slice as a signed 32-bit big endian integer and
1437/// returns it.
1438///
1439/// # Panics
1440///
1441/// When `bytes.len() != 4`.
1442fn from_be_bytes_i32(bytes: &[u8]) -> i32 {
1443    i32::from_be_bytes(bytes.try_into().unwrap())
1444}
1445
1446/// Interprets the given slice as a signed 64-bit big endian integer and
1447/// returns it.
1448///
1449/// # Panics
1450///
1451/// When `bytes.len() != 8`.
1452fn from_be_bytes_i64(bytes: &[u8]) -> i64 {
1453    i64::from_be_bytes(bytes.try_into().unwrap())
1454}