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}