Skip to main content

geometry_io_wkb/
header.rs

1//! WKB byte-order, error type, and the low-level byte cursor.
2//!
3//! Mirrors the header layout of OGC Simple Feature Access 06-103r4 §8.2:
4//! every WKB record opens with a one-byte endianness flag followed by a
5//! 32-bit geometry-type tag read *in that byte order*. Boost ships no
6//! WKB reader, so there is no C++ counterpart to mirror; the shapes here
7//! follow the OGC spec directly. The [`Cursor`] is the shared, panic-free
8//! byte reader every `parse`/`write` step drives.
9//!
10//! Reference: OGC 06-103r4 §8.2 (Well-Known Binary representation).
11
12/// The two byte orders a WKB record may declare.
13///
14/// The leading byte of every WKB record is `0x00` for big-endian
15/// (network byte order) or `0x01` for little-endian, per OGC 06-103r4
16/// §8.2.3. Every multi-byte scalar in that record is then read/written
17/// in the declared order.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ByteOrder {
20    /// `0x01` — least-significant byte first.
21    LittleEndian,
22    /// `0x00` — most-significant byte first (XDR / network order).
23    BigEndian,
24}
25
26/// Everything that can go wrong reading WKB.
27///
28/// Covers the cursor's bounds failures plus the parser's header-level
29/// failures, so a single error type flows through the whole read path
30/// (mirroring how [`crate::from_wkb`] surfaces one error kind, the way
31/// the sibling WKT reader funnels through `WktError`).
32#[derive(Debug, Clone, PartialEq)]
33pub enum WkbError {
34    /// The cursor ran off the end of the buffer while a read was still
35    /// in progress.
36    UnexpectedEof,
37    /// The leading byte-order flag was neither `0x00` nor `0x01`.
38    InvalidByteOrder(u8),
39    /// The 32-bit geometry-type tag is not one of the seven OGC base
40    /// codes (`1`..=`7`).
41    UnknownGeometryType(u32),
42    /// The geometry-type tag carried a `Z`, `M`, or `ZM` dimension flag
43    /// (high bits `0x8000_0000` / `0x4000_0000`, or the ISO `1000`+
44    /// ranges). This is a strictly-2D port and rejects higher
45    /// dimensions rather than silently dropping ordinates.
46    UnsupportedDimension,
47    /// The top-level geometry was parsed successfully but bytes remained
48    /// in the buffer afterwards.
49    TrailingBytes,
50    /// The multi / collection nesting exceeded the reader's recursion
51    /// limit. Rejecting deep nesting keeps a hostile buffer from
52    /// overflowing the native stack (an uncatchable process abort).
53    NestingTooDeep,
54    /// A multi-geometry member record parsed successfully but has the
55    /// wrong kind — e.g. a `MultiPoint` whose member is a
56    /// `LineString`. Both codes are OGC base type codes (`1`..=`7`).
57    MismatchedMemberType {
58        /// The member code the container requires.
59        expected: u32,
60        /// The member code actually found.
61        found: u32,
62    },
63}
64
65impl core::fmt::Display for WkbError {
66    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
67        match self {
68            WkbError::UnexpectedEof => f.write_str("unexpected end of WKB input"),
69            WkbError::InvalidByteOrder(b) => {
70                write!(
71                    f,
72                    "invalid byte-order flag {b:#04x} (expected 0x00 or 0x01)"
73                )
74            }
75            WkbError::UnknownGeometryType(t) => write!(f, "unknown WKB geometry type {t}"),
76            WkbError::UnsupportedDimension => {
77                f.write_str("unsupported WKB dimension (Z/M ordinates); this reader is 2D only")
78            }
79            WkbError::TrailingBytes => f.write_str("trailing bytes after WKB geometry"),
80            WkbError::NestingTooDeep => {
81                f.write_str("WKB nesting too deep; exceeded the reader's recursion limit")
82            }
83            WkbError::MismatchedMemberType { expected, found } => write!(
84                f,
85                "WKB multi-geometry member has type {found}, expected {expected}"
86            ),
87        }
88    }
89}
90
91#[cfg(feature = "std")]
92impl std::error::Error for WkbError {}
93
94/// A panic-free cursor over a WKB byte buffer.
95///
96/// Every read is bounds-checked and yields [`WkbError::UnexpectedEof`]
97/// rather than indexing out of range — the crate forbids `unsafe`, so
98/// all slice access flows through these methods. Multi-byte reads take
99/// the [`ByteOrder`] of the enclosing record and use
100/// `u32::from_le_bytes` / `from_be_bytes` (and the `f64` equivalents),
101/// so no external `byteorder` dependency is needed.
102pub(crate) struct Cursor<'a> {
103    bytes: &'a [u8],
104    pos: usize,
105}
106
107impl<'a> Cursor<'a> {
108    /// Wrap a byte slice at position zero.
109    pub(crate) fn new(bytes: &'a [u8]) -> Self {
110        Self { bytes, pos: 0 }
111    }
112
113    /// `true` once every byte has been consumed. Used by
114    /// [`crate::from_wkb`] to detect [`WkbError::TrailingBytes`].
115    pub(crate) fn is_empty(&self) -> bool {
116        self.pos >= self.bytes.len()
117    }
118
119    /// Bytes left to read. Used to bound speculative `Vec::with_capacity`
120    /// reservations against a possibly-hostile element count so a
121    /// corrupt/malicious buffer cannot request a huge allocation before a
122    /// single element is read.
123    pub(crate) fn remaining(&self) -> usize {
124        self.bytes.len().saturating_sub(self.pos)
125    }
126
127    /// Read `len` bytes as one borrowed slice, advancing the cursor.
128    /// Point runs use this to validate their complete fixed-width body
129    /// once instead of repeating a bounds check for every ordinate.
130    pub(crate) fn read_slice(&mut self, len: usize) -> Result<&'a [u8], WkbError> {
131        let end = self.pos.checked_add(len).ok_or(WkbError::UnexpectedEof)?;
132        let slice = self
133            .bytes
134            .get(self.pos..end)
135            .ok_or(WkbError::UnexpectedEof)?;
136        self.pos = end;
137        Ok(slice)
138    }
139
140    /// Read the next `N` bytes as a fixed-size array, advancing the
141    /// cursor. Fails with [`WkbError::UnexpectedEof`] if fewer than `N`
142    /// bytes remain.
143    fn read_array<const N: usize>(&mut self) -> Result<[u8; N], WkbError> {
144        let slice = self.read_slice(N)?;
145        let mut buf = [0u8; N];
146        buf.copy_from_slice(slice);
147        Ok(buf)
148    }
149
150    /// Read one byte, advancing the cursor.
151    ///
152    /// # Errors
153    ///
154    /// [`WkbError::UnexpectedEof`] if the buffer is exhausted.
155    pub(crate) fn read_u8(&mut self) -> Result<u8, WkbError> {
156        Ok(self.read_array::<1>()?[0])
157    }
158
159    /// Read a 32-bit unsigned integer in the given byte order.
160    ///
161    /// # Errors
162    ///
163    /// [`WkbError::UnexpectedEof`] if fewer than four bytes remain.
164    pub(crate) fn read_u32(&mut self, order: ByteOrder) -> Result<u32, WkbError> {
165        let b = self.read_array::<4>()?;
166        Ok(match order {
167            ByteOrder::LittleEndian => u32::from_le_bytes(b),
168            ByteOrder::BigEndian => u32::from_be_bytes(b),
169        })
170    }
171
172    /// Read a 64-bit IEEE-754 float in the given byte order.
173    ///
174    /// # Errors
175    ///
176    /// [`WkbError::UnexpectedEof`] if fewer than eight bytes remain.
177    pub(crate) fn read_f64(&mut self, order: ByteOrder) -> Result<f64, WkbError> {
178        let b = self.read_array::<8>()?;
179        Ok(match order {
180            ByteOrder::LittleEndian => f64::from_le_bytes(b),
181            ByteOrder::BigEndian => f64::from_be_bytes(b),
182        })
183    }
184
185    /// Read the one-byte endianness flag that opens a WKB record
186    /// (OGC 06-103r4 §8.2.3): `0x00` → big-endian, `0x01` → little.
187    ///
188    /// # Errors
189    ///
190    /// [`WkbError::UnexpectedEof`] at end of input, or
191    /// [`WkbError::InvalidByteOrder`] for any other flag byte.
192    pub(crate) fn read_byte_order(&mut self) -> Result<ByteOrder, WkbError> {
193        match self.read_u8()? {
194            0x00 => Ok(ByteOrder::BigEndian),
195            0x01 => Ok(ByteOrder::LittleEndian),
196            other => Err(WkbError::InvalidByteOrder(other)),
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    //! Cursor-level witnesses against hand-crafted bytes, per OGC
204    //! 06-103r4 §8.2.
205
206    use super::*;
207
208    #[test]
209    fn reads_le_u32() {
210        // 0x0000_0001 little-endian.
211        let mut c = Cursor::new(&[0x01, 0x00, 0x00, 0x00]);
212        assert_eq!(c.read_u32(ByteOrder::LittleEndian).unwrap(), 1);
213    }
214
215    #[test]
216    fn reads_be_u32() {
217        // 0x0000_0001 big-endian.
218        let mut c = Cursor::new(&[0x00, 0x00, 0x00, 0x01]);
219        assert_eq!(c.read_u32(ByteOrder::BigEndian).unwrap(), 1);
220    }
221
222    #[test]
223    fn reads_byte_order_flags() {
224        assert_eq!(
225            Cursor::new(&[0x00]).read_byte_order().unwrap(),
226            ByteOrder::BigEndian
227        );
228        assert_eq!(
229            Cursor::new(&[0x01]).read_byte_order().unwrap(),
230            ByteOrder::LittleEndian
231        );
232    }
233
234    #[test]
235    fn invalid_byte_order_is_rejected() {
236        let err = Cursor::new(&[0x02]).read_byte_order().unwrap_err();
237        assert_eq!(err, WkbError::InvalidByteOrder(0x02));
238    }
239
240    #[test]
241    fn short_buffer_is_eof() {
242        let err = Cursor::new(&[0x00, 0x00])
243            .read_u32(ByteOrder::LittleEndian)
244            .unwrap_err();
245        assert_eq!(err, WkbError::UnexpectedEof);
246    }
247
248    /// Every `WkbError` variant renders a distinct, descriptive message
249    /// through its `Display` impl, including the embedded byte/type/code
250    /// values.
251    #[test]
252    fn every_error_variant_displays_descriptively() {
253        extern crate alloc;
254        use alloc::format;
255
256        assert_eq!(
257            format!("{}", WkbError::UnexpectedEof),
258            "unexpected end of WKB input"
259        );
260        assert_eq!(
261            format!("{}", WkbError::InvalidByteOrder(0x02)),
262            "invalid byte-order flag 0x02 (expected 0x00 or 0x01)"
263        );
264        assert_eq!(
265            format!("{}", WkbError::UnknownGeometryType(9)),
266            "unknown WKB geometry type 9"
267        );
268        assert!(
269            format!("{}", WkbError::UnsupportedDimension).contains("2D only"),
270            "dimension message"
271        );
272        assert_eq!(
273            format!("{}", WkbError::TrailingBytes),
274            "trailing bytes after WKB geometry"
275        );
276        assert!(
277            format!("{}", WkbError::NestingTooDeep).contains("nesting too deep"),
278            "nesting message"
279        );
280        assert_eq!(
281            format!(
282                "{}",
283                WkbError::MismatchedMemberType {
284                    expected: 1,
285                    found: 2
286                }
287            ),
288            "WKB multi-geometry member has type 2, expected 1"
289        );
290    }
291}