Skip to main content

tzcompile/tzif/
mod.rs

1//! TZif output: the in-memory [`TzifData`] model and the writer that serialises it to the
2//! on-disk format described by [RFC 9636] / `tzfile(5)`.
3//!
4//! # File shape (RFC 9636 §3)
5//!
6//! A version-2+ TZif file is three parts back to back:
7//!
8//! 1. a **version-1 block** — a 44-byte header followed by a data block that uses 32-bit
9//!    transition times;
10//! 2. a **version-2+ block** — an identical-shape header (same version byte) followed by a
11//!    data block that uses 64-bit transition times;
12//! 3. a **footer** — `\n`, a POSIX `TZ` string describing behaviour after the last
13//!    transition, then `\n`.
14//!
15//! Modern `zic` (the "slim" default) emits the v1 block as a near-empty **stub**: zero
16//! transitions and a single placeholder local-time-type with offset 0 and an empty
17//! abbreviation. All real information lives in the v2+ block and the footer. We reproduce
18//! that exactly (it is what we must byte-match), and we document it loudly because it is
19//! surprising: a strictly v1-only reader gets UT for every zone. That is `zic`'s own
20//! behaviour in slim mode, not ours.
21//!
22//! [RFC 9636]: https://www.rfc-editor.org/rfc/rfc9636
23
24pub mod data_block;
25pub mod header;
26pub mod rfc9636;
27pub mod validate;
28pub mod writer;
29
30pub use header::Counts;
31pub use validate::{parse, ParsedTzif};
32pub use writer::write_bytes;
33
34/// One local-time-type (`ttinfo`): a UT offset, a DST flag, and an abbreviation.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct LocalTimeType {
37    /// Seconds east of UT (the `tt_utoff` field).
38    pub utoff: i32,
39    /// Whether this type is daylight saving time.
40    pub is_dst: bool,
41    /// The timezone abbreviation (e.g. `EST`). May be empty (the v1 stub uses `""`).
42    pub abbr: String,
43}
44
45/// One transition: the UT instant at which `type_index` begins to apply.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct Transition {
48    /// Seconds since the Unix epoch, UT.
49    pub at: i64,
50    /// Index into [`TzifData::types`].
51    pub type_index: u8,
52}
53
54/// The fully-resolved, ready-to-serialise contents of one zone's TZif file.
55///
56/// Invariants the writer relies on (and [`validate`] re-checks):
57/// * `types` is non-empty;
58/// * every `transitions[i].type_index` is a valid index into `types`;
59/// * `transitions` is strictly increasing in `at`.
60#[derive(Debug, Clone)]
61pub struct TzifData {
62    pub types: Vec<LocalTimeType>,
63    pub transitions: Vec<Transition>,
64    /// POSIX `TZ` string for the footer, **without** the surrounding newlines. Empty is
65    /// permitted (means "no proleptic rule"), though we always synthesise one in T1.
66    pub footer: String,
67    /// The TZif version byte to stamp (`b'2'`, `b'3'`, ...). Content-driven; see the
68    /// module docs and `docs/tzif-notes.md`. T1 always emits `b'2'`.
69    pub version: u8,
70    /// Leap-second correction records for the **authoritative (v2+) block** (T11.3). Each entry is
71    /// **already adjusted** (cumulative `corr`; `trans` includes prior corrections — `zic`'s
72    /// `adjleap`). **Empty for every ordinary zone** (leaps come only from an explicit leap-source via
73    /// [`crate::compile::apply_leaps`]), so ordinary output is byte-unchanged. Orthogonal to
74    /// `transitions`/`types`: a leap is **not** a local-time-type transition.
75    pub leaps: Vec<LeapRecord>,
76}
77
78/// One emitted TZif leap-second record (RFC 9636 §3.2 array 5): the (already-adjusted) occurrence
79/// instant and the cumulative correction at/after it.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct LeapRecord {
82    /// Occurrence, seconds since epoch, in the file's timescale (cumulative-adjusted by `adjleap`).
83    pub trans: i64,
84    /// Cumulative correction (e.g. `+1, +2, +3, …`).
85    pub corr: i32,
86}
87
88impl TzifData {
89    /// Convenience constructor for a single fixed-offset type with no transitions.
90    pub fn fixed(utoff: i32, abbr: impl Into<String>, footer: impl Into<String>) -> Self {
91        TzifData {
92            types: vec![LocalTimeType {
93                utoff,
94                is_dst: false,
95                abbr: abbr.into(),
96            }],
97            transitions: Vec::new(),
98            footer: footer.into(),
99            version: b'2',
100            leaps: Vec::new(),
101        }
102    }
103}