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.