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}