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// WKB count fields are attacker-controlled. The bound caps allocation and
31// parsing work even when the encoded input itself is very large.
32const MAX_WKB_ELEMENTS: usize = 1_000_000;
33const MAX_WKB_BYTES: usize = 64 * 1024 * 1024;
34// Nested geometry collections are recursive on the stack. Keep the budget
35// explicit so hostile nesting cannot exhaust the decoder thread's stack.
36const MAX_WKB_DEPTH: usize = 64;
37
38/// Serialize a Geometry to WKB (little-endian).
39///
40/// Returns `None` when the geometry exceeds the decoder's element or nesting
41/// limits, ensuring every emitted WKB value remains decodable.
42pub fn geometry_to_wkb(geom: &Geometry) -> Option<Vec<u8>> {
43    let encoded_size = checked_encoded_size(geom, MAX_WKB_DEPTH)?;
44    let mut buf = Vec::with_capacity(encoded_size);
45    write_geometry(&mut buf, geom, MAX_WKB_DEPTH)?;
46    debug_assert_eq!(buf.len(), encoded_size);
47    Some(buf)
48}
49
50/// Deserialize a Geometry from WKB bytes.
51///
52/// Returns `None` if the bytes are malformed or truncated.
53pub fn geometry_from_wkb(data: &[u8]) -> Option<Geometry> {
54    if data.len() > MAX_WKB_BYTES {
55        return None;
56    }
57    let mut cursor = 0;
58    read_geometry(data, &mut cursor, MAX_WKB_DEPTH)
59}
60
61/// Extract bounding box from WKB without full deserialization.
62///
63/// Scans coordinate values to find min/max. Faster than full deserialize
64/// when only the bbox is needed (e.g., R-tree insertion from Arrow batch).
65pub fn wkb_bbox(data: &[u8]) -> Option<nodedb_types::BoundingBox> {
66    let geom = geometry_from_wkb(data)?;
67    Some(nodedb_types::geometry_bbox(&geom))
68}
69
70// ── Write helpers ──
71
72fn checked_encoded_size(geom: &Geometry, depth_remaining: usize) -> Option<usize> {
73    if depth_remaining == 0 {
74        return None;
75    }
76    let size = match geom {
77        Geometry::Point { .. } => 21,
78        Geometry::LineString { coordinates } => {
79            checked_count(coordinates.len())?;
80            bounded_add(9, coordinates.len().checked_mul(16)?)?
81        }
82        Geometry::Polygon { coordinates } => {
83            checked_count(coordinates.len())?;
84            let mut size = 9usize;
85            for ring in coordinates {
86                checked_count(ring.len())?;
87                size = bounded_add(size, bounded_add(4, ring.len().checked_mul(16)?)?)?;
88            }
89            size
90        }
91        Geometry::MultiPoint { coordinates } => {
92            checked_count(coordinates.len())?;
93            bounded_add(9, coordinates.len().checked_mul(21)?)?
94        }
95        Geometry::MultiLineString { coordinates } => {
96            if depth_remaining == 1 {
97                return None;
98            }
99            checked_count(coordinates.len())?;
100            let mut size = 9usize;
101            for line in coordinates {
102                checked_count(line.len())?;
103                size = bounded_add(size, bounded_add(9, line.len().checked_mul(16)?)?)?;
104            }
105            size
106        }
107        Geometry::MultiPolygon { coordinates } => {
108            if depth_remaining == 1 {
109                return None;
110            }
111            checked_count(coordinates.len())?;
112            let mut size = 9usize;
113            for polygon in coordinates {
114                checked_count(polygon.len())?;
115                let mut polygon_size = 9usize;
116                for ring in polygon {
117                    checked_count(ring.len())?;
118                    polygon_size =
119                        bounded_add(polygon_size, bounded_add(4, ring.len().checked_mul(16)?)?)?;
120                }
121                size = bounded_add(size, polygon_size)?;
122            }
123            size
124        }
125        Geometry::GeometryCollection { geometries } => {
126            checked_count(geometries.len())?;
127            let mut size = 9usize;
128            for geometry in geometries {
129                size = bounded_add(size, checked_encoded_size(geometry, depth_remaining - 1)?)?;
130            }
131            size
132        }
133        _ => 9,
134    };
135    (size <= MAX_WKB_BYTES).then_some(size)
136}
137
138fn checked_count(count: usize) -> Option<()> {
139    (count <= MAX_WKB_ELEMENTS && u32::try_from(count).is_ok()).then_some(())
140}
141
142fn bounded_add(left: usize, right: usize) -> Option<usize> {
143    left.checked_add(right)
144        .filter(|&size| size <= MAX_WKB_BYTES)
145}
146
147fn write_geometry(buf: &mut Vec<u8>, geom: &Geometry, depth_remaining: usize) -> Option<()> {
148    if depth_remaining == 0 {
149        return None;
150    }
151    match geom {
152        Geometry::Point { coordinates } => {
153            write_header(buf, WKB_POINT);
154            write_f64(buf, coordinates[0]);
155            write_f64(buf, coordinates[1]);
156        }
157        Geometry::LineString { coordinates } => {
158            write_header(buf, WKB_LINESTRING);
159            write_count(buf, coordinates.len())?;
160            for c in coordinates {
161                write_f64(buf, c[0]);
162                write_f64(buf, c[1]);
163            }
164        }
165        Geometry::Polygon { coordinates } => {
166            write_header(buf, WKB_POLYGON);
167            write_count(buf, coordinates.len())?;
168            for ring in coordinates {
169                write_count(buf, ring.len())?;
170                for c in ring {
171                    write_f64(buf, c[0]);
172                    write_f64(buf, c[1]);
173                }
174            }
175        }
176        Geometry::MultiPoint { coordinates } => {
177            write_header(buf, WKB_MULTIPOINT);
178            write_count(buf, coordinates.len())?;
179            for c in coordinates {
180                write_header(buf, WKB_POINT);
181                write_f64(buf, c[0]);
182                write_f64(buf, c[1]);
183            }
184        }
185        Geometry::MultiLineString { coordinates } => {
186            write_header(buf, WKB_MULTILINESTRING);
187            write_count(buf, coordinates.len())?;
188            for ls in coordinates {
189                write_geometry(
190                    buf,
191                    &Geometry::LineString {
192                        coordinates: ls.clone(),
193                    },
194                    depth_remaining - 1,
195                )?;
196            }
197        }
198        Geometry::MultiPolygon { coordinates } => {
199            write_header(buf, WKB_MULTIPOLYGON);
200            write_count(buf, coordinates.len())?;
201            for poly in coordinates {
202                write_geometry(
203                    buf,
204                    &Geometry::Polygon {
205                        coordinates: poly.clone(),
206                    },
207                    depth_remaining - 1,
208                )?;
209            }
210        }
211        Geometry::GeometryCollection { geometries } => {
212            write_header(buf, WKB_GEOMETRYCOLLECTION);
213            write_count(buf, geometries.len())?;
214            for geometry in geometries {
215                write_geometry(buf, geometry, depth_remaining - 1)?;
216            }
217        }
218        _ => {
219            write_header(buf, WKB_GEOMETRYCOLLECTION);
220            write_u32(buf, 0);
221        }
222    }
223    Some(())
224}
225
226fn write_count(buf: &mut Vec<u8>, count: usize) -> Option<()> {
227    if count > MAX_WKB_ELEMENTS {
228        return None;
229    }
230    write_u32(buf, u32::try_from(count).ok()?);
231    Some(())
232}
233
234fn write_header(buf: &mut Vec<u8>, wkb_type: u32) {
235    buf.push(BYTE_ORDER_LE);
236    write_u32(buf, wkb_type);
237}
238
239fn write_u32(buf: &mut Vec<u8>, val: u32) {
240    buf.extend_from_slice(&val.to_le_bytes());
241}
242
243fn write_f64(buf: &mut Vec<u8>, val: f64) {
244    buf.extend_from_slice(&val.to_le_bytes());
245}
246
247// ── Read helpers ──
248
249fn read_geometry(data: &[u8], cursor: &mut usize, depth_remaining: usize) -> Option<Geometry> {
250    if depth_remaining == 0 {
251        return None;
252    }
253    let byte_order = read_u8(data, cursor)?;
254    let is_le = byte_order == 1;
255    let wkb_type = read_u32(data, cursor, is_le)?;
256
257    match wkb_type {
258        WKB_POINT => {
259            let x = read_f64(data, cursor, is_le)?;
260            let y = read_f64(data, cursor, is_le)?;
261            Some(Geometry::Point {
262                coordinates: [x, y],
263            })
264        }
265        WKB_LINESTRING => {
266            let count = read_u32(data, cursor, is_le)? as usize;
267            let coords = read_coords(data, cursor, count, is_le)?;
268            Some(Geometry::LineString {
269                coordinates: coords,
270            })
271        }
272        WKB_POLYGON => {
273            let count = read_u32(data, cursor, is_le)? as usize;
274            let capacity = checked_wkb_capacity(data, cursor, count, 4)?;
275            let mut rings = Vec::with_capacity(capacity);
276            for _ in 0..capacity {
277                let ring_count = read_u32(data, cursor, is_le)? as usize;
278                let ring = read_coords(data, cursor, ring_count, is_le)?;
279                rings.push(ring);
280            }
281            Some(Geometry::Polygon { coordinates: rings })
282        }
283        WKB_MULTIPOINT => {
284            let count = read_u32(data, cursor, is_le)? as usize;
285            let capacity = checked_wkb_capacity(data, cursor, count, 21)?;
286            let mut coords = Vec::with_capacity(capacity);
287            for _ in 0..capacity {
288                let inner = read_geometry(data, cursor, depth_remaining - 1)?;
289                if let Geometry::Point { coordinates } = inner {
290                    coords.push(coordinates);
291                } else {
292                    return None;
293                }
294            }
295            Some(Geometry::MultiPoint {
296                coordinates: coords,
297            })
298        }
299        WKB_MULTILINESTRING => {
300            let count = read_u32(data, cursor, is_le)? as usize;
301            let capacity = checked_wkb_capacity(data, cursor, count, 5)?;
302            let mut lines = Vec::with_capacity(capacity);
303            for _ in 0..capacity {
304                let inner = read_geometry(data, cursor, depth_remaining - 1)?;
305                if let Geometry::LineString { coordinates } = inner {
306                    lines.push(coordinates);
307                } else {
308                    return None;
309                }
310            }
311            Some(Geometry::MultiLineString { coordinates: lines })
312        }
313        WKB_MULTIPOLYGON => {
314            let count = read_u32(data, cursor, is_le)? as usize;
315            let capacity = checked_wkb_capacity(data, cursor, count, 5)?;
316            let mut polys = Vec::with_capacity(capacity);
317            for _ in 0..capacity {
318                let inner = read_geometry(data, cursor, depth_remaining - 1)?;
319                if let Geometry::Polygon { coordinates } = inner {
320                    polys.push(coordinates);
321                } else {
322                    return None;
323                }
324            }
325            Some(Geometry::MultiPolygon { coordinates: polys })
326        }
327        WKB_GEOMETRYCOLLECTION => {
328            let count = read_u32(data, cursor, is_le)? as usize;
329            let capacity = checked_wkb_capacity(data, cursor, count, 5)?;
330            let mut geoms = Vec::with_capacity(capacity);
331            for _ in 0..capacity {
332                geoms.push(read_geometry(data, cursor, depth_remaining - 1)?);
333            }
334            Some(Geometry::GeometryCollection { geometries: geoms })
335        }
336        _ => None,
337    }
338}
339
340/// Validate an untrusted count before using it as a vector capacity.
341///
342/// Every encoded element must occupy at least `min_encoded_bytes` after the
343/// current cursor. This makes a truncated count fail before allocation, while
344/// the independent element cap bounds work on large valid inputs.
345fn checked_wkb_capacity(
346    data: &[u8],
347    cursor: &usize,
348    count: usize,
349    min_encoded_bytes: usize,
350) -> Option<usize> {
351    if count > MAX_WKB_ELEMENTS {
352        return None;
353    }
354    let required = count.checked_mul(min_encoded_bytes)?;
355    let remaining = data.len().checked_sub(*cursor)?;
356    (required <= remaining).then_some(count)
357}
358
359fn read_u8(data: &[u8], cursor: &mut usize) -> Option<u8> {
360    if *cursor >= data.len() {
361        return None;
362    }
363    let val = data[*cursor];
364    *cursor += 1;
365    Some(val)
366}
367
368fn read_u32(data: &[u8], cursor: &mut usize, is_le: bool) -> Option<u32> {
369    if *cursor + 4 > data.len() {
370        return None;
371    }
372    let bytes: [u8; 4] = [
373        data[*cursor],
374        data[*cursor + 1],
375        data[*cursor + 2],
376        data[*cursor + 3],
377    ];
378    *cursor += 4;
379    Some(if is_le {
380        u32::from_le_bytes(bytes)
381    } else {
382        u32::from_be_bytes(bytes)
383    })
384}
385
386fn read_f64(data: &[u8], cursor: &mut usize, is_le: bool) -> Option<f64> {
387    if *cursor + 8 > data.len() {
388        return None;
389    }
390    let bytes: [u8; 8] = [
391        data[*cursor],
392        data[*cursor + 1],
393        data[*cursor + 2],
394        data[*cursor + 3],
395        data[*cursor + 4],
396        data[*cursor + 5],
397        data[*cursor + 6],
398        data[*cursor + 7],
399    ];
400    *cursor += 8;
401    Some(if is_le {
402        f64::from_le_bytes(bytes)
403    } else {
404        f64::from_be_bytes(bytes)
405    })
406}
407
408fn read_coords(
409    data: &[u8],
410    cursor: &mut usize,
411    count: usize,
412    is_le: bool,
413) -> Option<Vec<[f64; 2]>> {
414    let capacity = checked_wkb_capacity(data, cursor, count, 16)?;
415    let mut coords = Vec::with_capacity(capacity);
416    for _ in 0..capacity {
417        let x = read_f64(data, cursor, is_le)?;
418        let y = read_f64(data, cursor, is_le)?;
419        coords.push([x, y]);
420    }
421    Some(coords)
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    fn geometry_to_wkb(geometry: &Geometry) -> Vec<u8> {
429        super::geometry_to_wkb(geometry).expect("test geometry must fit WKB limits")
430    }
431
432    #[test]
433    fn point_roundtrip() {
434        let geom = Geometry::point(-73.9857, 40.7484);
435        let wkb = geometry_to_wkb(&geom);
436        let decoded = geometry_from_wkb(&wkb).unwrap();
437        assert_eq!(geom, decoded);
438    }
439
440    #[test]
441    fn linestring_roundtrip() {
442        let geom = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]]);
443        let wkb = geometry_to_wkb(&geom);
444        let decoded = geometry_from_wkb(&wkb).unwrap();
445        assert_eq!(geom, decoded);
446    }
447
448    #[test]
449    fn polygon_roundtrip() {
450        let geom = Geometry::polygon(vec![
451            vec![
452                [0.0, 0.0],
453                [10.0, 0.0],
454                [10.0, 10.0],
455                [0.0, 10.0],
456                [0.0, 0.0],
457            ],
458            vec![[2.0, 2.0], [3.0, 2.0], [3.0, 3.0], [2.0, 3.0], [2.0, 2.0]], // hole
459        ]);
460        let wkb = geometry_to_wkb(&geom);
461        let decoded = geometry_from_wkb(&wkb).unwrap();
462        assert_eq!(geom, decoded);
463    }
464
465    #[test]
466    fn multipoint_roundtrip() {
467        let geom = Geometry::MultiPoint {
468            coordinates: vec![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]],
469        };
470        let wkb = geometry_to_wkb(&geom);
471        let decoded = geometry_from_wkb(&wkb).unwrap();
472        assert_eq!(geom, decoded);
473    }
474
475    #[test]
476    fn multilinestring_roundtrip() {
477        let geom = Geometry::MultiLineString {
478            coordinates: vec![
479                vec![[0.0, 0.0], [1.0, 1.0]],
480                vec![[2.0, 2.0], [3.0, 3.0], [4.0, 2.0]],
481            ],
482        };
483        let wkb = geometry_to_wkb(&geom);
484        let decoded = geometry_from_wkb(&wkb).unwrap();
485        assert_eq!(geom, decoded);
486    }
487
488    #[test]
489    fn multipolygon_roundtrip() {
490        let geom = Geometry::MultiPolygon {
491            coordinates: vec![
492                vec![vec![
493                    [0.0, 0.0],
494                    [1.0, 0.0],
495                    [1.0, 1.0],
496                    [0.0, 1.0],
497                    [0.0, 0.0],
498                ]],
499                vec![vec![
500                    [5.0, 5.0],
501                    [6.0, 5.0],
502                    [6.0, 6.0],
503                    [5.0, 6.0],
504                    [5.0, 5.0],
505                ]],
506            ],
507        };
508        let wkb = geometry_to_wkb(&geom);
509        let decoded = geometry_from_wkb(&wkb).unwrap();
510        assert_eq!(geom, decoded);
511    }
512
513    #[test]
514    fn geometry_collection_roundtrip() {
515        let geom = Geometry::GeometryCollection {
516            geometries: vec![
517                Geometry::point(1.0, 2.0),
518                Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]),
519            ],
520        };
521        let wkb = geometry_to_wkb(&geom);
522        let decoded = geometry_from_wkb(&wkb).unwrap();
523        assert_eq!(geom, decoded);
524    }
525
526    #[test]
527    fn parsed_max_counts_are_rejected_before_allocation() {
528        for geometry_type in [
529            WKB_LINESTRING,
530            WKB_POLYGON,
531            WKB_MULTIPOINT,
532            WKB_MULTILINESTRING,
533            WKB_MULTIPOLYGON,
534            WKB_GEOMETRYCOLLECTION,
535        ] {
536            let mut wkb = Vec::new();
537            write_header(&mut wkb, geometry_type);
538            write_u32(&mut wkb, u32::MAX);
539            assert!(geometry_from_wkb(&wkb).is_none(), "type={geometry_type}");
540        }
541    }
542
543    #[test]
544    fn encoder_rejects_total_output_above_byte_limit_before_writing() {
545        let geometry = Geometry::MultiLineString {
546            coordinates: (0..5).map(|_| vec![[0.0, 0.0]; 840_000]).collect(),
547        };
548        assert!(checked_encoded_size(&geometry, MAX_WKB_DEPTH).is_none());
549        assert!(super::geometry_to_wkb(&geometry).is_none());
550    }
551
552    #[test]
553    fn encoder_rejects_element_count_above_decoder_limit() {
554        let geometry = Geometry::LineString {
555            coordinates: vec![[0.0, 0.0]; MAX_WKB_ELEMENTS + 1],
556        };
557        assert!(super::geometry_to_wkb(&geometry).is_none());
558    }
559
560    #[test]
561    fn truncated_count_bodies_are_rejected_before_allocation() {
562        for geometry_type in [
563            WKB_LINESTRING,
564            WKB_POLYGON,
565            WKB_MULTIPOINT,
566            WKB_MULTILINESTRING,
567            WKB_MULTIPOLYGON,
568            WKB_GEOMETRYCOLLECTION,
569        ] {
570            let mut wkb = Vec::new();
571            write_header(&mut wkb, geometry_type);
572            write_u32(&mut wkb, 1);
573            assert!(geometry_from_wkb(&wkb).is_none(), "type={geometry_type}");
574        }
575    }
576
577    #[test]
578    fn geometry_collection_depth_is_bounded() {
579        let mut valid = Geometry::point(1.0, 2.0);
580        for _ in 0..MAX_WKB_DEPTH - 1 {
581            valid = Geometry::GeometryCollection {
582                geometries: vec![valid],
583            };
584        }
585        let valid_wkb = geometry_to_wkb(&valid);
586        assert_eq!(geometry_from_wkb(&valid_wkb), Some(valid));
587
588        let mut too_deep = Geometry::point(1.0, 2.0);
589        for _ in 0..MAX_WKB_DEPTH {
590            too_deep = Geometry::GeometryCollection {
591                geometries: vec![too_deep],
592            };
593        }
594        assert!(super::geometry_to_wkb(&too_deep).is_none());
595    }
596
597    #[test]
598    fn truncated_data_returns_none() {
599        let wkb = geometry_to_wkb(&Geometry::point(1.0, 2.0));
600        assert!(geometry_from_wkb(&wkb[..3]).is_none());
601        assert!(geometry_from_wkb(&[]).is_none());
602    }
603
604    #[test]
605    fn invalid_type_returns_none() {
606        let mut wkb = geometry_to_wkb(&Geometry::point(1.0, 2.0));
607        wkb[1] = 99; // invalid WKB type
608        assert!(geometry_from_wkb(&wkb).is_none());
609    }
610
611    #[test]
612    fn wkb_bbox_extraction() {
613        let geom = Geometry::polygon(vec![vec![
614            [-10.0, -5.0],
615            [10.0, -5.0],
616            [10.0, 5.0],
617            [-10.0, 5.0],
618            [-10.0, -5.0],
619        ]]);
620        let wkb = geometry_to_wkb(&geom);
621        let bb = wkb_bbox(&wkb).unwrap();
622        assert_eq!(bb.min_lng, -10.0);
623        assert_eq!(bb.max_lng, 10.0);
624        assert_eq!(bb.min_lat, -5.0);
625        assert_eq!(bb.max_lat, 5.0);
626    }
627
628    #[test]
629    fn point_wkb_size() {
630        let wkb = geometry_to_wkb(&Geometry::point(0.0, 0.0));
631        // 1 (byte order) + 4 (type) + 8 (x) + 8 (y) = 21 bytes
632        assert_eq!(wkb.len(), 21);
633    }
634}