Skip to main content

geonative_core/
wkb.rs

1//! OGC Simple Features **Well-Known Binary** (WKB) encoder + decoder for [`Geometry`].
2//!
3//! ## What this module is
4//!
5//! The codec for the OGC SF binary geometry format — the lingua franca that
6//! every "serious" spatial system speaks: PostGIS, SpatiaLite, GeoPackage,
7//! FlatGeoBuf, GeoParquet, MVT (variants), Shapefile (related). If we encode
8//! once and decode once correctly, every downstream format becomes a
9//! mechanical translation.
10//!
11//! ## How it sits in the architecture
12//!
13//! - **Encoder** ([`Geometry::to_wkb`]) walks the `Geometry` enum tree and
14//!   serializes bytes. Used by `geonative-geoparquet` (and any future GPKG /
15//!   MVT writer).
16//! - **Decoder** ([`Geometry::from_wkb`]) is the inverse — bytes → enum tree.
17//!   Used by future GeoParquet/GPKG readers.
18//! - **BBox fast-path** ([`bbox_from_bytes`]) walks WKB without materializing
19//!   the `Geometry` — for spatial filters that only need an envelope test.
20//!
21//! ## Clever bits
22//!
23//! - **Per-nested byte order.** ISO WKB allows each sub-geometry inside a
24//!   collection to carry its own byte-order byte. The decoder respects this;
25//!   the encoder always emits LE for simplicity.
26//! - **`POINT EMPTY` as NaN/NaN.** The PostGIS/GEOS convention. Other empties
27//!   serialize as their type code + child count 0.
28//! - **2D-only scope.** Z/M variants are recognized by their type-code high
29//!   bits (`0x80000000` / `0x40000000`) and ISO-extended codes (`1001..`); the
30//!   decoder errors with a clear "unsupported" message rather than silently
31//!   dropping coordinates. v0.1 scope cut; revisit in v0.2.
32//! - **`write_wkb_into`** lets callers reuse a scratch buffer across rows —
33//!   the GeoParquet writer uses this in its row-by-row loop to avoid
34//!   per-feature allocation.
35//!
36//! ## Type codes (2D)
37//!
38//! | Code | Type |
39//! | ---: | --- |
40//! | 1 | Point |
41//! | 2 | LineString |
42//! | 3 | Polygon |
43//! | 4 | MultiPoint |
44//! | 5 | MultiLineString |
45//! | 6 | MultiPolygon |
46//! | 7 | GeometryCollection |
47//!
48//! ## Empty geometries
49//!
50//! - `POINT EMPTY` is encoded with `NaN` for both X and Y (the convention
51//!   used by PostGIS and GEOS).
52//! - Collection-shaped empties (LineString, Polygon, Multi\*, Collection)
53//!   serialize their child-count as 0.
54//!
55//! ## Z / M
56//!
57//! v0.1 emits 2D only. Geometries carrying Z or M ordinates drop them
58//! silently — callers concerned with Z/M fidelity should error out before
59//! encoding (the IR carries `has_z()` / `has_m()` for that purpose).
60
61use crate::error::{Error, Result};
62use crate::geometry::{Coord, Geometry, GeometryType, LineString, Polygon};
63
64const BYTE_ORDER_LE: u8 = 1;
65
66const WKB_POINT: u32 = 1;
67const WKB_LINESTRING: u32 = 2;
68const WKB_POLYGON: u32 = 3;
69const WKB_MULTIPOINT: u32 = 4;
70const WKB_MULTILINESTRING: u32 = 5;
71const WKB_MULTIPOLYGON: u32 = 6;
72const WKB_GEOMETRYCOLLECTION: u32 = 7;
73
74impl Geometry {
75    /// Encode as OGC Simple Features Well-Known Binary (little-endian, 2D).
76    pub fn to_wkb(&self) -> Vec<u8> {
77        let mut out = Vec::with_capacity(estimated_size(self));
78        write_geometry(self, &mut out);
79        out
80    }
81
82    /// Encode WKB into a caller-provided buffer (clears it first). Useful for
83    /// row-by-row writers that want to reuse a scratch buffer.
84    pub fn write_wkb_into(&self, buf: &mut Vec<u8>) {
85        buf.clear();
86        buf.reserve(estimated_size(self));
87        write_geometry(self, buf);
88    }
89}
90
91fn estimated_size(g: &Geometry) -> usize {
92    // 5 bytes preamble + 4 (count) + n_coords * 16 in the worst case.
93    // We just upper-bound a bit; Vec will grow if we underestimate.
94    let coord_count = count_coords(g);
95    5 + 4 + coord_count * 16
96}
97
98fn count_coords(g: &Geometry) -> usize {
99    match g {
100        Geometry::Point(_) => 1,
101        Geometry::LineString(ls) => ls.coords.len(),
102        Geometry::Polygon(p) => {
103            p.exterior.coords.len() + p.holes.iter().map(|h| h.coords.len()).sum::<usize>()
104        }
105        Geometry::MultiPoint(v) => v.len(),
106        Geometry::MultiLineString(v) => v.iter().map(|ls| ls.coords.len()).sum(),
107        Geometry::MultiPolygon(v) => v
108            .iter()
109            .map(|p| {
110                p.exterior.coords.len() + p.holes.iter().map(|h| h.coords.len()).sum::<usize>()
111            })
112            .sum(),
113        Geometry::GeometryCollection(v) => v.iter().map(count_coords).sum(),
114        Geometry::Empty(_) => 0,
115    }
116}
117
118fn write_geometry(g: &Geometry, out: &mut Vec<u8>) {
119    match g {
120        Geometry::Point(c) => write_point(c, out),
121        Geometry::LineString(ls) => write_linestring(ls, out),
122        Geometry::Polygon(p) => write_polygon(p, out),
123        Geometry::MultiPoint(v) => write_multipoint(v, out),
124        Geometry::MultiLineString(v) => write_multilinestring(v, out),
125        Geometry::MultiPolygon(v) => write_multipolygon(v, out),
126        Geometry::GeometryCollection(v) => write_collection(v, out),
127        Geometry::Empty(t) => write_empty(*t, out),
128    }
129}
130
131fn write_preamble(out: &mut Vec<u8>, type_code: u32) {
132    out.push(BYTE_ORDER_LE);
133    out.extend_from_slice(&type_code.to_le_bytes());
134}
135
136fn write_xy(c: &Coord, out: &mut Vec<u8>) {
137    out.extend_from_slice(&c.x.to_le_bytes());
138    out.extend_from_slice(&c.y.to_le_bytes());
139}
140
141fn write_point(c: &Coord, out: &mut Vec<u8>) {
142    write_preamble(out, WKB_POINT);
143    write_xy(c, out);
144}
145
146fn write_linestring(ls: &LineString, out: &mut Vec<u8>) {
147    write_preamble(out, WKB_LINESTRING);
148    out.extend_from_slice(&(ls.coords.len() as u32).to_le_bytes());
149    for c in &ls.coords {
150        write_xy(c, out);
151    }
152}
153
154fn write_polygon(p: &Polygon, out: &mut Vec<u8>) {
155    write_preamble(out, WKB_POLYGON);
156    let n_rings = 1 + p.holes.len();
157    out.extend_from_slice(&(n_rings as u32).to_le_bytes());
158    write_ring(&p.exterior, out);
159    for h in &p.holes {
160        write_ring(h, out);
161    }
162}
163
164fn write_ring(ls: &LineString, out: &mut Vec<u8>) {
165    out.extend_from_slice(&(ls.coords.len() as u32).to_le_bytes());
166    for c in &ls.coords {
167        write_xy(c, out);
168    }
169}
170
171fn write_multipoint(pts: &[Coord], out: &mut Vec<u8>) {
172    write_preamble(out, WKB_MULTIPOINT);
173    out.extend_from_slice(&(pts.len() as u32).to_le_bytes());
174    for c in pts {
175        write_point(c, out);
176    }
177}
178
179fn write_multilinestring(parts: &[LineString], out: &mut Vec<u8>) {
180    write_preamble(out, WKB_MULTILINESTRING);
181    out.extend_from_slice(&(parts.len() as u32).to_le_bytes());
182    for ls in parts {
183        write_linestring(ls, out);
184    }
185}
186
187fn write_multipolygon(polys: &[Polygon], out: &mut Vec<u8>) {
188    write_preamble(out, WKB_MULTIPOLYGON);
189    out.extend_from_slice(&(polys.len() as u32).to_le_bytes());
190    for p in polys {
191        write_polygon(p, out);
192    }
193}
194
195fn write_collection(geoms: &[Geometry], out: &mut Vec<u8>) {
196    write_preamble(out, WKB_GEOMETRYCOLLECTION);
197    out.extend_from_slice(&(geoms.len() as u32).to_le_bytes());
198    for g in geoms {
199        write_geometry(g, out);
200    }
201}
202
203fn write_empty(t: GeometryType, out: &mut Vec<u8>) {
204    match t {
205        GeometryType::Point => {
206            // POINT EMPTY: x = y = NaN.
207            write_preamble(out, WKB_POINT);
208            out.extend_from_slice(&f64::NAN.to_le_bytes());
209            out.extend_from_slice(&f64::NAN.to_le_bytes());
210        }
211        GeometryType::LineString => write_preamble_with_zero_count(out, WKB_LINESTRING),
212        GeometryType::Polygon => write_preamble_with_zero_count(out, WKB_POLYGON),
213        GeometryType::MultiPoint => write_preamble_with_zero_count(out, WKB_MULTIPOINT),
214        GeometryType::MultiLineString => write_preamble_with_zero_count(out, WKB_MULTILINESTRING),
215        GeometryType::MultiPolygon => write_preamble_with_zero_count(out, WKB_MULTIPOLYGON),
216        GeometryType::GeometryCollection => {
217            write_preamble_with_zero_count(out, WKB_GEOMETRYCOLLECTION)
218        }
219    }
220}
221
222fn write_preamble_with_zero_count(out: &mut Vec<u8>, type_code: u32) {
223    write_preamble(out, type_code);
224    out.extend_from_slice(&0u32.to_le_bytes());
225}
226
227// ============================================================================
228// Decoder
229// ============================================================================
230
231/// Mask off the high bits used for Z/M/curve modifiers on the "Extended" WKB
232/// type codes. We only consume the base type code (1..=7).
233const TYPE_LOW_MASK: u32 = 0xFF;
234/// ISO-extended Z bit (some implementations encode 3D this way).
235const ISO_Z_FLAG: u32 = 0x8000_0000;
236/// ISO-extended M bit.
237const ISO_M_FLAG: u32 = 0x4000_0000;
238
239impl Geometry {
240    /// Decode OGC Simple Features WKB bytes into a [`Geometry`].
241    ///
242    /// Accepts both little-endian and big-endian, and respects per-nested
243    /// byte-order bytes inside collections (the ISO WKB rule).
244    ///
245    /// v0.1 returns [`Error::Unsupported`] for Z/M variants — the type code
246    /// is recognized but ordinates beyond X/Y are not decoded.
247    pub fn from_wkb(bytes: &[u8]) -> Result<Self> {
248        let mut c = WkbCursor::new(bytes);
249        decode_geometry(&mut c)
250    }
251}
252
253/// Scan WKB bytes and compute `[xmin, ymin, xmax, ymax]` **without
254/// materializing** the [`Geometry`]. Faster than `from_wkb(bytes)?.bbox()`
255/// because no allocations happen for coords/rings.
256///
257/// Returns `None` for input that doesn't contain any finite coordinate
258/// (e.g. `POINT EMPTY`, empty collections, or malformed bytes).
259pub fn bbox_from_bytes(bytes: &[u8]) -> Option<[f64; 4]> {
260    let mut c = WkbCursor::new(bytes);
261    let mut acc = BboxAcc::default();
262    walk_bbox(&mut c, &mut acc).ok()?;
263    acc.finish()
264}
265
266#[derive(Default)]
267struct BboxAcc {
268    have_any: bool,
269    xmin: f64,
270    ymin: f64,
271    xmax: f64,
272    ymax: f64,
273}
274
275impl BboxAcc {
276    fn add(&mut self, x: f64, y: f64) {
277        if !x.is_finite() || !y.is_finite() {
278            return;
279        }
280        if !self.have_any {
281            self.have_any = true;
282            self.xmin = x;
283            self.ymin = y;
284            self.xmax = x;
285            self.ymax = y;
286        } else {
287            if x < self.xmin {
288                self.xmin = x;
289            }
290            if y < self.ymin {
291                self.ymin = y;
292            }
293            if x > self.xmax {
294                self.xmax = x;
295            }
296            if y > self.ymax {
297                self.ymax = y;
298            }
299        }
300    }
301
302    fn finish(self) -> Option<[f64; 4]> {
303        if self.have_any {
304            Some([self.xmin, self.ymin, self.xmax, self.ymax])
305        } else {
306            None
307        }
308    }
309}
310
311/// Cursor over WKB bytes. Carries no byte-order state itself; each call to
312/// [`read_u32`] / [`read_f64`] takes the `le` flag explicitly because nested
313/// geometries inside collections can flip endianness.
314struct WkbCursor<'a> {
315    bytes: &'a [u8],
316    pos: usize,
317}
318
319impl<'a> WkbCursor<'a> {
320    fn new(bytes: &'a [u8]) -> Self {
321        Self { bytes, pos: 0 }
322    }
323
324    fn need(&self, n: usize) -> Result<()> {
325        if self.pos + n > self.bytes.len() {
326            Err(Error::malformed(format!(
327                "WKB truncated at byte {}: need {} more, have {}",
328                self.pos,
329                n,
330                self.bytes.len().saturating_sub(self.pos)
331            )))
332        } else {
333            Ok(())
334        }
335    }
336
337    fn read_u8(&mut self) -> Result<u8> {
338        self.need(1)?;
339        let v = self.bytes[self.pos];
340        self.pos += 1;
341        Ok(v)
342    }
343
344    fn read_u32(&mut self, le: bool) -> Result<u32> {
345        self.need(4)?;
346        let s = &self.bytes[self.pos..self.pos + 4];
347        let arr: [u8; 4] = s.try_into().unwrap();
348        self.pos += 4;
349        Ok(if le {
350            u32::from_le_bytes(arr)
351        } else {
352            u32::from_be_bytes(arr)
353        })
354    }
355
356    fn read_f64(&mut self, le: bool) -> Result<f64> {
357        self.need(8)?;
358        let s = &self.bytes[self.pos..self.pos + 8];
359        let arr: [u8; 8] = s.try_into().unwrap();
360        self.pos += 8;
361        Ok(if le {
362            f64::from_le_bytes(arr)
363        } else {
364            f64::from_be_bytes(arr)
365        })
366    }
367
368    /// Read the standard preamble (byte order + type code) and split out the
369    /// base type vs the Z/M flags.
370    fn read_preamble(&mut self) -> Result<(bool, u32, bool, bool)> {
371        let bo = self.read_u8()?;
372        let le = match bo {
373            0 => false,
374            1 => true,
375            other => {
376                return Err(Error::malformed(format!(
377                    "WKB byte-order byte {other}; expected 0 or 1"
378                )))
379            }
380        };
381        let raw_type = self.read_u32(le)?;
382        // Detect Z/M via either the ISO high bits or the extended 1000-series codes.
383        let iso_has_z = raw_type & ISO_Z_FLAG != 0;
384        let iso_has_m = raw_type & ISO_M_FLAG != 0;
385        let base = raw_type & TYPE_LOW_MASK;
386        let (base, range_has_z, range_has_m) = if (1001..=1007).contains(&raw_type) {
387            (raw_type - 1000, true, false)
388        } else if (2001..=2007).contains(&raw_type) {
389            (raw_type - 2000, false, true)
390        } else if (3001..=3007).contains(&raw_type) {
391            (raw_type - 3000, true, true)
392        } else {
393            (base, false, false)
394        };
395        Ok((le, base, iso_has_z || range_has_z, iso_has_m || range_has_m))
396    }
397}
398
399fn decode_geometry(c: &mut WkbCursor) -> Result<Geometry> {
400    let (le, base, has_z, has_m) = c.read_preamble()?;
401    if has_z || has_m {
402        return Err(Error::unsupported(
403            "WKB Z/M geometries (v0.1 is 2D-only)".to_string(),
404        ));
405    }
406    match base {
407        1 => decode_point(c, le),
408        2 => decode_linestring(c, le),
409        3 => decode_polygon(c, le),
410        4 => decode_multipoint(c, le),
411        5 => decode_multilinestring(c, le),
412        6 => decode_multipolygon(c, le),
413        7 => decode_collection(c, le),
414        other => Err(Error::unsupported(format!(
415            "WKB geometry type code {other}"
416        ))),
417    }
418}
419
420fn decode_point(c: &mut WkbCursor, le: bool) -> Result<Geometry> {
421    let x = c.read_f64(le)?;
422    let y = c.read_f64(le)?;
423    if x.is_nan() && y.is_nan() {
424        return Ok(Geometry::Empty(GeometryType::Point));
425    }
426    Ok(Geometry::Point(Coord::xy(x, y)))
427}
428
429fn decode_linestring_coords(c: &mut WkbCursor, le: bool) -> Result<Vec<Coord>> {
430    let n = c.read_u32(le)? as usize;
431    let mut coords = Vec::with_capacity(n);
432    for _ in 0..n {
433        let x = c.read_f64(le)?;
434        let y = c.read_f64(le)?;
435        coords.push(Coord::xy(x, y));
436    }
437    Ok(coords)
438}
439
440fn decode_linestring(c: &mut WkbCursor, le: bool) -> Result<Geometry> {
441    let coords = decode_linestring_coords(c, le)?;
442    if coords.is_empty() {
443        Ok(Geometry::Empty(GeometryType::LineString))
444    } else {
445        Ok(Geometry::LineString(LineString::new(coords)))
446    }
447}
448
449fn decode_polygon(c: &mut WkbCursor, le: bool) -> Result<Geometry> {
450    let n_rings = c.read_u32(le)? as usize;
451    if n_rings == 0 {
452        return Ok(Geometry::Empty(GeometryType::Polygon));
453    }
454    let mut rings: Vec<LineString> = Vec::with_capacity(n_rings);
455    for _ in 0..n_rings {
456        rings.push(LineString::new(decode_linestring_coords(c, le)?));
457    }
458    let exterior = rings.remove(0);
459    Ok(Geometry::Polygon(Polygon::new(exterior, rings)))
460}
461
462fn decode_multipoint(c: &mut WkbCursor, le: bool) -> Result<Geometry> {
463    // Children have their own headers per ISO WKB. The outer `le` flag is
464    // ignored once we're inside the child.
465    let _ = le;
466    let n = c.read_u32_after_check(le)?;
467    if n == 0 {
468        return Ok(Geometry::Empty(GeometryType::MultiPoint));
469    }
470    let mut pts = Vec::with_capacity(n);
471    for _ in 0..n {
472        match decode_geometry(c)? {
473            Geometry::Point(p) => pts.push(p),
474            Geometry::Empty(GeometryType::Point) => pts.push(Coord::xy(f64::NAN, f64::NAN)),
475            other => {
476                return Err(Error::malformed(format!(
477                    "MultiPoint child must be Point, got {:?}",
478                    other.type_of()
479                )))
480            }
481        }
482    }
483    Ok(Geometry::MultiPoint(pts))
484}
485
486fn decode_multilinestring(c: &mut WkbCursor, le: bool) -> Result<Geometry> {
487    let n = c.read_u32_after_check(le)?;
488    if n == 0 {
489        return Ok(Geometry::Empty(GeometryType::MultiLineString));
490    }
491    let mut parts = Vec::with_capacity(n);
492    for _ in 0..n {
493        match decode_geometry(c)? {
494            Geometry::LineString(ls) => parts.push(ls),
495            Geometry::Empty(GeometryType::LineString) => parts.push(LineString::default()),
496            other => {
497                return Err(Error::malformed(format!(
498                    "MultiLineString child must be LineString, got {:?}",
499                    other.type_of()
500                )))
501            }
502        }
503    }
504    Ok(Geometry::MultiLineString(parts))
505}
506
507fn decode_multipolygon(c: &mut WkbCursor, le: bool) -> Result<Geometry> {
508    let n = c.read_u32_after_check(le)?;
509    if n == 0 {
510        return Ok(Geometry::Empty(GeometryType::MultiPolygon));
511    }
512    let mut polys = Vec::with_capacity(n);
513    for _ in 0..n {
514        match decode_geometry(c)? {
515            Geometry::Polygon(p) => polys.push(p),
516            Geometry::Empty(GeometryType::Polygon) => polys.push(Polygon::default()),
517            other => {
518                return Err(Error::malformed(format!(
519                    "MultiPolygon child must be Polygon, got {:?}",
520                    other.type_of()
521                )))
522            }
523        }
524    }
525    Ok(Geometry::MultiPolygon(polys))
526}
527
528fn decode_collection(c: &mut WkbCursor, le: bool) -> Result<Geometry> {
529    let n = c.read_u32_after_check(le)?;
530    if n == 0 {
531        return Ok(Geometry::Empty(GeometryType::GeometryCollection));
532    }
533    let mut v = Vec::with_capacity(n);
534    for _ in 0..n {
535        v.push(decode_geometry(c)?);
536    }
537    Ok(Geometry::GeometryCollection(v))
538}
539
540impl<'a> WkbCursor<'a> {
541    /// Read the child count u32, using the parent's byte order (the preamble
542    /// for the children themselves is read by recursing into `decode_geometry`).
543    fn read_u32_after_check(&mut self, le: bool) -> Result<usize> {
544        Ok(self.read_u32(le)? as usize)
545    }
546}
547
548// ============================================================================
549// Bbox walker — same control flow as decoder, but no allocations.
550// ============================================================================
551
552fn walk_bbox(c: &mut WkbCursor, acc: &mut BboxAcc) -> Result<()> {
553    let (le, base, has_z, has_m) = c.read_preamble()?;
554    // Even for Z/M we could compute a 2D bbox by skipping extra ordinates,
555    // but v0.1 stays consistent with the decoder: reject Z/M.
556    if has_z || has_m {
557        return Err(Error::unsupported("WKB Z/M (v0.1 is 2D-only)".to_string()));
558    }
559    match base {
560        1 => {
561            let x = c.read_f64(le)?;
562            let y = c.read_f64(le)?;
563            // POINT EMPTY = NaN/NaN, filtered by `acc.add`.
564            acc.add(x, y);
565        }
566        2 => walk_xy_array(c, le, acc)?,
567        3 => {
568            let n_rings = c.read_u32(le)? as usize;
569            for _ in 0..n_rings {
570                walk_xy_array(c, le, acc)?;
571            }
572        }
573        4..=7 => {
574            let n = c.read_u32(le)? as usize;
575            for _ in 0..n {
576                walk_bbox(c, acc)?;
577            }
578        }
579        other => {
580            return Err(Error::unsupported(format!(
581                "WKB geometry type code {other}"
582            )))
583        }
584    }
585    Ok(())
586}
587
588fn walk_xy_array(c: &mut WkbCursor, le: bool, acc: &mut BboxAcc) -> Result<()> {
589    let n = c.read_u32(le)? as usize;
590    for _ in 0..n {
591        let x = c.read_f64(le)?;
592        let y = c.read_f64(le)?;
593        acc.add(x, y);
594    }
595    Ok(())
596}
597
598#[cfg(test)]
599mod decoder_tests {
600    use super::*;
601
602    fn round_trip(g: &Geometry) {
603        let bytes = g.to_wkb();
604        let back = Geometry::from_wkb(&bytes).expect("decode");
605        assert_eq!(&back, g, "round-trip mismatch for {g:?}");
606    }
607
608    #[test]
609    fn round_trip_point() {
610        round_trip(&Geometry::Point(Coord::xy(1.0, 2.0)));
611        round_trip(&Geometry::Point(Coord::xy(-180.0, 90.0)));
612    }
613
614    #[test]
615    fn round_trip_point_empty() {
616        let g = Geometry::Empty(GeometryType::Point);
617        let bytes = g.to_wkb();
618        let back = Geometry::from_wkb(&bytes).unwrap();
619        // POINT EMPTY round-trips through NaN/NaN and is normalized back to Empty.
620        assert_eq!(back, g);
621    }
622
623    #[test]
624    fn round_trip_linestring() {
625        let ls = LineString::new(vec![
626            Coord::xy(0.0, 0.0),
627            Coord::xy(1.0, 1.0),
628            Coord::xy(2.0, 0.0),
629        ]);
630        round_trip(&Geometry::LineString(ls));
631    }
632
633    #[test]
634    fn round_trip_polygon_with_hole() {
635        let p = Polygon::new(
636            LineString::new(vec![
637                Coord::xy(0.0, 0.0),
638                Coord::xy(10.0, 0.0),
639                Coord::xy(10.0, 10.0),
640                Coord::xy(0.0, 10.0),
641                Coord::xy(0.0, 0.0),
642            ]),
643            vec![LineString::new(vec![
644                Coord::xy(2.0, 2.0),
645                Coord::xy(4.0, 2.0),
646                Coord::xy(4.0, 4.0),
647                Coord::xy(2.0, 4.0),
648                Coord::xy(2.0, 2.0),
649            ])],
650        );
651        round_trip(&Geometry::Polygon(p));
652    }
653
654    #[test]
655    fn round_trip_multipoint() {
656        round_trip(&Geometry::MultiPoint(vec![
657            Coord::xy(0.0, 0.0),
658            Coord::xy(1.0, 2.0),
659            Coord::xy(-3.0, 4.5),
660        ]));
661    }
662
663    #[test]
664    fn round_trip_multilinestring() {
665        round_trip(&Geometry::MultiLineString(vec![
666            LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(1.0, 1.0)]),
667            LineString::new(vec![
668                Coord::xy(2.0, 2.0),
669                Coord::xy(3.0, 3.0),
670                Coord::xy(4.0, 4.0),
671            ]),
672        ]));
673    }
674
675    #[test]
676    fn round_trip_multipolygon() {
677        let p1 = Polygon::new(
678            LineString::new(vec![
679                Coord::xy(0.0, 0.0),
680                Coord::xy(1.0, 0.0),
681                Coord::xy(1.0, 1.0),
682                Coord::xy(0.0, 0.0),
683            ]),
684            vec![],
685        );
686        let p2 = Polygon::new(
687            LineString::new(vec![
688                Coord::xy(10.0, 10.0),
689                Coord::xy(11.0, 10.0),
690                Coord::xy(11.0, 11.0),
691                Coord::xy(10.0, 10.0),
692            ]),
693            vec![],
694        );
695        round_trip(&Geometry::MultiPolygon(vec![p1, p2]));
696    }
697
698    #[test]
699    fn round_trip_geometry_collection() {
700        round_trip(&Geometry::GeometryCollection(vec![
701            Geometry::Point(Coord::xy(1.0, 2.0)),
702            Geometry::LineString(LineString::new(vec![
703                Coord::xy(0.0, 0.0),
704                Coord::xy(1.0, 1.0),
705            ])),
706        ]));
707    }
708
709    #[test]
710    fn round_trip_empty_collection_shapes() {
711        for t in [
712            GeometryType::LineString,
713            GeometryType::Polygon,
714            GeometryType::MultiPoint,
715            GeometryType::MultiLineString,
716            GeometryType::MultiPolygon,
717            GeometryType::GeometryCollection,
718        ] {
719            round_trip(&Geometry::Empty(t));
720        }
721    }
722
723    #[test]
724    fn decode_be_encoded_point() {
725        // Hand-craft a big-endian Point(3.0, 4.0).
726        let mut buf = vec![0u8]; // BE
727        buf.extend_from_slice(&1u32.to_be_bytes());
728        buf.extend_from_slice(&3.0f64.to_be_bytes());
729        buf.extend_from_slice(&4.0f64.to_be_bytes());
730        let g = Geometry::from_wkb(&buf).unwrap();
731        assert_eq!(g, Geometry::Point(Coord::xy(3.0, 4.0)));
732    }
733
734    #[test]
735    fn decode_mixed_endian_collection() {
736        // LE outer GeometryCollection containing one BE Point and one LE Point.
737        let mut buf = Vec::new();
738        buf.push(1u8);
739        buf.extend_from_slice(&7u32.to_le_bytes()); // collection
740        buf.extend_from_slice(&2u32.to_le_bytes()); // 2 children
741                                                    // BE point (1.0, 2.0)
742        buf.push(0u8);
743        buf.extend_from_slice(&1u32.to_be_bytes());
744        buf.extend_from_slice(&1.0f64.to_be_bytes());
745        buf.extend_from_slice(&2.0f64.to_be_bytes());
746        // LE point (3.0, 4.0)
747        buf.push(1u8);
748        buf.extend_from_slice(&1u32.to_le_bytes());
749        buf.extend_from_slice(&3.0f64.to_le_bytes());
750        buf.extend_from_slice(&4.0f64.to_le_bytes());
751
752        let g = Geometry::from_wkb(&buf).unwrap();
753        match g {
754            Geometry::GeometryCollection(v) => {
755                assert_eq!(v.len(), 2);
756                assert_eq!(v[0], Geometry::Point(Coord::xy(1.0, 2.0)));
757                assert_eq!(v[1], Geometry::Point(Coord::xy(3.0, 4.0)));
758            }
759            _ => panic!("expected GeometryCollection"),
760        }
761    }
762
763    #[test]
764    fn truncated_input_errors_cleanly() {
765        let truncated = &[1u8, 1, 0, 0, 0]; // type code only, no x/y
766        assert!(Geometry::from_wkb(truncated).is_err());
767    }
768
769    #[test]
770    fn z_variant_errors_as_unsupported() {
771        // PointZ via ISO high bit
772        let mut buf = vec![1u8];
773        buf.extend_from_slice(&(1u32 | ISO_Z_FLAG).to_le_bytes());
774        buf.extend_from_slice(&1.0f64.to_le_bytes());
775        buf.extend_from_slice(&2.0f64.to_le_bytes());
776        buf.extend_from_slice(&3.0f64.to_le_bytes()); // z
777        let err = Geometry::from_wkb(&buf).unwrap_err();
778        assert!(matches!(err, Error::Unsupported(_)));
779
780        // PointZ via 1001 extended code
781        let mut buf = vec![1u8];
782        buf.extend_from_slice(&1001u32.to_le_bytes());
783        let err = Geometry::from_wkb(&buf).unwrap_err();
784        assert!(matches!(err, Error::Unsupported(_)));
785    }
786
787    #[test]
788    fn bad_byte_order_byte_errors() {
789        let buf = [9u8, 1, 0, 0, 0]; // bo=9 (invalid)
790        assert!(Geometry::from_wkb(&buf).is_err());
791    }
792}
793
794#[cfg(test)]
795mod bbox_walker_tests {
796    use super::*;
797
798    fn bbox_via_walker(g: &Geometry) -> Option<[f64; 4]> {
799        bbox_from_bytes(&g.to_wkb())
800    }
801
802    #[test]
803    fn bbox_walker_matches_geometry_bbox_for_all_variants() {
804        let cases: Vec<Geometry> = vec![
805            Geometry::Point(Coord::xy(5.0, 7.0)),
806            Geometry::LineString(LineString::new(vec![
807                Coord::xy(0.0, 0.0),
808                Coord::xy(10.0, -5.0),
809                Coord::xy(2.0, 8.0),
810            ])),
811            Geometry::Polygon(Polygon::new(
812                LineString::new(vec![
813                    Coord::xy(0.0, 0.0),
814                    Coord::xy(10.0, 0.0),
815                    Coord::xy(10.0, 10.0),
816                    Coord::xy(0.0, 10.0),
817                    Coord::xy(0.0, 0.0),
818                ]),
819                vec![],
820            )),
821            Geometry::MultiPoint(vec![Coord::xy(-1.0, -1.0), Coord::xy(1.0, 1.0)]),
822            Geometry::MultiLineString(vec![
823                LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(5.0, 5.0)]),
824                LineString::new(vec![Coord::xy(100.0, -100.0), Coord::xy(101.0, -99.0)]),
825            ]),
826            Geometry::GeometryCollection(vec![
827                Geometry::Point(Coord::xy(-50.0, -50.0)),
828                Geometry::Point(Coord::xy(50.0, 50.0)),
829            ]),
830        ];
831        for g in cases {
832            assert_eq!(
833                bbox_via_walker(&g),
834                g.bbox(),
835                "walker disagrees with Geometry::bbox for {g:?}"
836            );
837        }
838    }
839
840    #[test]
841    fn bbox_walker_returns_none_for_empty_geoms() {
842        for t in [
843            GeometryType::Point,
844            GeometryType::LineString,
845            GeometryType::Polygon,
846            GeometryType::MultiPolygon,
847            GeometryType::GeometryCollection,
848        ] {
849            assert!(bbox_via_walker(&Geometry::Empty(t)).is_none(), "{t:?}");
850        }
851    }
852
853    #[test]
854    fn bbox_walker_returns_none_for_malformed_input() {
855        assert!(bbox_from_bytes(&[1u8, 1, 0, 0, 0]).is_none()); // truncated point
856        assert!(bbox_from_bytes(&[]).is_none()); // empty
857        assert!(bbox_from_bytes(&[1, 99, 99, 99, 99]).is_none()); // bad type code
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use super::*;
864
865    #[test]
866    fn point_wkb_byte_exact() {
867        let g = Geometry::Point(Coord::xy(1.0, 2.0));
868        let wkb = g.to_wkb();
869        // 1 byte order + 4 bytes type + 8 bytes x + 8 bytes y = 21
870        assert_eq!(wkb.len(), 21);
871        assert_eq!(wkb[0], 1); // LE
872        assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 1); // Point
873        assert_eq!(f64::from_le_bytes(wkb[5..13].try_into().unwrap()), 1.0);
874        assert_eq!(f64::from_le_bytes(wkb[13..21].try_into().unwrap()), 2.0);
875    }
876
877    #[test]
878    fn point_empty_uses_nan() {
879        let wkb = Geometry::Empty(GeometryType::Point).to_wkb();
880        assert_eq!(wkb.len(), 21);
881        assert!(f64::from_le_bytes(wkb[5..13].try_into().unwrap()).is_nan());
882        assert!(f64::from_le_bytes(wkb[13..21].try_into().unwrap()).is_nan());
883    }
884
885    #[test]
886    fn linestring_wkb_layout() {
887        let ls = LineString::new(vec![
888            Coord::xy(0.0, 0.0),
889            Coord::xy(1.0, 1.0),
890            Coord::xy(2.0, 0.0),
891        ]);
892        let wkb = Geometry::LineString(ls).to_wkb();
893        // 5 (preamble) + 4 (npts) + 3*16 = 57
894        assert_eq!(wkb.len(), 57);
895        assert_eq!(wkb[0], 1);
896        assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 2);
897        assert_eq!(u32::from_le_bytes(wkb[5..9].try_into().unwrap()), 3);
898        // first coord
899        assert_eq!(f64::from_le_bytes(wkb[9..17].try_into().unwrap()), 0.0);
900        assert_eq!(f64::from_le_bytes(wkb[17..25].try_into().unwrap()), 0.0);
901        // third coord
902        assert_eq!(f64::from_le_bytes(wkb[41..49].try_into().unwrap()), 2.0);
903        assert_eq!(f64::from_le_bytes(wkb[49..57].try_into().unwrap()), 0.0);
904    }
905
906    #[test]
907    fn polygon_with_hole() {
908        let exterior = LineString::new(vec![
909            Coord::xy(0.0, 0.0),
910            Coord::xy(10.0, 0.0),
911            Coord::xy(10.0, 10.0),
912            Coord::xy(0.0, 10.0),
913            Coord::xy(0.0, 0.0),
914        ]);
915        let hole = LineString::new(vec![
916            Coord::xy(2.0, 2.0),
917            Coord::xy(4.0, 2.0),
918            Coord::xy(4.0, 4.0),
919            Coord::xy(2.0, 4.0),
920            Coord::xy(2.0, 2.0),
921        ]);
922        let p = Polygon::new(exterior, vec![hole]);
923        let wkb = Geometry::Polygon(p).to_wkb();
924        // 5 (preamble) + 4 (n_rings = 2) + 4 (ring0 npts) + 5*16 + 4 (ring1 npts) + 5*16
925        assert_eq!(wkb.len(), 5 + 4 + 4 + 80 + 4 + 80);
926        assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 3);
927        assert_eq!(u32::from_le_bytes(wkb[5..9].try_into().unwrap()), 2); // 2 rings
928        assert_eq!(u32::from_le_bytes(wkb[9..13].try_into().unwrap()), 5); // ring0: 5 pts
929                                                                           // ring1 count follows ring0's 5 coords (80 bytes), at offset 13 + 80 = 93
930        assert_eq!(u32::from_le_bytes(wkb[93..97].try_into().unwrap()), 5);
931    }
932
933    #[test]
934    fn multipoint_each_point_carries_own_header() {
935        let g = Geometry::MultiPoint(vec![Coord::xy(0.0, 0.0), Coord::xy(1.0, 1.0)]);
936        let wkb = g.to_wkb();
937        // 5 (preamble) + 4 (count) + 2 * 21 (each point full WKB)
938        assert_eq!(wkb.len(), 5 + 4 + 42);
939        assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 4);
940        assert_eq!(u32::from_le_bytes(wkb[5..9].try_into().unwrap()), 2);
941        // first inner point header
942        assert_eq!(wkb[9], 1);
943        assert_eq!(u32::from_le_bytes(wkb[10..14].try_into().unwrap()), 1);
944    }
945
946    #[test]
947    fn multilinestring_wkb_layout() {
948        let g = Geometry::MultiLineString(vec![
949            LineString::new(vec![Coord::xy(0.0, 0.0), Coord::xy(1.0, 1.0)]),
950            LineString::new(vec![
951                Coord::xy(2.0, 2.0),
952                Coord::xy(3.0, 3.0),
953                Coord::xy(4.0, 4.0),
954            ]),
955        ]);
956        let wkb = g.to_wkb();
957        assert_eq!(wkb[0], 1);
958        assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 5);
959        assert_eq!(u32::from_le_bytes(wkb[5..9].try_into().unwrap()), 2);
960        // First inner LineString starts at 9: byte_order + type + n_pts + coords
961        assert_eq!(wkb[9], 1);
962        assert_eq!(u32::from_le_bytes(wkb[10..14].try_into().unwrap()), 2);
963        assert_eq!(u32::from_le_bytes(wkb[14..18].try_into().unwrap()), 2); // 2 pts
964    }
965
966    #[test]
967    fn empty_linestring_wkb() {
968        let wkb = Geometry::Empty(GeometryType::LineString).to_wkb();
969        assert_eq!(wkb.len(), 5 + 4);
970        assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 2);
971        assert_eq!(u32::from_le_bytes(wkb[5..9].try_into().unwrap()), 0);
972    }
973
974    #[test]
975    fn empty_polygon_and_multipolygon() {
976        for (t, code) in [
977            (GeometryType::Polygon, 3),
978            (GeometryType::MultiPolygon, 6),
979            (GeometryType::MultiLineString, 5),
980            (GeometryType::MultiPoint, 4),
981            (GeometryType::GeometryCollection, 7),
982        ] {
983            let wkb = Geometry::Empty(t).to_wkb();
984            assert_eq!(wkb.len(), 5 + 4, "empty {t:?}");
985            assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), code);
986            assert_eq!(u32::from_le_bytes(wkb[5..9].try_into().unwrap()), 0);
987        }
988    }
989
990    #[test]
991    fn geometry_collection_nests_correctly() {
992        let g = Geometry::GeometryCollection(vec![
993            Geometry::Point(Coord::xy(1.0, 2.0)),
994            Geometry::LineString(LineString::new(vec![
995                Coord::xy(0.0, 0.0),
996                Coord::xy(1.0, 1.0),
997            ])),
998        ]);
999        let wkb = g.to_wkb();
1000        assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 7);
1001        assert_eq!(u32::from_le_bytes(wkb[5..9].try_into().unwrap()), 2);
1002        // First inner geom (point) at offset 9
1003        assert_eq!(wkb[9], 1);
1004        assert_eq!(u32::from_le_bytes(wkb[10..14].try_into().unwrap()), 1); // point
1005                                                                            // Second inner geom (linestring) starts at 9 + 21 = 30
1006        assert_eq!(wkb[30], 1);
1007        assert_eq!(u32::from_le_bytes(wkb[31..35].try_into().unwrap()), 2); // linestring
1008    }
1009
1010    #[test]
1011    fn write_wkb_into_clears_buffer() {
1012        let mut scratch = vec![0xFFu8; 100];
1013        Geometry::Point(Coord::xy(5.0, 6.0)).write_wkb_into(&mut scratch);
1014        assert_eq!(scratch.len(), 21);
1015        assert_eq!(scratch[0], 1);
1016    }
1017}