Skip to main content

nodedb_spatial/
wkb.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Well-Known Binary (WKB) serialization for Geometry types.
4//!
5//! WKB is the standard binary format for geometry interchange (ISO 13249).
6//! Used as the Arrow `DataType::Binary` backing for spatial columns —
7//! avoids JSON parse overhead during DataFusion query execution.
8//!
9//! Format (little-endian):
10//! ```text
11//! [byte_order: u8] [type: u32] [coordinates...]
12//! ```
13//!
14//! Byte order: 1 = little-endian (NDR), 0 = big-endian (XDR). We always
15//! write little-endian and accept both on read.
16
17use nodedb_types::geometry::Geometry;
18
19// WKB geometry type codes (ISO 13249 / OGC SFA).
20const WKB_POINT: u32 = 1;
21const WKB_LINESTRING: u32 = 2;
22const WKB_POLYGON: u32 = 3;
23const WKB_MULTIPOINT: u32 = 4;
24const WKB_MULTILINESTRING: u32 = 5;
25const WKB_MULTIPOLYGON: u32 = 6;
26const WKB_GEOMETRYCOLLECTION: u32 = 7;
27
28const BYTE_ORDER_LE: u8 = 1;
29
30/// Serialize a Geometry to WKB (little-endian).
31pub fn geometry_to_wkb(geom: &Geometry) -> Vec<u8> {
32    // no-governor: fixed-tiny WKB output buffer (64-byte hint); geometry serialization cold path
33    let mut buf = Vec::with_capacity(64);
34    write_geometry(&mut buf, geom);
35    buf
36}
37
38/// Deserialize a Geometry from WKB bytes.
39///
40/// Returns `None` if the bytes are malformed or truncated.
41pub fn geometry_from_wkb(data: &[u8]) -> Option<Geometry> {
42    let mut cursor = 0;
43    read_geometry(data, &mut cursor)
44}
45
46/// Extract bounding box from WKB without full deserialization.
47///
48/// Scans coordinate values to find min/max. Faster than full deserialize
49/// when only the bbox is needed (e.g., R-tree insertion from Arrow batch).
50pub fn wkb_bbox(data: &[u8]) -> Option<nodedb_types::BoundingBox> {
51    let geom = geometry_from_wkb(data)?;
52    Some(nodedb_types::geometry_bbox(&geom))
53}
54
55// ── Write helpers ──
56
57fn write_geometry(buf: &mut Vec<u8>, geom: &Geometry) {
58    match geom {
59        Geometry::Point { coordinates } => {
60            write_header(buf, WKB_POINT);
61            write_f64(buf, coordinates[0]);
62            write_f64(buf, coordinates[1]);
63        }
64        Geometry::LineString { coordinates } => {
65            write_header(buf, WKB_LINESTRING);
66            write_u32(buf, coordinates.len() as u32);
67            for c in coordinates {
68                write_f64(buf, c[0]);
69                write_f64(buf, c[1]);
70            }
71        }
72        Geometry::Polygon { coordinates } => {
73            write_header(buf, WKB_POLYGON);
74            write_u32(buf, coordinates.len() as u32);
75            for ring in coordinates {
76                write_u32(buf, ring.len() as u32);
77                for c in ring {
78                    write_f64(buf, c[0]);
79                    write_f64(buf, c[1]);
80                }
81            }
82        }
83        Geometry::MultiPoint { coordinates } => {
84            write_header(buf, WKB_MULTIPOINT);
85            write_u32(buf, coordinates.len() as u32);
86            for c in coordinates {
87                // Each point is a full WKB Point.
88                write_header(buf, WKB_POINT);
89                write_f64(buf, c[0]);
90                write_f64(buf, c[1]);
91            }
92        }
93        Geometry::MultiLineString { coordinates } => {
94            write_header(buf, WKB_MULTILINESTRING);
95            write_u32(buf, coordinates.len() as u32);
96            for ls in coordinates {
97                write_geometry(
98                    buf,
99                    &Geometry::LineString {
100                        coordinates: ls.clone(),
101                    },
102                );
103            }
104        }
105        Geometry::MultiPolygon { coordinates } => {
106            write_header(buf, WKB_MULTIPOLYGON);
107            write_u32(buf, coordinates.len() as u32);
108            for poly in coordinates {
109                write_geometry(
110                    buf,
111                    &Geometry::Polygon {
112                        coordinates: poly.clone(),
113                    },
114                );
115            }
116        }
117        Geometry::GeometryCollection { geometries } => {
118            write_header(buf, WKB_GEOMETRYCOLLECTION);
119            write_u32(buf, geometries.len() as u32);
120            for g in geometries {
121                write_geometry(buf, g);
122            }
123        }
124
125        // Unknown future geometry type — write empty geometry collection.
126        _ => {
127            write_header(buf, WKB_GEOMETRYCOLLECTION);
128            write_u32(buf, 0);
129        }
130    }
131}
132
133fn write_header(buf: &mut Vec<u8>, wkb_type: u32) {
134    buf.push(BYTE_ORDER_LE);
135    write_u32(buf, wkb_type);
136}
137
138fn write_u32(buf: &mut Vec<u8>, val: u32) {
139    buf.extend_from_slice(&val.to_le_bytes());
140}
141
142fn write_f64(buf: &mut Vec<u8>, val: f64) {
143    buf.extend_from_slice(&val.to_le_bytes());
144}
145
146// ── Read helpers ──
147
148fn read_geometry(data: &[u8], cursor: &mut usize) -> Option<Geometry> {
149    let byte_order = read_u8(data, cursor)?;
150    let is_le = byte_order == 1;
151    let wkb_type = read_u32(data, cursor, is_le)?;
152
153    match wkb_type {
154        WKB_POINT => {
155            let x = read_f64(data, cursor, is_le)?;
156            let y = read_f64(data, cursor, is_le)?;
157            Some(Geometry::Point {
158                coordinates: [x, y],
159            })
160        }
161        WKB_LINESTRING => {
162            let n = read_u32(data, cursor, is_le)? as usize;
163            let coords = read_coords(data, cursor, n, is_le)?;
164            Some(Geometry::LineString {
165                coordinates: coords,
166            })
167        }
168        WKB_POLYGON => {
169            let num_rings = read_u32(data, cursor, is_le)? as usize;
170            // no-governor: WKB decode; rings per polygon bounded by input geometry, parse path
171            let mut rings = Vec::with_capacity(num_rings);
172            for _ in 0..num_rings {
173                let n = read_u32(data, cursor, is_le)? as usize;
174                let ring = read_coords(data, cursor, n, is_le)?;
175                rings.push(ring);
176            }
177            Some(Geometry::Polygon { coordinates: rings })
178        }
179        WKB_MULTIPOINT => {
180            let count = read_u32(data, cursor, is_le)? as usize;
181            // no-governor: WKB decode; multipoint count bounded by input, parse path
182            let mut coords = Vec::with_capacity(count);
183            for _ in 0..count {
184                let inner = read_geometry(data, cursor)?;
185                if let Geometry::Point { coordinates } = inner {
186                    coords.push(coordinates);
187                } else {
188                    return None;
189                }
190            }
191            Some(Geometry::MultiPoint {
192                coordinates: coords,
193            })
194        }
195        WKB_MULTILINESTRING => {
196            let count = read_u32(data, cursor, is_le)? as usize;
197            // no-governor: WKB decode; linestring count bounded by input, parse path
198            let mut lines = Vec::with_capacity(count);
199            for _ in 0..count {
200                let inner = read_geometry(data, cursor)?;
201                if let Geometry::LineString { coordinates } = inner {
202                    lines.push(coordinates);
203                } else {
204                    return None;
205                }
206            }
207            Some(Geometry::MultiLineString { coordinates: lines })
208        }
209        WKB_MULTIPOLYGON => {
210            let count = read_u32(data, cursor, is_le)? as usize;
211            // no-governor: WKB decode; polygon count bounded by input, parse path
212            let mut polys = Vec::with_capacity(count);
213            for _ in 0..count {
214                let inner = read_geometry(data, cursor)?;
215                if let Geometry::Polygon { coordinates } = inner {
216                    polys.push(coordinates);
217                } else {
218                    return None;
219                }
220            }
221            Some(Geometry::MultiPolygon { coordinates: polys })
222        }
223        WKB_GEOMETRYCOLLECTION => {
224            let count = read_u32(data, cursor, is_le)? as usize;
225            // no-governor: WKB decode; geometry count bounded by input, parse path
226            let mut geoms = Vec::with_capacity(count);
227            for _ in 0..count {
228                geoms.push(read_geometry(data, cursor)?);
229            }
230            Some(Geometry::GeometryCollection { geometries: geoms })
231        }
232        _ => None,
233    }
234}
235
236fn read_u8(data: &[u8], cursor: &mut usize) -> Option<u8> {
237    if *cursor >= data.len() {
238        return None;
239    }
240    let val = data[*cursor];
241    *cursor += 1;
242    Some(val)
243}
244
245fn read_u32(data: &[u8], cursor: &mut usize, is_le: bool) -> Option<u32> {
246    if *cursor + 4 > data.len() {
247        return None;
248    }
249    let bytes: [u8; 4] = [
250        data[*cursor],
251        data[*cursor + 1],
252        data[*cursor + 2],
253        data[*cursor + 3],
254    ];
255    *cursor += 4;
256    Some(if is_le {
257        u32::from_le_bytes(bytes)
258    } else {
259        u32::from_be_bytes(bytes)
260    })
261}
262
263fn read_f64(data: &[u8], cursor: &mut usize, is_le: bool) -> Option<f64> {
264    if *cursor + 8 > data.len() {
265        return None;
266    }
267    let bytes: [u8; 8] = [
268        data[*cursor],
269        data[*cursor + 1],
270        data[*cursor + 2],
271        data[*cursor + 3],
272        data[*cursor + 4],
273        data[*cursor + 5],
274        data[*cursor + 6],
275        data[*cursor + 7],
276    ];
277    *cursor += 8;
278    Some(if is_le {
279        f64::from_le_bytes(bytes)
280    } else {
281        f64::from_be_bytes(bytes)
282    })
283}
284
285fn read_coords(
286    data: &[u8],
287    cursor: &mut usize,
288    count: usize,
289    is_le: bool,
290) -> Option<Vec<[f64; 2]>> {
291    // no-governor: WKB decode coords; count from WKB header, parse path
292    let mut coords = Vec::with_capacity(count);
293    for _ in 0..count {
294        let x = read_f64(data, cursor, is_le)?;
295        let y = read_f64(data, cursor, is_le)?;
296        coords.push([x, y]);
297    }
298    Some(coords)
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn point_roundtrip() {
307        let geom = Geometry::point(-73.9857, 40.7484);
308        let wkb = geometry_to_wkb(&geom);
309        let decoded = geometry_from_wkb(&wkb).unwrap();
310        assert_eq!(geom, decoded);
311    }
312
313    #[test]
314    fn linestring_roundtrip() {
315        let geom = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]]);
316        let wkb = geometry_to_wkb(&geom);
317        let decoded = geometry_from_wkb(&wkb).unwrap();
318        assert_eq!(geom, decoded);
319    }
320
321    #[test]
322    fn polygon_roundtrip() {
323        let geom = Geometry::polygon(vec![
324            vec![
325                [0.0, 0.0],
326                [10.0, 0.0],
327                [10.0, 10.0],
328                [0.0, 10.0],
329                [0.0, 0.0],
330            ],
331            vec![[2.0, 2.0], [3.0, 2.0], [3.0, 3.0], [2.0, 3.0], [2.0, 2.0]], // hole
332        ]);
333        let wkb = geometry_to_wkb(&geom);
334        let decoded = geometry_from_wkb(&wkb).unwrap();
335        assert_eq!(geom, decoded);
336    }
337
338    #[test]
339    fn multipoint_roundtrip() {
340        let geom = Geometry::MultiPoint {
341            coordinates: vec![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]],
342        };
343        let wkb = geometry_to_wkb(&geom);
344        let decoded = geometry_from_wkb(&wkb).unwrap();
345        assert_eq!(geom, decoded);
346    }
347
348    #[test]
349    fn multilinestring_roundtrip() {
350        let geom = Geometry::MultiLineString {
351            coordinates: vec![
352                vec![[0.0, 0.0], [1.0, 1.0]],
353                vec![[2.0, 2.0], [3.0, 3.0], [4.0, 2.0]],
354            ],
355        };
356        let wkb = geometry_to_wkb(&geom);
357        let decoded = geometry_from_wkb(&wkb).unwrap();
358        assert_eq!(geom, decoded);
359    }
360
361    #[test]
362    fn multipolygon_roundtrip() {
363        let geom = Geometry::MultiPolygon {
364            coordinates: vec![
365                vec![vec![
366                    [0.0, 0.0],
367                    [1.0, 0.0],
368                    [1.0, 1.0],
369                    [0.0, 1.0],
370                    [0.0, 0.0],
371                ]],
372                vec![vec![
373                    [5.0, 5.0],
374                    [6.0, 5.0],
375                    [6.0, 6.0],
376                    [5.0, 6.0],
377                    [5.0, 5.0],
378                ]],
379            ],
380        };
381        let wkb = geometry_to_wkb(&geom);
382        let decoded = geometry_from_wkb(&wkb).unwrap();
383        assert_eq!(geom, decoded);
384    }
385
386    #[test]
387    fn geometry_collection_roundtrip() {
388        let geom = Geometry::GeometryCollection {
389            geometries: vec![
390                Geometry::point(1.0, 2.0),
391                Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]),
392            ],
393        };
394        let wkb = geometry_to_wkb(&geom);
395        let decoded = geometry_from_wkb(&wkb).unwrap();
396        assert_eq!(geom, decoded);
397    }
398
399    #[test]
400    fn truncated_data_returns_none() {
401        let wkb = geometry_to_wkb(&Geometry::point(1.0, 2.0));
402        assert!(geometry_from_wkb(&wkb[..3]).is_none());
403        assert!(geometry_from_wkb(&[]).is_none());
404    }
405
406    #[test]
407    fn invalid_type_returns_none() {
408        let mut wkb = geometry_to_wkb(&Geometry::point(1.0, 2.0));
409        wkb[1] = 99; // invalid WKB type
410        assert!(geometry_from_wkb(&wkb).is_none());
411    }
412
413    #[test]
414    fn wkb_bbox_extraction() {
415        let geom = Geometry::polygon(vec![vec![
416            [-10.0, -5.0],
417            [10.0, -5.0],
418            [10.0, 5.0],
419            [-10.0, 5.0],
420            [-10.0, -5.0],
421        ]]);
422        let wkb = geometry_to_wkb(&geom);
423        let bb = wkb_bbox(&wkb).unwrap();
424        assert_eq!(bb.min_lng, -10.0);
425        assert_eq!(bb.max_lng, 10.0);
426        assert_eq!(bb.min_lat, -5.0);
427        assert_eq!(bb.max_lat, 5.0);
428    }
429
430    #[test]
431    fn point_wkb_size() {
432        let wkb = geometry_to_wkb(&Geometry::point(0.0, 0.0));
433        // 1 (byte order) + 4 (type) + 8 (x) + 8 (y) = 21 bytes
434        assert_eq!(wkb.len(), 21);
435    }
436}