Skip to main content

jiff_core/tz/tzif/
mod.rs

1/*!
2Implements [TZif] time zone parsing and transition handling.
3
4The TZif format is used by the [Time Zone Database].
5
6The parser in this module is designed to handle untrusted input. That is, there
7is no input that should cause it to panic or allocate memory in a way that is
8not proportional to the size of the TZif data.
9
10These binary files are the ones commonly found in Unix distributions in the
11`/usr/share/zoneinfo` directory.
12
13[Time Zone Database]: https://www.iana.org/time-zones
14[TZif]: https://datatracker.ietf.org/doc/rfc9636/
15*/
16
17use crate::{
18    bounds::RangeError,
19    civil,
20    macros::unwrapr,
21    tz::{self, posix, Abbreviation, Dst, Offset},
22    util::MaybeStaticSlice,
23};
24
25#[cfg(feature = "alloc")]
26mod parser;
27mod query;
28
29#[cfg(feature = "alloc")]
30pub use self::parser::ParseError;
31
32/// A representation of a possibly named time zone backed by [TZif] data.
33///
34/// It is useful to represent a named time zone as distinct from the time zone
35/// itself because a name is not inherently part of the TZif data. Indeed, there
36/// are a couple reasons why it's important to separate the name:
37///
38/// * It's possible that no meaningful name exists. For example, a
39/// `/etc/localtime` file with no obvious mapping to a name. (e.g., No symlink
40/// and no discoverable Time Zone Database.)
41/// * Some IANA time zone identifiers correspond to different regions, but
42/// share the same TZif data.
43///
44/// [TZif]: https://datatracker.ietf.org/doc/rfc9636/
45#[derive(Clone, Debug, PartialEq)]
46pub struct MaybeNamedTimeZone {
47    /// The name of this TZif time zone. e.g., `America/New_York`.
48    pub name: Option<tz::TimeZoneId>,
49    /// The time zone itself.
50    pub tz: TimeZone,
51}
52
53impl MaybeNamedTimeZone {
54    /// Returns the underlying time zone name as a string.
55    #[inline]
56    pub fn name(&self) -> Option<&str> {
57        self.name.as_deref()
58    }
59
60    /// Returns a reference to the underlying time zone definition.
61    #[inline]
62    pub fn tz(&self) -> &TimeZone {
63        &self.tz
64    }
65}
66
67/// A representation of an unnamed time zone backed by [TZif] data.
68///
69/// Two time zones are considered equivalent when their CRC32 sums are
70/// equivalent.
71///
72/// [TZif]: https://datatracker.ietf.org/doc/rfc9636/
73#[derive(Clone, Debug)]
74// This ensures the alignment of this type is always *at least* 8 bytes. This
75// is required for the pointer tagging inside of `TimeZone` to be sound. At
76// time of writing (2024-02-24), this explicit `repr` isn't required on 64-bit
77// systems since the type definition is such that it will have an alignment of
78// at least 8 bytes anyway. But this *is* required for 32-bit systems, where
79// the type definition at present only has an alignment of 4 bytes.
80#[repr(align(8))]
81pub struct TimeZone {
82    /// An ASCII byte corresponding to the version number. So, 0x50 is '2'.
83    pub version: u8,
84    /// A CRC32 checksum of the underlying TZif data.
85    ///
86    /// This, along with the time zone's IANA identifier, is used to provide a
87    /// "best effort" but also cheap notion of strict equality between two time
88    /// zones.
89    pub checksum: u32,
90    /// The time zone abbreviations referenced by local time types.
91    pub designations: MaybeStaticSlice<Abbreviation>,
92    /// A POSIX time zone used for determining time zone transitions after the
93    /// last transition in some TZif data.
94    ///
95    /// This is technically optional, but is usually present.
96    pub posix_tz: Option<posix::TimeZone>,
97    /// The local time types in this TZif data.
98    ///
99    /// Each local time type represents a distinct combination of offset,
100    /// abbreviation and whether the region is in daylight saving time or not.
101    pub types: MaybeStaticSlice<LocalTimeType>,
102    /// The concrete transitions that make up this time zone.
103    pub transitions: Transitions,
104}
105
106impl TimeZone {
107    /// Converts this unnamed time zone into a time zone with the given name.
108    #[inline]
109    pub fn into_named(self, name: tz::TimeZoneId) -> MaybeNamedTimeZone {
110        self.into_maybe_named(Some(name))
111    }
112
113    /// Converts this unnamed time zone into a time zone with the given
114    /// optional name.
115    #[inline]
116    pub fn into_maybe_named(
117        self,
118        name: Option<tz::TimeZoneId>,
119    ) -> MaybeNamedTimeZone {
120        MaybeNamedTimeZone { name, tz: self }
121    }
122}
123
124impl PartialEq for TimeZone {
125    fn eq(&self, rhs: &TimeZone) -> bool {
126        self.checksum == rhs.checksum
127    }
128}
129
130/// A "local time type" from TZif data.
131///
132/// This is referenced by time zone transitions. It may be used by one or
133/// more time zone transitions. This contains information about whether
134/// the transition moves into DST, its time zone abbreviation, and most
135/// importantly, the offset.
136#[derive(Clone, Copy, Debug)]
137pub struct LocalTimeType {
138    /// The offset from UTC.
139    pub offset: Offset,
140    /// Whether the region is considered to be in daylight saving time or not.
141    pub dst: Dst,
142    /// An index into `TimeZone::designations` corresponding to the time zone
143    /// abbreviation for this local time type.
144    pub designation: u8,
145    /// It's unclear to the author of Jiff what this is or what it's supposed
146    /// to be used for.
147    pub indicator: Indicator,
148}
149
150impl LocalTimeType {
151    fn designation(&self) -> usize {
152        usize::from(self.designation)
153    }
154}
155
156/// The possible indicator values for standard/wall and UT/local.
157///
158/// Their purpose, as of 2026-07-06, is unknown to Jiff's author. But they are
159/// represented here for completeness.
160// Note that UT+Wall is not allowed.
161//
162// I honestly have no earthly clue what they mean. I've read the section about
163// them in RFC 8536 several times and I can't make sense of it. I've even
164// looked at data files that have these set and still can't make sense of
165// them. I've even looked at what other datetime libraries do with these, and
166// they all seem to just ignore them. Like, WTF. I've spent the last couple
167// months of my life steeped in time, and I just cannot figure this out. Am I
168// just dumb?
169//
170// Anyway, we parse them, but otherwise ignore them because that's what all
171// the cool kids do.
172//
173// The default is `LocalWall`, which also occurs when no indicators are
174// present.
175//
176// I tried again and still don't get it. Here's a dump for `Pacific/Honolulu`:
177//
178// ```text
179// $ ./scripts/jiff-debug tzif /usr/share/zoneinfo/Pacific/Honolulu
180// TIME ZONE NAME
181//   /usr/share/zoneinfo/Pacific/Honolulu
182// LOCAL TIME TYPES
183//   000: offset=-10:31:26, is_dst=false, designation=LMT, indicator=local/wall
184//   001: offset=-10:30, is_dst=false, designation=HST, indicator=local/wall
185//   002: offset=-09:30, is_dst=true, designation=HDT, indicator=local/wall
186//   003: offset=-09:30, is_dst=true, designation=HWT, indicator=local/wall
187//   004: offset=-09:30, is_dst=true, designation=HPT, indicator=ut/std
188//   005: offset=-10, is_dst=false, designation=HST, indicator=local/wall
189// TRANSITIONS
190//   0000: -9999-01-02T01:59:59 :: -377705023201 :: type=0, -10:31:26, is_dst=false, LMT, local/wall
191//   0001: 1896-01-13T22:31:26 :: -2334101314 :: type=1, -10:30, is_dst=false, HST, local/wall
192//   0002: 1933-04-30T12:30:00 :: -1157283000 :: type=2, -09:30, is_dst=true, HDT, local/wall
193//   0003: 1933-05-21T21:30:00 :: -1155436200 :: type=1, -10:30, is_dst=false, HST, local/wall
194//   0004: 1942-02-09T12:30:00 :: -880198200 :: type=3, -09:30, is_dst=true, HWT, local/wall
195//   0005: 1945-08-14T23:00:00 :: -769395600 :: type=4, -09:30, is_dst=true, HPT, ut/std
196//   0006: 1945-09-30T11:30:00 :: -765376200 :: type=1, -10:30, is_dst=false, HST, local/wall
197//   0007: 1947-06-08T12:30:00 :: -712150200 :: type=5, -10, is_dst=false, HST, local/wall
198// POSIX TIME ZONE STRING
199//   HST10
200// ```
201//
202// See how type 004 has a ut/std indicator? What the fuck does that mean?
203// All transitions are defined in terms of UTC. I confirmed this with `zdump`:
204//
205// ```text
206// $ zdump -v Pacific/Honolulu | rg 1945
207// Pacific/Honolulu  Tue Aug 14 22:59:59 1945 UT = Tue Aug 14 13:29:59 1945 HWT isdst=1 gmtoff=-34200
208// Pacific/Honolulu  Tue Aug 14 23:00:00 1945 UT = Tue Aug 14 13:30:00 1945 HPT isdst=1 gmtoff=-34200
209// Pacific/Honolulu  Sun Sep 30 11:29:59 1945 UT = Sun Sep 30 01:59:59 1945 HPT isdst=1 gmtoff=-34200
210// Pacific/Honolulu  Sun Sep 30 11:30:00 1945 UT = Sun Sep 30 01:00:00 1945 HST isdst=0 gmtoff=-37800
211// ```
212//
213// The times match up. All of them. The indicators don't seem to make a
214// difference. I'm clearly missing something.
215#[allow(missing_docs)]
216#[derive(Clone, Copy, Debug)]
217pub enum Indicator {
218    LocalWall,
219    LocalStandard,
220    UTStandard,
221}
222
223/// The set of transitions in TZif data, laid out in column orientation.
224///
225/// The column orientation is used to make TZ lookups faster. Specifically,
226/// for finding an offset for a timestamp, we do a binary search on
227/// `timestamps`. For finding an offset for a local datetime, we do a binary
228/// search on `civil_starts`. By making these two distinct sequences with
229/// nothing else in them, we make them as small as possible and thus improve
230/// cache locality.
231///
232/// All sequences in this type are in correspondence with one another. They
233/// are all guaranteed to have the same length.
234#[derive(Clone, Debug)]
235pub struct Transitions {
236    /// The timestamp at which this transition begins.
237    pub timestamps: MaybeStaticSlice<Timestamp>,
238    /// The wall clock time for when a transition begins.
239    pub civil_starts: MaybeStaticSlice<DateTime>,
240    /// The wall clock time for when a transition ends.
241    ///
242    /// This is equivalent to the corresponding entry in `civil_starts` when
243    /// the corresponding transition is neither a gap nor a fold. A transition
244    /// that isn't a gap or a fold keeps the offset the same but may change
245    /// something else, like the abbreviation or whether it's considered
246    /// daylight saving time.
247    pub civil_ends: MaybeStaticSlice<DateTime>,
248    /// Any other relevant data about a transition, such as its local type
249    /// index and the transition kind.
250    pub infos: MaybeStaticSlice<TransitionInfo>,
251}
252
253/// TZif transition info beyond the timestamp and civil datetime.
254///
255/// For example, this contains a transition's "local type index," which in
256/// turn gives access to the offset (among other metadata) for that transition.
257#[derive(Clone, Copy, Debug)]
258pub struct TransitionInfo {
259    /// The index into the sequence of local time type records. This is what
260    /// provides the correct offset (from UTC) that is active beginning at
261    /// this transition.
262    pub type_index: u8,
263    /// The boundary condition for quickly determining if a given wall clock
264    /// time is ambiguous (i.e., falls in a gap or a fold).
265    pub kind: TransitionKind,
266}
267
268/// The kind of a transition.
269///
270/// This is used when trying to determine the offset for a local datetime. It
271/// indicates how the corresponding civil datetimes in `civil_starts` and
272/// `civil_ends` should be interpreted. That is, there are three possible
273/// cases:
274///
275/// 1. The offset of this transition is equivalent to the offset of the
276/// previous transition. That means there are no ambiguous civil datetimes
277/// between the transitions. This can occur, e.g., when the time zone
278/// abbreviation changes.
279/// 2. The offset of the transition is greater than the offset of the previous
280/// transition. That means there is a "gap" in local time between the
281/// transitions. This typically corresponds to entering daylight saving time.
282/// It is usually, but not always, 1 hour.
283/// 3. The offset of the transition is less than the offset of the previous
284/// transition. That means there is a "fold" in local time where time is
285/// repeated. This typically corresponds to leaving daylight saving time. It
286/// is usually, but not always, 1 hour.
287///
288/// # More explanation
289///
290/// This, when combined with `civil_starts` and `civil_ends` in
291/// `Transitions`, explicitly represents ambiguous wall clock times that
292/// occur at the boundaries of transitions.
293///
294/// The start of the wall clock time is always the earlier possible wall clock
295/// time that could occur with this transition's corresponding offset. For a
296/// gap, it's the previous transition's offset. For a fold, it's the current
297/// transition's offset.
298///
299/// For example, DST for `America/New_York` began on `2024-03-10T07:00:00+00`.
300/// The offset prior to this instant in time is `-05`, corresponding
301/// to standard time (EST). Thus, in wall clock time, DST began at
302/// `2024-03-10T02:00:00`. And since this is a DST transition that jumps ahead
303/// an hour, the start of DST also corresponds to the start of a gap. That is,
304/// the times `02:00:00` through `02:59:59` never appear on a clock for this
305/// hour. The question is thus: which offset should we apply to `02:00:00`?
306/// We could apply the offset from the earlier transition `-05` and get
307/// `2024-03-10T01:00:00-05` (that's `2024-03-10T06:00:00+00`), or we could
308/// apply the offset from the later transition `-04` and get
309/// `2024-03-10T03:00:00-04` (that's `2024-03-10T07:00:00+00`).
310///
311/// So in the above, we would have a `Gap` variant where `start` (inclusive) is
312/// `2024-03-10T02:00:00` and `end` (exclusive) is `2024-03-10T03:00:00`.
313///
314/// The fold case is the same idea, but where the same time is repeated.
315/// For example, in `America/New_York`, standard time began on
316/// `2024-11-03T06:00:00+00`. The offset prior to this instant in time
317/// is `-04`, corresponding to DST (EDT). Thus, in wall clock time, DST
318/// ended at `2024-11-03T02:00:00`. However, since this is a fold, the
319/// actual set of ambiguous times begins at `2024-11-03T01:00:00` and
320/// ends at `2024-11-03T01:59:59.999999999`. That is, the wall clock time
321/// `2024-11-03T02:00:00` is unambiguous.
322///
323/// So in the fold case above, we would have a `Fold` variant where
324/// `start` (inclusive) is `2024-11-03T01:00:00` and `end` (exclusive) is
325/// `2024-11-03T02:00:00`.
326///
327/// Since this gets bundled in with the sorted sequence of transitions, we'll
328/// use the "start" time in all three cases as our target of binary search.
329/// Once we land on a transition, we'll know our given wall clock time is
330/// greater than or equal to its start wall clock time. At that point, to
331/// determine if there is ambiguity, we merely need to determine if the given
332/// wall clock time is less than the corresponding `end` time. If it is, then
333/// it falls in a gap or fold. Otherwise, it's unambiguous.
334///
335/// Note that we could compute these datetime values while searching for the
336/// correct transition, but there's a fair bit of math involved in going
337/// between timestamps (which is what TZif gives us) and calendar datetimes
338/// (which is what we're given as input). It is also necessary that we offset
339/// the timestamp given in TZif at some point, since it is in UTC and the
340/// datetime given is in wall clock time. So I decided it would be worth
341/// pre-computing what we need in terms of what the input is. This way, we
342/// don't need to do any conversions, or indeed, any arithmetic at all, for
343/// time zone lookups. We *could* store these as transitions, but then the
344/// input datetime would need to be converted to a timestamp before searching
345/// the transitions.
346#[derive(Clone, Copy, Debug)]
347pub enum TransitionKind {
348    /// This transition cannot possibly lead to an unambiguous offset because
349    /// its offset is equivalent to the offset of the previous transition.
350    ///
351    /// Has an entry in `civil_starts`, but corresponding entry in `civil_ends`
352    /// is always zeroes (i.e., meaningless).
353    Unambiguous,
354    /// This occurs when this transition's offset is strictly greater than the
355    /// previous transition's offset. This effectively results in a "gap" of
356    /// time equal to the difference in the offsets between the two
357    /// transitions.
358    ///
359    /// Has an entry in `civil_starts` for when the gap starts (inclusive) in
360    /// local time. Also has an entry in `civil_ends` for when the fold ends
361    /// (exclusive) in local time.
362    Gap,
363    /// This occurs when this transition's offset is strictly less than the
364    /// previous transition's offset. This results in a "fold" of time where
365    /// the two transitions have an overlap where it is ambiguous which one
366    /// applies given a wall clock time. In effect, a span of time equal to the
367    /// difference in the offsets is repeated.
368    ///
369    /// Has an entry in `civil_starts` for when the fold starts (inclusive) in
370    /// local time. Also has an entry in `civil_ends` for when the fold ends
371    /// (exclusive) in local time.
372    Fold,
373}
374
375/// The representation for a timestamp used by the TZif implementation.
376///
377/// We don't use [`Timestamp`](crate::Timestamp) from the root of this crate
378/// because TZif data doesn't require nanosecond resolution. Instead, this
379/// representation uses only second resolution. This makes for a more compact
380/// sequence of transitions, which means more data fits into cache and thus
381/// faster binary search.
382#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
383pub struct Timestamp {
384    second: i64,
385}
386
387impl Timestamp {
388    /// The minimum timestamp value.
389    pub const MIN: Timestamp = Timestamp::new(crate::Timestamp::MIN);
390
391    /// The maximum timestamp value.
392    pub const MAX: Timestamp = Timestamp::new(crate::Timestamp::MAX);
393
394    /// The zero value for a timestamp, which also corresponds to the Unix
395    /// epoch (`1970-01-01T00:00:00Z`).
396    pub const UNIX_EPOCH: Timestamp =
397        Timestamp::new(crate::Timestamp::UNIX_EPOCH);
398
399    /// Creates a new TZif timestamp from jiff-core's standard timestamp type.
400    ///
401    /// Note that this completely ignores any subsecond component of the
402    /// provided timestamp.
403    pub const fn new(ts: crate::Timestamp) -> Timestamp {
404        Timestamp { second: ts.as_second() }
405    }
406
407    /// Creates a new `Timestamp` from a Unix timestamp integer value.
408    ///
409    /// This returns an error if the value is not in the legal bounds for this
410    /// type.
411    pub const fn from_second(second: i64) -> Result<Timestamp, RangeError> {
412        match crate::Timestamp::from_second(second) {
413            Ok(ts) => Ok(Timestamp::new(ts)),
414            Err(err) => Err(err),
415        }
416    }
417
418    /// Returns the second value of this timestamp.
419    ///
420    /// It is the number of seconds since the Unix epoch
421    /// (`1970-01-01T00:00:00Z`). Timestamps prior to the Unix epoch are
422    /// negative. Timestamps are the Unix epoch are `0`.
423    pub const fn as_second(self) -> i64 {
424        self.second
425    }
426
427    /// Adds the number of seconds to this timestamp, saturating at the minimum
428    /// or maximum legal value.
429    const fn saturating_add(self, seconds: i64) -> Timestamp {
430        let second = self.as_second() + seconds;
431        if second > Timestamp::MAX.as_second() {
432            Timestamp::MAX
433        } else if second < Timestamp::MIN.as_second() {
434            Timestamp::MIN
435        } else {
436            Timestamp { second }
437        }
438    }
439
440    /// Converts this timestamp to a civil datetime for the offset given.
441    pub const fn to_datetime(self, offset: Offset) -> DateTime {
442        DateTime::new(self.to_standard_timestamp().to_datetime(offset))
443    }
444
445    /// Converts this timestamp back to the "standard" timestamp.
446    ///
447    /// Note that the timestamp returned here always has its nanosecond
448    /// component set to `0`.
449    pub const fn to_standard_timestamp(self) -> crate::Timestamp {
450        // OK because we don't provide a way to construct, mutate or
451        // change a `Timestamp` that drifts from the valid values of a
452        // `crate::Timestamp` (for its second component).
453        unwrapr!(
454            crate::Timestamp::from_second(self.as_second()),
455            "always in bounds"
456        )
457    }
458}
459
460impl core::fmt::Debug for Timestamp {
461    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
462        core::fmt::Debug::fmt(&self.to_standard_timestamp(), f)
463    }
464}
465
466/// The representation for a civil datetime used by the TZif implementation.
467///
468/// We don't use [`civil::DateTime`] here because we specifically
469/// do not need to represent fractional seconds. This lets us easily represent
470/// what we need in 8 bytes.
471///
472/// Moreover, we pack the fields into a single `i64` to make comparisons
473/// extremely cheap. This is especially useful since we do a binary search on
474/// civil datetimes when determining the instant that corresponds to a civil
475/// datetime.
476#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
477pub struct DateTime {
478    bits: i64,
479}
480
481impl DateTime {
482    /// The minimum civil datetime value.
483    pub const MIN: DateTime = DateTime::new(civil::DateTime::from_parts(
484        civil::Date::MIN,
485        civil::Time::MIN,
486    ));
487
488    /// The maximum civil datetime value.
489    pub const MAX: DateTime = DateTime::new(civil::DateTime::from_parts(
490        civil::Date::MAX,
491        civil::Time::MAX,
492    ));
493
494    /// Creates a new TZif civil datetime from jiff-core's standard civil
495    /// datetime type.
496    ///
497    /// Note that this completely ignores any fractional second component on
498    /// the provided datetime.
499    pub const fn new(dt: civil::DateTime) -> DateTime {
500        let (d, t) = (dt.date(), dt.time());
501        let mut bits = 0;
502        bits |= (d.year() as u64) << 48;
503        bits |= (d.month() as u64) << 40;
504        bits |= (d.day() as u64) << 32;
505        bits |= (t.hour() as u64) << 24;
506        bits |= (t.minute() as u64) << 16;
507        bits |= (t.second() as u64) << 8;
508        // The least significant 8 bits remain 0.
509        DateTime { bits: bits as i64 }
510    }
511
512    /// Returns the year component of this civil datetime.
513    pub const fn year(self) -> i16 {
514        (self.bits as u64 >> 48) as u16 as i16
515    }
516
517    /// Returns the month component of this civil datetime.
518    pub const fn month(self) -> i8 {
519        (self.bits as u64 >> 40) as u8 as i8
520    }
521
522    /// Returns the day component of this civil datetime.
523    pub const fn day(self) -> i8 {
524        (self.bits as u64 >> 32) as u8 as i8
525    }
526
527    /// Returns the hour component of this civil datetime.
528    pub const fn hour(self) -> i8 {
529        (self.bits as u64 >> 24) as u8 as i8
530    }
531
532    /// Returns the minute component of this civil datetime.
533    pub const fn minute(self) -> i8 {
534        (self.bits as u64 >> 16) as u8 as i8
535    }
536
537    /// Returns the second component of this civil datetime.
538    pub const fn second(self) -> i8 {
539        (self.bits as u64 >> 8) as u8 as i8
540    }
541}
542
543/// Creates a new bit packed datetime from jiff-core's standard datetime type.
544///
545/// Note that this completely ignores any fractional second component on the
546/// provided datetime.
547impl From<civil::DateTime> for DateTime {
548    fn from(dt: civil::DateTime) -> DateTime {
549        DateTime::new(dt)
550    }
551}
552
553impl core::fmt::Debug for DateTime {
554    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
555        if !f.alternate() {
556            f.debug_struct("DateTime").field("bits", &self.bits).finish()
557        } else {
558            f.debug_tuple("DateTime")
559                .field(&format_args!(
560                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
561                    self.year(),
562                    self.month(),
563                    self.day(),
564                    self.hour(),
565                    self.minute(),
566                    self.second(),
567                ))
568                .finish()
569        }
570    }
571}
572
573/// Returns true if the data might be in TZif format.
574///
575/// It is possible that this returns true even if the given data is not in TZif
576/// format. However, it is impossible for this to return false when the given
577/// data is TZif. That is, a false positive is allowed but a false negative is
578/// not.
579pub fn is_possibly_tzif(data: &[u8]) -> bool {
580    data.starts_with(b"TZif")
581}
582
583// If you're looking for tests, they can be found in Jiff's `tz::timezone`
584// and `tz::tzif` modules.