Skip to main content

tzf_rs/tzb/
mod.rs

1//! Reader for the TZF embedded binary format (`.tzb`).
2//!
3//! Implements the container and E profile, plus the FUZZY section,
4//! mirroring the Go reference implementation in
5//! `github.com/ringsaturn/tzf/v2/internal/embedbin`.
6
7mod expand;
8mod fuzzy;
9mod raycast;
10mod reader;
11mod tile;
12
13pub(crate) use expand::ExpandedPolygon;
14pub(crate) use raycast::raycast_seg;
15pub(crate) use reader::Reader;
16pub(crate) use tile::TileId;
17
18use geometry_rs::I32Point;
19use std::fmt;
20
21pub(crate) const HEADER_SIZE: u64 = 64;
22pub(crate) const SECTION_ENTRY_LEN: u64 = 16;
23pub(crate) const FOOTER_SIZE: u64 = 4;
24pub(crate) const FORMAT_MAJOR: u8 = 1;
25pub(crate) const COORD_SCALE: u32 = 100_000;
26
27/// Header byte assigned as `profile` in format revision 1.1.
28pub(crate) const PROFILE_OFFSET: usize = 48;
29pub(crate) const PROFILE_E: u8 = 0;
30pub(crate) const PROFILE_M: u8 = 1;
31
32pub(crate) const FLAG_GRID: u32 = 1 << 0;
33pub(crate) const FLAG_NO_SHORTCUT: u32 = 1 << 1;
34
35pub(crate) const SECTION_NAMES: u32 = 1;
36pub(crate) const SECTION_TZDIR: u32 = 2;
37pub(crate) const SECTION_POLYDIR: u32 = 3;
38pub(crate) const SECTION_RINGDIR: u32 = 4;
39pub(crate) const SECTION_RINGOPS: u32 = 5;
40pub(crate) const SECTION_GROUPDIR: u32 = 6;
41pub(crate) const SECTION_CHUNKDIR: u32 = 7;
42pub(crate) const SECTION_GRID: u32 = 8;
43pub(crate) const SECTION_POINTS: u32 = 9;
44pub(crate) const SECTION_FUZZY: u32 = 10;
45pub(crate) const SECTION_FLAT_POINTS: u32 = 12;
46pub(crate) const SECTION_FLAT_RING_DIR: u32 = 13;
47pub(crate) const SECTION_YSTRIPES: u32 = 14;
48
49/// Sizes the per-type section table (types 1..14).
50pub(crate) const SECTION_SLOTS: usize = 15;
51
52pub(crate) const TZ_RECORD_LEN: u64 = 24;
53pub(crate) const POLY_RECORD_LEN: u64 = 24;
54pub(crate) const RING_RECORD_LEN: u64 = 28;
55pub(crate) const GROUP_RECORD_LEN: u64 = 44;
56pub(crate) const CHUNK_RECORD_LEN: u64 = 24;
57
58pub(crate) const FUZZY_HEADER_LEN: u64 = 16;
59/// Marks a FUZZY value word as a multi_dir group reference; the low 15 bits
60/// are then a group index instead of a NAMES index.
61pub(crate) const FUZZY_MULTI: u16 = 1 << 15;
62
63/// Errors reported by the `.tzb` reader.
64#[derive(Debug, Clone, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum Error {
67    /// The file violates the format's structural rules.
68    Malformed(&'static str),
69    /// The file is a memory-image (`.tzm`, M profile), which tzf-rs does not
70    /// consume: it exists for the Go runtime's zero-copy ring aliasing,
71    /// which gains nothing here. Use the `.tzb` file.
72    Profile,
73    /// The file carries no FUZZY section.
74    NoFuzzy,
75    /// A timezone index is out of range.
76    Index,
77}
78
79impl fmt::Display for Error {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Error::Malformed(what) => write!(f, "tzb: malformed file: {what}"),
83            Error::Profile => {
84                write!(
85                    f,
86                    "tzb: memory-image (.tzm) files are not supported; use .tzb"
87                )
88            }
89            Error::NoFuzzy => write!(f, "tzb: file has no FUZZY section"),
90            Error::Index => write!(f, "tzb: timezone index out of range"),
91        }
92    }
93}
94
95impl std::error::Error for Error {}
96
97pub(crate) fn malformed<T>(what: &'static str) -> Result<T, Error> {
98    Err(Error::Malformed(what))
99}
100
101/// Scaled-int32 bounding box, compared in `f64` like the Go reader.
102#[derive(Debug, Clone, Copy)]
103pub(crate) struct BBox {
104    pub min_x: i32,
105    pub min_y: i32,
106    pub max_x: i32,
107    pub max_y: i32,
108}
109
110impl BBox {
111    pub(crate) fn read(raw: &[u8], off: usize) -> Self {
112        Self {
113            min_x: i32_le(raw, off),
114            min_y: i32_le(raw, off + 4),
115            max_x: i32_le(raw, off + 8),
116            max_y: i32_le(raw, off + 12),
117        }
118    }
119
120    fn ordered(self) -> bool {
121        self.min_x <= self.max_x && self.min_y <= self.max_y
122    }
123
124    /// Ordered with all bounds in the storage domain (±180°/±90° scaled).
125    pub(crate) fn in_domain(self) -> bool {
126        self.ordered()
127            && self.min_x >= -18_000_000
128            && self.max_x <= 18_000_000
129            && self.min_y >= -9_000_000
130            && self.max_y <= 9_000_000
131    }
132
133    pub(crate) fn contains(self, x: f64, y: f64) -> bool {
134        x >= f64::from(self.min_x)
135            && x <= f64::from(self.max_x)
136            && y >= f64::from(self.min_y)
137            && y <= f64::from(self.max_y)
138    }
139
140    /// Whether a leftward ray from (x, y) can interact with segments inside
141    /// this box: the raycast counts crossings at `lng >= x` only.
142    pub(crate) fn ray_relevant(self, x: f64, y: f64) -> bool {
143        y >= f64::from(self.min_y) && y <= f64::from(self.max_y) && f64::from(self.max_x) >= x
144    }
145}
146
147pub(crate) fn point_in_domain(p: I32Point) -> bool {
148    (-18_000_000..=18_000_000).contains(&p.x) && (-9_000_000..=9_000_000).contains(&p.y)
149}
150
151pub(crate) fn same_point(a: I32Point, b: I32Point) -> bool {
152    a.x == b.x && a.y == b.y
153}
154
155pub(crate) fn align4(n: u64) -> u64 {
156    (n + 3) & !3
157}
158
159// Little-endian field loads. Callers guarantee in-bounds offsets; the slice
160// indexing still panics rather than reads out of bounds if they do not.
161pub(crate) fn u16_le(raw: &[u8], off: usize) -> u16 {
162    u16::from_le_bytes([raw[off], raw[off + 1]])
163}
164
165pub(crate) fn u32_le(raw: &[u8], off: usize) -> u32 {
166    u32::from_le_bytes([raw[off], raw[off + 1], raw[off + 2], raw[off + 3]])
167}
168
169pub(crate) fn u64_le(raw: &[u8], off: usize) -> u64 {
170    let mut b = [0u8; 8];
171    b.copy_from_slice(&raw[off..off + 8]);
172    u64::from_le_bytes(b)
173}
174
175pub(crate) fn i16_le(raw: &[u8], off: usize) -> i16 {
176    u16_le(raw, off) as i16
177}
178
179pub(crate) fn i32_le(raw: &[u8], off: usize) -> i32 {
180    u32_le(raw, off) as i32
181}
182
183/// Adds a decoded delta to the previous coordinate, rejecting i32 overflow.
184pub(crate) fn add_delta(prev: i32, delta: i32) -> Result<i32, Error> {
185    match prev.checked_add(delta) {
186        Some(v) => Ok(v),
187        None => malformed("coordinate overflow"),
188    }
189}
190
191/// CRC32 (IEEE 802.3), the polynomial `hash/crc32.IEEE` uses in Go.
192/// Slicing-by-8: the checksum runs once over the whole file at open, so its
193/// throughput dominates `EmbeddedFinder` open time.
194pub(crate) fn crc32_ieee(data: &[u8]) -> u32 {
195    const TABLES: [[u32; 256]; 8] = crc32_tables();
196    let mut crc = !0u32;
197    let (chunks, remainder) = data.as_chunks::<8>();
198    for chunk in chunks {
199        let lo = u32_le(chunk, 0) ^ crc;
200        let hi = u32_le(chunk, 4);
201        crc = TABLES[7][(lo & 0xff) as usize]
202            ^ TABLES[6][((lo >> 8) & 0xff) as usize]
203            ^ TABLES[5][((lo >> 16) & 0xff) as usize]
204            ^ TABLES[4][(lo >> 24) as usize]
205            ^ TABLES[3][(hi & 0xff) as usize]
206            ^ TABLES[2][((hi >> 8) & 0xff) as usize]
207            ^ TABLES[1][((hi >> 16) & 0xff) as usize]
208            ^ TABLES[0][(hi >> 24) as usize];
209    }
210    for &b in remainder {
211        crc = TABLES[0][((crc ^ u32::from(b)) & 0xff) as usize] ^ (crc >> 8);
212    }
213    !crc
214}
215
216const fn crc32_tables() -> [[u32; 256]; 8] {
217    let mut tables = [[0u32; 256]; 8];
218    let mut i = 0;
219    while i < 256 {
220        let mut crc = i as u32;
221        let mut bit = 0;
222        while bit < 8 {
223            crc = if crc & 1 != 0 {
224                0xEDB8_8320 ^ (crc >> 1)
225            } else {
226                crc >> 1
227            };
228            bit += 1;
229        }
230        tables[0][i] = crc;
231        i += 1;
232    }
233    let mut t = 1;
234    while t < 8 {
235        let mut i = 0;
236        while i < 256 {
237            let prev = tables[t - 1][i];
238            tables[t][i] = tables[0][(prev & 0xff) as usize] ^ (prev >> 8);
239            i += 1;
240        }
241        t += 1;
242    }
243    tables
244}
245
246/// Zigzag-LEB128 varint cursor over one chunk's byte range. Decoders MUST
247/// consume exactly the range: both a varint crossing the boundary and
248/// trailing undecoded bytes are malformed-file errors (spec §6.7).
249pub(crate) struct StreamCursor<'a> {
250    buf: &'a [u8],
251    pos: usize,
252}
253
254impl<'a> StreamCursor<'a> {
255    /// A cursor over `data[pos..end)`; the range is bounds-checked once here
256    /// instead of once per byte.
257    pub(crate) fn new(data: &'a [u8], pos: u64, end: u64) -> Result<Self, Error> {
258        let buf = data
259            .get(pos as usize..end as usize)
260            .ok_or(Error::Malformed("chunk byte range"))?;
261        Ok(Self { buf, pos: 0 })
262    }
263
264    pub(crate) fn at_end(&self) -> bool {
265        self.pos == self.buf.len()
266    }
267
268    #[inline]
269    pub(crate) fn varint(&mut self) -> Result<i32, Error> {
270        let Some(&b0) = self.buf.get(self.pos) else {
271            return malformed("truncated varint");
272        };
273        // One- and two-byte deltas are ~98% of quantized boundary data
274        // (measured: 66%/32% full, 26%/72% lite), so both are inlined.
275        if b0 & 0x80 == 0 {
276            self.pos += 1;
277            let u = u32::from(b0);
278            return Ok(((u >> 1) ^ (u & 1).wrapping_neg()) as i32);
279        }
280        if let Some(&b1) = self.buf.get(self.pos + 1)
281            && b1 & 0x80 == 0
282        {
283            if b1 == 0 {
284                return malformed("nonminimal varint");
285            }
286            self.pos += 2;
287            let u = u32::from(b0 & 0x7f) | u32::from(b1) << 7;
288            return Ok(((u >> 1) ^ (u & 1).wrapping_neg()) as i32);
289        }
290        self.varint_slow(u32::from(b0 & 0x7f))
291    }
292
293    #[inline(never)]
294    fn varint_slow(&mut self, mut u: u32) -> Result<i32, Error> {
295        self.pos += 1;
296        for i in 1..5 {
297            let Some(&b) = self.buf.get(self.pos) else {
298                return malformed("truncated varint");
299            };
300            self.pos += 1;
301            if i == 4 && b & 0xf0 != 0 {
302                return malformed("varint exceeds 32 bits");
303            }
304            u |= u32::from(b & 0x7f) << (7 * i);
305            if b & 0x80 == 0 {
306                if b == 0 {
307                    return malformed("nonminimal varint");
308                }
309                return Ok(((u >> 1) ^ (u & 1).wrapping_neg()) as i32);
310            }
311        }
312        malformed("unterminated varint")
313    }
314}