Skip to main content

geopackage_core/
geometry.rs

1//! Lazy geometry wrapper over a GeoPackage Binary (GPB) blob.
2//!
3//! A [`GpbGeometry`] pairs a parsed GPB header (see [`crate::gpb`]) with the
4//! ISO WKB body that follows it. The georust [`wkb`] crate reads the body:
5//! [`GpbGeometry`] implements [`geo_traits::GeometryTrait`] by delegating to
6//! [`wkb::reader::Wkb`], so you can traverse coordinates without materialising
7//! an owned geometry. When the `geo-types` feature is enabled, you can convert
8//! to `geo-types` with [`GpbGeometry::to_geo`].
9//!
10//! This wrapper and the envelope traversal below live in `geopackage-core`
11//! rather than the container crate, which keeps the fuzz workspace free of the
12//! SQLite dependency. The intended long-term home is an upstreamed `gpb`
13//! feature in georust `wkb` itself.
14//!
15//! Parsing arbitrary bytes never panics. A malformed header, a truncated body,
16//! or a geometry type the `wkb` crate cannot read all yield a
17//! [`GeometryError`].
18
19use crate::gpb::{self, GpbHeader};
20use crate::types::{GeometryType, GeometryTypeSet};
21
22use geo_traits::{
23    CoordTrait, Dimensions, GeometryTrait, LineStringTrait, LineTrait, MultiLineStringTrait,
24    MultiPointTrait, MultiPolygonTrait, PointTrait, PolygonTrait, RectTrait, TriangleTrait,
25};
26use geo_traits::{GeometryCollectionTrait, GeometryType as GtGeometryType};
27use wkb::Endianness;
28use wkb::reader::{
29    GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Wkb,
30};
31use wkb::writer::{WriteOptions, geometry_wkb_size, write_geometry};
32
33/// Errors from constructing or reading a [`GpbGeometry`].
34#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum GeometryError {
37    /// The GPB header could not be parsed.
38    #[error(transparent)]
39    Header(#[from] gpb::GpbError),
40    /// The ISO WKB body could not be read. This covers structurally malformed
41    /// bodies and the non-linear curve types (`CIRCULARSTRING`,
42    /// `CURVEPOLYGON`, …) that the `wkb` crate does not yet support.
43    ///
44    /// Code that reads a body through the `wkb` reader, such as
45    /// [`GpbGeometry`], returns this variant. [`encode_gpb_from_wkb`] reads
46    /// bodies with [`crate::curve`] instead, and reports structural problems
47    /// with that scanner's variants.
48    #[error("WKB body is unreadable (unsupported curve type or malformed geometry)")]
49    Body(#[from] wkb::error::WkbError),
50    /// A body of a core type that contains a non-linear member, such as a
51    /// `GEOMETRYCOLLECTION` containing a `CIRCULARSTRING`.
52    ///
53    /// The byte scanner can read such a body. Reading it back as a geometry
54    /// goes through the `wkb` reader, which is chosen by the body's own type
55    /// code and cannot read the member. Writing one would produce a blob that
56    /// this crate's own read path cannot return, so the write path rejects it.
57    #[error(
58        "a {container} containing a {member} cannot be written: a non-linear geometry is writable only as a body's own type"
59    )]
60    NonLinearMember {
61        /// The body's own type.
62        container: GeometryType,
63        /// The first non-linear type found inside it.
64        member: GeometryType,
65    },
66    /// The WKB body was too short to contain the one-byte order marker and
67    /// four-byte geometry type code.
68    #[error("WKB body truncated: need at least 5 bytes for the geometry type code")]
69    TruncatedWkb,
70    /// The WKB geometry type code is not one of the GeoPackage geometry types
71    /// (Annex G).
72    #[error("unknown WKB geometry type code {0}")]
73    UnknownWkbType(u32),
74    /// The WKB body declares one of the abstract supertypes (`GEOMETRY`,
75    /// `CURVE`, `SURFACE`), which cannot be instantiated and so cannot appear
76    /// as a body.
77    #[error("WKB geometry type code {0} is an abstract supertype and has no encoding")]
78    AbstractWkbType(u32),
79    /// The WKB body ended before a count, coordinate, or nested geometry its
80    /// structure requires.
81    #[error("WKB body truncated at byte {offset}")]
82    TruncatedAt {
83        /// Byte offset into the body at which the read ran out of bytes.
84        offset: usize,
85    },
86    /// A WKB geometry opened with a byte-order marker that is neither 0 (big
87    /// endian) nor 1 (little endian).
88    #[error("invalid WKB byte order marker at byte {offset}")]
89    InvalidByteOrder {
90        /// Byte offset into the body of the offending marker.
91        offset: usize,
92    },
93    /// Nested geometry containers exceeded the walk's depth limit.
94    #[error("WKB geometry nesting is too deep")]
95    NestingTooDeep,
96    /// The georust `wkb` writer could not encode the geometry, for example
97    /// because it has a coordinate dimension the writer cannot serialise.
98    #[error("failed to encode geometry to WKB")]
99    EncodeWkb(#[source] wkb::error::WkbError),
100}
101
102/// A GeoPackage geometry: a parsed GPB header plus its ISO WKB body.
103///
104/// Construct one with [`GpbGeometry::parse`]. The wrapper borrows the source
105/// blob. The underlying `wkb` reader makes a single validating pass on
106/// construction, so coordinate access is constant-time, but not zero-copy.
107#[derive(Debug, Clone)]
108pub struct GpbGeometry<'a> {
109    header: GpbHeader,
110    body: &'a [u8],
111    wkb: Wkb<'a>,
112}
113
114impl<'a> GpbGeometry<'a> {
115    /// Parses a complete GPB blob: the header, then the ISO WKB body.
116    ///
117    /// Never panics on arbitrary input.
118    ///
119    /// # Errors
120    ///
121    /// [`GeometryError`] for a malformed header, a truncated or malformed
122    /// body, or a geometry type the `wkb` crate cannot read.
123    #[hotpath::measure(label = "GpbGeometry::parse")]
124    pub fn parse(blob: &'a [u8]) -> Result<Self, GeometryError> {
125        let (header, offset) = gpb::parse_header(blob)?;
126        // `parse_header` guarantees `offset <= blob.len()`; `get` keeps this
127        // panic-free, and an (impossible) out-of-range offset yields an empty
128        // body that `Wkb::try_new` rejects as a typed error rather than a panic.
129        let body = blob.get(offset..).unwrap_or_default();
130        let wkb = Wkb::try_new(body)?;
131        Ok(Self { header, body, wkb })
132    }
133
134    /// Returns the parsed GPB header.
135    pub fn header(&self) -> &GpbHeader {
136        &self.header
137    }
138
139    /// Returns the raw ISO WKB body slice (everything after the GPB header).
140    ///
141    /// The slice may include trailing bytes beyond the geometry. Use
142    /// [`GpbGeometry::wkb`] and [`wkb::reader::Wkb::buf`] for the exact
143    /// geometry extent.
144    pub fn wkb_body(&self) -> &'a [u8] {
145        self.body
146    }
147
148    /// Returns the parsed `wkb` reader for the body.
149    pub fn wkb(&self) -> &Wkb<'a> {
150        &self.wkb
151    }
152
153    /// Converts to an owned [`geo_types::Geometry`].
154    ///
155    /// Returns `None` for a geometry `geo-types` cannot represent (an empty
156    /// point). Only the X and Y dimensions are kept; any Z or M values in the
157    /// body are dropped.
158    #[cfg(feature = "geo-types")]
159    pub fn to_geo(&self) -> Option<geo_types::Geometry<f64>> {
160        use geo_traits::to_geo::ToGeoGeometry;
161        self.wkb.try_to_geometry()
162    }
163
164    /// Returns the XY bounding box `[min_x, max_x, min_y, max_y]` computed by
165    /// walking the WKB body, or `None` when the geometry has no finite
166    /// coordinate (an empty geometry).
167    ///
168    /// The `ST_*` SQL functions use [`blob_xy_envelope`] instead, which also
169    /// reads the curve types. The walk here goes through the `geo-traits`
170    /// interface, so it handles every geometry type the `wkb` crate can read,
171    /// in either byte order and with any Z/M dimensions. Only X and Y bound
172    /// the box; Z and M are ignored. Non-finite coordinates, such as the NaN
173    /// empty-point convention or malformed values, do not contribute to the
174    /// box.
175    pub fn xy_envelope(&self) -> Option<[f64; 4]> {
176        let mut bounds = XyBounds::new();
177        visit_coords(&self.wkb, &mut |x, y, _| bounds.add(x, y));
178        bounds.finish()
179    }
180
181    /// Returns `true` if this geometry is empty.
182    ///
183    /// A geometry is empty when the header's empty flag is set, or when the
184    /// body has no finite coordinate. That covers an empty point (the all-NaN
185    /// convention), an empty linestring/polygon/multi-geometry, and an empty
186    /// geometry collection.
187    pub fn is_empty(&self) -> bool {
188        self.header.empty || self.xy_envelope().is_none()
189    }
190
191    /// Returns the geometry type of the WKB body, as a [`GeometryType`].
192    ///
193    /// This reflects what the `wkb` crate parsed, so it is one of the seven
194    /// linear types, or [`GeometryType::Geometry`] for a type a later `wkb`
195    /// release learns to read. To classify a body whose type the `wkb` crate
196    /// cannot read (a curve type), read the raw discriminator with
197    /// [`wkb_geometry_type`] instead.
198    pub fn geometry_type(&self) -> GeometryType {
199        use wkb::reader::GeometryType as WkbType;
200        match self.wkb.geometry_type() {
201            WkbType::Point => GeometryType::Point,
202            WkbType::LineString => GeometryType::LineString,
203            WkbType::Polygon => GeometryType::Polygon,
204            WkbType::MultiPoint => GeometryType::MultiPoint,
205            WkbType::MultiLineString => GeometryType::MultiLineString,
206            WkbType::MultiPolygon => GeometryType::MultiPolygon,
207            WkbType::GeometryCollection => GeometryType::GeometryCollection,
208            // `wkb::reader::GeometryType` is `#[non_exhaustive]`; a future
209            // variant would be a type `wkb` newly learned to read. Report it
210            // as the `GEOMETRY` supertype until this mapping is extended.
211            _ => GeometryType::Geometry,
212        }
213    }
214
215    /// Returns `true` if this geometry's type satisfies a column `declared`
216    /// as a given [`GeometryType`], per [`geometry_type_matches`].
217    pub fn matches_declared(&self, declared: GeometryType) -> bool {
218        geometry_type_matches(self.geometry_type(), declared)
219    }
220}
221
222/// Returns `true` if the WKB geometry type `actual` satisfies a column
223/// declared as `declared`, per the GeoPackage instantiable-type rules.
224///
225/// The rules are deliberately narrow, with no general walk of the subtype
226/// lattice. A value satisfies its column when one of the following holds:
227///
228/// - its type matches exactly;
229/// - the column is declared `GEOMETRY`, the root supertype, which accepts any
230///   geometry;
231/// - the column is declared `GEOMETRYCOLLECTION`, which accepts only the
232///   collection types (`GEOMETRYCOLLECTION`, `MULTIPOINT`, `MULTILINESTRING`,
233///   `MULTIPOLYGON`, `MULTICURVE`, `MULTISURFACE`).
234///
235/// A `LINESTRING` does **not** satisfy a `MULTILINESTRING` column, because a
236/// multi-geometry is a collection of its parts, not their supertype.
237pub fn geometry_type_matches(actual: GeometryType, declared: GeometryType) -> bool {
238    use GeometryType::*;
239    if declared == Geometry || declared == actual {
240        return true;
241    }
242    if declared == GeometryCollection {
243        return matches!(
244            actual,
245            GeometryCollection
246                | MultiPoint
247                | MultiLineString
248                | MultiPolygon
249                | MultiCurve
250                | MultiSurface
251        );
252    }
253    false
254}
255
256/// Traverses a GPB blob's body for its XY bounds, non-linear types included.
257///
258/// [`crate::curve`] walks every body. It reads all the Annex G types directly
259/// from the WKB bytes and is the faster reader over coordinate runs. As a
260/// result, a core-typed container holding a non-linear member still yields its
261/// bounds here, so you can index a file that another tool wrote that way. This
262/// crate's writer refuses such a container, and the `wkb`-reader read path
263/// cannot return it.
264fn body_xy_bounds(blob: &[u8]) -> Result<Option<[f64; 4]>, GeometryError> {
265    let (_, offset) = gpb::parse_header(blob)?;
266    // `parse_header` guarantees `offset <= blob.len()`.
267    let body = blob.get(offset..).unwrap_or_default();
268    crate::curve::xy_envelope(body)
269}
270
271/// Returns the XY bounds `[min_x, max_x, min_y, max_y]` of a GPB blob, or
272/// `None` when the geometry is empty.
273///
274/// Reads the header envelope in constant time when one is present, and
275/// traverses the body otherwise. `ST_MinX` and its three siblings use this.
276///
277/// # Errors
278///
279/// [`GeometryError`] if the header or the body cannot be read.
280pub fn blob_xy_envelope(blob: &[u8]) -> Result<Option<[f64; 4]>, GeometryError> {
281    let (header, _) = gpb::parse_header(blob)?;
282    if let Some((min_x, max_x, min_y, max_y)) = header.envelope.xy_bounds() {
283        return Ok(Some([min_x, max_x, min_y, max_y]));
284    }
285    body_xy_bounds(blob)
286}
287
288/// Returns `true` if a GPB blob's geometry is empty.
289///
290/// Checks the header's empty flag first, in constant time, and traverses the
291/// body only when the flag is not set. A geometry with no finite coordinate is
292/// empty whatever the flag says. `ST_IsEmpty` uses this.
293///
294/// # Errors
295///
296/// [`GeometryError`] if the header or the body cannot be read.
297pub fn blob_is_empty(blob: &[u8]) -> Result<bool, GeometryError> {
298    let (header, _) = gpb::parse_header(blob)?;
299    if header.empty {
300        return Ok(true);
301    }
302    Ok(body_xy_bounds(blob)?.is_none())
303}
304
305/// Returns the XY bounds of a GPB blob and whether it is empty, from a single
306/// traversal.
307///
308/// The bulk index build needs both values, and they must agree with what the
309/// `ST_IsEmpty`-guarded triggers would have indexed. Calling
310/// [`blob_is_empty`] and [`blob_xy_envelope`] separately would traverse the
311/// body twice. This function decides emptiness from the traversal, and uses
312/// the header envelope for the bounds when there is one.
313///
314/// # Errors
315///
316/// [`GeometryError`] if the header or the body cannot be read.
317#[hotpath::measure(label = "geometry::blob_envelope_and_empty")]
318pub fn blob_envelope_and_empty(blob: &[u8]) -> Result<(Option<[f64; 4]>, bool), GeometryError> {
319    let (header, _) = gpb::parse_header(blob)?;
320    if header.empty {
321        return Ok((None, true));
322    }
323    let Some(body_bounds) = body_xy_bounds(blob)? else {
324        return Ok((None, true));
325    };
326    let bounds = match header.envelope.xy_bounds() {
327        Some((min_x, max_x, min_y, max_y)) => [min_x, max_x, min_y, max_y],
328        None => body_bounds,
329    };
330    Ok((Some(bounds), false))
331}
332
333/// Reads the geometry type discriminator from the start of an ISO WKB body,
334/// without reading coordinates.
335///
336/// Unlike [`GpbGeometry::geometry_type`], this works on curve types the `wkb`
337/// crate cannot fully read. A declared-type validator needs this, because it
338/// must detect a `CURVEPOLYGON` body in a column declared `POLYGON`.
339/// GeoPackage bodies are ISO WKB. This function also handles an extended-WKB
340/// (EWKB) type flag defensively, but a GPB blob is not expected to have one.
341#[hotpath::measure(label = "geometry::wkb_geometry_type")]
342pub fn wkb_geometry_type(wkb_body: &[u8]) -> Result<GeometryType, GeometryError> {
343    // Byte-order marker plus the four-byte type code; a shorter body is
344    // truncated. `..` ignores any coordinate bytes that follow.
345    let &[order, c0, c1, c2, c3, ..] = wkb_body else {
346        return Err(GeometryError::TruncatedWkb);
347    };
348    let little_endian = match order {
349        0 => false,
350        1 => true,
351        _ => return Err(GeometryError::TruncatedWkb),
352    };
353    let bytes = [c0, c1, c2, c3];
354    let code = if little_endian {
355        u32::from_le_bytes(bytes)
356    } else {
357        u32::from_be_bytes(bytes)
358    };
359    // ISO WKB encodes the dimension as a +1000/+2000/+3000 offset on the base
360    // type; EWKB instead sets high bit flags and keeps the base type low.
361    let base = if code & 0xE000_0000 != 0 {
362        code & 0x0000_00FF
363    } else {
364        code % 1000
365    };
366    GeometryType::from_wkb_base(base).ok_or(GeometryError::UnknownWkbType(code))
367}
368
369/// Returns the GPB envelope to write for a geometry, and whether the geometry
370/// is empty.
371///
372/// This crate's writer always emits an envelope, so readers and the `ST_*`
373/// functions get the bounds without traversing the WKB body.
374///
375/// The envelope is [`Envelope::Xyz`] when the geometry has a Z dimension, even
376/// for a single point, and [`Envelope::Xy`] otherwise. An M dimension never
377/// widens the envelope, because readers and the rtree use only X and Y bounds,
378/// plus Z for 3D. A geometry with no finite coordinate is *empty*. For one,
379/// the function returns [`Envelope::None`] and `true`, so the encoder omits
380/// the envelope and sets the header empty flag.
381///
382/// [`Envelope::Xy`]: crate::gpb::Envelope::Xy
383/// [`Envelope::Xyz`]: crate::gpb::Envelope::Xyz
384/// [`Envelope::None`]: crate::gpb::Envelope::None
385pub fn write_envelope<G: GeometryTrait<T = f64>>(geom: &G) -> (gpb::Envelope, bool) {
386    let mut bounds = XyzBounds::new();
387    visit_coords(geom, &mut |x, y, z| bounds.add(x, y, z));
388    let Some([min_x, max_x, min_y, max_y]) = bounds.xy_bounds() else {
389        return (gpb::Envelope::None, true);
390    };
391    match bounds.z_bounds() {
392        Some((min_z, max_z)) => (
393            gpb::Envelope::Xyz([min_x, max_x, min_y, max_y, min_z, max_z]),
394            false,
395        ),
396        None => (gpb::Envelope::Xy([min_x, max_x, min_y, max_y]), false),
397    }
398}
399
400/// Encodes a GPB blob from a body that is already ISO WKB, without
401/// re-serialising it.
402///
403/// [`encode_gpb`] serialises a geometry object through the `wkb` writer. If
404/// you already have ISO WKB bytes, as a GeoArrow column does, you don't need
405/// that step. The GPB body *is* ISO WKB, so this function copies the bytes
406/// after the header. It is the write-side counterpart of reading a geometry
407/// column as WKB by skipping the header.
408///
409/// The function still parses the bytes, for two reasons. First, it must
410/// compute the envelope for the header, which always includes one, and for
411/// the spatial index, which needs a coordinate traversal either way. Second,
412/// parsing rejects a body that is not ISO WKB, such as PostGIS EWKB. Without
413/// the parse, such a body would be copied verbatim into a file that claims to
414/// be conformant.
415///
416/// Only the geometry's own extent is copied, not any trailing bytes beyond
417/// it.
418///
419/// Returns the blob, its XY envelope as [`encode_gpb`] does, and the body's
420/// dimensions, so you can check them against the column's `z`/`m`
421/// constraints without a second parse.
422///
423/// [`crate::curve::scan`] parses every body and reads all the Annex G types.
424/// For a non-linear body, the header sets the extended flag, per Annex F.1
425/// Requirement 68. Writing one still needs the matching `gpkg_geom_<TYPE>` row
426/// in `gpkg_extensions`. You must register it yourself, because this function
427/// sees a body, not a table.
428///
429/// # Errors
430///
431/// [`GeometryError`] if `wkb_body` is not a body [`crate::curve::scan`]
432/// accepts, or is a core-typed container holding a non-linear member
433/// ([`GeometryError::NonLinearMember`]).
434#[hotpath::measure(label = "geometry::encode_gpb_from_wkb")]
435pub fn encode_gpb_from_wkb(wkb_body: &[u8], srs_id: i32) -> Result<EncodedGpb, GeometryError> {
436    let own_type = wkb_geometry_type(wkb_body)?;
437    let scan = crate::curve::scan(wkb_body)?;
438    let extended = own_type.is_extension();
439    // The scanner reads a core-typed container with non-linear members, but
440    // the `wkb`-reader read path cannot, so writing one is refused rather
441    // than producing a blob this crate cannot read back.
442    if !extended && let Some(member) = scan.extension_types.iter().next() {
443        return Err(GeometryError::NonLinearMember {
444            container: own_type,
445            member,
446        });
447    }
448    let body = wkb_body.get(..scan.len).unwrap_or(wkb_body);
449    // Sized for header and body together, so the blob is one allocation per
450    // row rather than an exactly-sized header that the body then grows.
451    let mut blob = Vec::with_capacity(gpb::header_len(&scan.envelope) + body.len());
452    gpb::encode_header_into(&mut blob, srs_id, &scan.envelope, scan.empty, extended);
453    blob.extend_from_slice(body);
454    Ok(EncodedGpb {
455        blob,
456        xy_envelope: scan.xy_envelope,
457        dimensions: scan.dimensions,
458        extension_types: scan.extension_types,
459    })
460}
461
462/// The result of [`encode_gpb_from_wkb`].
463#[derive(Debug, Clone, PartialEq)]
464pub struct EncodedGpb {
465    /// The complete GPB blob: header followed by the ISO WKB body.
466    pub blob: Vec<u8>,
467    /// The geometry's XY envelope, or `None` when it is empty.
468    pub xy_envelope: Option<[f64; 4]>,
469    /// The dimensions the WKB body declares, for checking against the column.
470    pub dimensions: Dimensions,
471    /// The non-linear types the body contains, at every nesting depth. Each
472    /// one needs a `gpkg_geom_<TYPE>` row for the column it is written to.
473    ///
474    /// Empty for a body containing only core types. A container's members are
475    /// not implied by its own type, so extension registration must use this
476    /// set rather than the column's declared type.
477    pub extension_types: GeometryTypeSet,
478}
479
480/// Encodes a geometry as a complete GeoPackage Binary (GPB) blob.
481///
482/// The blob is an always-little-endian header ([`gpb::encode_header`]) with an
483/// envelope per [`write_envelope`], followed by the little-endian ISO WKB body
484/// that the georust `wkb` crate writes.
485///
486/// `srs_id` is the geometry column's spatial reference system, and is written
487/// into the header. Returns the blob and its XY envelope
488/// `[min_x, max_x, min_y, max_y]`, or `None` for an empty geometry, so you can
489/// fold the envelope into a running bounding box without traversing the
490/// geometry again.
491///
492/// # Errors
493///
494/// [`GeometryError::EncodeWkb`] if the `wkb` writer cannot serialise the
495/// geometry.
496#[hotpath::measure(label = "geometry::encode_gpb")]
497pub fn encode_gpb<G: GeometryTrait<T = f64>>(
498    geom: &G,
499    srs_id: i32,
500) -> Result<(Vec<u8>, Option<[f64; 4]>), GeometryError> {
501    let (envelope, empty) = write_envelope(geom);
502    let xy = envelope
503        .xy_bounds()
504        .map(|(min_x, max_x, min_y, max_y)| [min_x, max_x, min_y, max_y]);
505    // As `encode_gpb_from_wkb`: the body's encoded length is known before it is
506    // written, so header and body share one allocation.
507    let mut blob = Vec::with_capacity(gpb::header_len(&envelope) + geometry_wkb_size(geom));
508    gpb::encode_header_into(&mut blob, srs_id, &envelope, empty, false);
509    let options = WriteOptions {
510        endianness: Endianness::LittleEndian,
511    };
512    write_geometry(&mut blob, geom, &options).map_err(GeometryError::EncodeWkb)?;
513    Ok((blob, xy))
514}
515
516/// Accumulator for an XY bounding box over visited coordinates.
517///
518/// Shared with [`crate::curve`], which walks WKB bytes directly rather than
519/// through the `wkb` reader but must apply the same non-finite policy.
520#[derive(Debug, Clone, Copy)]
521pub(crate) struct XyBounds {
522    min_x: f64,
523    max_x: f64,
524    min_y: f64,
525    max_y: f64,
526    seen: bool,
527}
528
529impl XyBounds {
530    pub(crate) fn new() -> Self {
531        Self {
532            min_x: f64::INFINITY,
533            max_x: f64::NEG_INFINITY,
534            min_y: f64::INFINITY,
535            max_y: f64::NEG_INFINITY,
536            seen: false,
537        }
538    }
539
540    pub(crate) fn add(&mut self, x: f64, y: f64) {
541        if !x.is_finite() || !y.is_finite() {
542            return;
543        }
544        self.min_x = self.min_x.min(x);
545        self.max_x = self.max_x.max(x);
546        self.min_y = self.min_y.min(y);
547        self.max_y = self.max_y.max(y);
548        self.seen = true;
549    }
550
551    /// [`Self::add`] without the skip branch.
552    ///
553    /// A non-finite pair is mapped to NaN. `min` and `max` ignore NaN by
554    /// definition, returning the other operand, so the pair contributes
555    /// nothing, exactly as with the early return.
556    ///
557    /// Straight-line code lets the compiler vectorise a fold over a long
558    /// coordinate run. When the run is too short to vectorise, it costs a
559    /// little more per coordinate than the branch. The scanner therefore
560    /// chooses per sequence, and everything else uses [`Self::add`].
561    pub(crate) fn add_branchless(&mut self, x: f64, y: f64) {
562        let finite = x.is_finite() & y.is_finite();
563        let x = if finite { x } else { f64::NAN };
564        let y = if finite { y } else { f64::NAN };
565        self.min_x = self.min_x.min(x);
566        self.max_x = self.max_x.max(x);
567        self.min_y = self.min_y.min(y);
568        self.max_y = self.max_y.max(y);
569        self.seen |= finite;
570    }
571
572    pub(crate) fn finish(self) -> Option<[f64; 4]> {
573        self.seen
574            .then_some([self.min_x, self.max_x, self.min_y, self.max_y])
575    }
576}
577
578/// Accumulator for an XY bounding box plus, when present, a Z bounds range.
579/// [`write_envelope`] uses it to build the envelope it always writes.
580///
581/// Shared with [`crate::curve`], which fills it by walking WKB bytes rather
582/// than through the `wkb` reader.
583#[derive(Debug, Clone, Copy)]
584pub(crate) struct XyzBounds {
585    xy: XyBounds,
586    min_z: f64,
587    max_z: f64,
588    seen_z: bool,
589}
590
591impl XyzBounds {
592    pub(crate) fn new() -> Self {
593        Self {
594            xy: XyBounds::new(),
595            min_z: f64::INFINITY,
596            max_z: f64::NEG_INFINITY,
597            seen_z: false,
598        }
599    }
600
601    pub(crate) fn add(&mut self, x: f64, y: f64, z: Option<f64>) {
602        self.xy.add(x, y);
603        if let Some(z) = z
604            && z.is_finite()
605        {
606            self.min_z = self.min_z.min(z);
607            self.max_z = self.max_z.max(z);
608            self.seen_z = true;
609        }
610    }
611
612    /// [`Self::add`] built on [`XyBounds::add_branchless`], for the scanner's
613    /// long-sequence fold. The `Some` test constant-folds away there, because
614    /// the coordinate layout fixes whether `z` is present.
615    pub(crate) fn add_branchless(&mut self, x: f64, y: f64, z: Option<f64>) {
616        self.xy.add_branchless(x, y);
617        if let Some(z) = z {
618            let finite = z.is_finite();
619            let z = if finite { z } else { f64::NAN };
620            self.min_z = self.min_z.min(z);
621            self.max_z = self.max_z.max(z);
622            self.seen_z |= finite;
623        }
624    }
625
626    /// Returns the XY bounds, or `None` when no finite XY coordinate was seen
627    /// (an empty geometry).
628    pub(crate) fn xy_bounds(&self) -> Option<[f64; 4]> {
629        self.xy.finish()
630    }
631
632    /// Returns the Z range, when any coordinate had a finite Z.
633    pub(crate) fn z_bounds(&self) -> Option<(f64, f64)> {
634        self.seen_z.then_some((self.min_z, self.max_z))
635    }
636}
637
638/// Reads a coordinate's X and Y and, when it has a Z dimension, its Z.
639///
640/// Only [`Dimensions::Xyz`] and [`Dimensions::Xyzm`] place Z at index 2;
641/// [`Dimensions::Xym`] puts M there, so Z reads as `None` for it.
642fn coord_xyz(coord: &impl CoordTrait<T = f64>) -> (f64, f64, Option<f64>) {
643    let z = match coord.dim() {
644        Dimensions::Xyz | Dimensions::Xyzm => coord.nth(2),
645        _ => None,
646    };
647    (coord.x(), coord.y(), z)
648}
649
650fn visit_point(point: &impl PointTrait<T = f64>, visit: &mut impl FnMut(f64, f64, Option<f64>)) {
651    if let Some(coord) = point.coord() {
652        let (x, y, z) = coord_xyz(&coord);
653        visit(x, y, z);
654    }
655}
656
657fn visit_line_string(
658    line_string: &impl LineStringTrait<T = f64>,
659    visit: &mut impl FnMut(f64, f64, Option<f64>),
660) {
661    for coord in line_string.coords() {
662        let (x, y, z) = coord_xyz(&coord);
663        visit(x, y, z);
664    }
665}
666
667fn visit_polygon(
668    polygon: &impl PolygonTrait<T = f64>,
669    visit: &mut impl FnMut(f64, f64, Option<f64>),
670) {
671    if let Some(exterior) = polygon.exterior() {
672        visit_line_string(&exterior, visit);
673    }
674    for interior in polygon.interiors() {
675        visit_line_string(&interior, visit);
676    }
677}
678
679/// Walks a geometry through the `geo-traits` interface, passing every
680/// coordinate's `(x, y, z?)` to `visit`. Recurses into geometry collections.
681/// The read-path envelope accumulates only X and Y and ignores the third
682/// argument. The write-path envelope uses it.
683fn visit_coords<G: GeometryTrait<T = f64>>(
684    geom: &G,
685    visit: &mut impl FnMut(f64, f64, Option<f64>),
686) {
687    match geom.as_type() {
688        GtGeometryType::Point(point) => visit_point(point, visit),
689        GtGeometryType::LineString(line_string) => visit_line_string(line_string, visit),
690        GtGeometryType::Polygon(polygon) => visit_polygon(polygon, visit),
691        GtGeometryType::MultiPoint(multi_point) => {
692            for point in multi_point.points() {
693                visit_point(&point, visit);
694            }
695        }
696        GtGeometryType::MultiLineString(multi_line_string) => {
697            for line_string in multi_line_string.line_strings() {
698                visit_line_string(&line_string, visit);
699            }
700        }
701        GtGeometryType::MultiPolygon(multi_polygon) => {
702            for polygon in multi_polygon.polygons() {
703                visit_polygon(&polygon, visit);
704            }
705        }
706        GtGeometryType::GeometryCollection(collection) => {
707            for member in collection.geometries() {
708                visit_coords(&member, visit);
709            }
710        }
711        // The `wkb` reader never yields these, but the traversal is generic
712        // over any `GeometryTrait`, so they are handled rather than ignored.
713        GtGeometryType::Rect(rect) => {
714            let (min, max) = (rect.min(), rect.max());
715            let (min_x, min_y, min_z) = coord_xyz(&min);
716            let (max_x, max_y, max_z) = coord_xyz(&max);
717            visit(min_x, min_y, min_z);
718            visit(max_x, max_y, max_z);
719        }
720        GtGeometryType::Triangle(triangle) => {
721            for coord in triangle.coords() {
722                let (x, y, z) = coord_xyz(&coord);
723                visit(x, y, z);
724            }
725        }
726        GtGeometryType::Line(line) => {
727            for coord in line.coords() {
728                let (x, y, z) = coord_xyz(&coord);
729                visit(x, y, z);
730            }
731        }
732    }
733}
734
735impl<'a> GeometryTrait for GpbGeometry<'a> {
736    type T = f64;
737    type PointType<'b>
738        = Point<'a>
739    where
740        Self: 'b;
741    type LineStringType<'b>
742        = LineString<'a>
743    where
744        Self: 'b;
745    type PolygonType<'b>
746        = Polygon<'a>
747    where
748        Self: 'b;
749    type MultiPointType<'b>
750        = MultiPoint<'a>
751    where
752        Self: 'b;
753    type MultiLineStringType<'b>
754        = MultiLineString<'a>
755    where
756        Self: 'b;
757    type MultiPolygonType<'b>
758        = MultiPolygon<'a>
759    where
760        Self: 'b;
761    type GeometryCollectionType<'b>
762        = GeometryCollection<'a>
763    where
764        Self: 'b;
765    type RectType<'b>
766        = geo_traits::UnimplementedRect<f64>
767    where
768        Self: 'b;
769    type TriangleType<'b>
770        = geo_traits::UnimplementedTriangle<f64>
771    where
772        Self: 'b;
773    type LineType<'b>
774        = geo_traits::UnimplementedLine<f64>
775    where
776        Self: 'b;
777
778    fn dim(&self) -> Dimensions {
779        self.wkb.dim()
780    }
781
782    fn as_type(
783        &self,
784    ) -> geo_traits::GeometryType<
785        '_,
786        Self::PointType<'_>,
787        Self::LineStringType<'_>,
788        Self::PolygonType<'_>,
789        Self::MultiPointType<'_>,
790        Self::MultiLineStringType<'_>,
791        Self::MultiPolygonType<'_>,
792        Self::GeometryCollectionType<'_>,
793        Self::RectType<'_>,
794        Self::TriangleType<'_>,
795        Self::LineType<'_>,
796    > {
797        self.wkb.as_type()
798    }
799}
800
801#[cfg(test)]
802mod tests {
803    use super::*;
804    use crate::gpb::{Envelope, encode_header};
805    use geo_traits::GeometryType as GtType;
806
807    /// Build a GPB blob from a little-endian WKB body and no envelope.
808    fn gpb(body: &[u8]) -> Vec<u8> {
809        let mut blob = encode_header(4326, &Envelope::None, false, false);
810        blob.extend_from_slice(body);
811        blob
812    }
813
814    /// A little-endian WKB point body.
815    fn wkb_point(x: f64, y: f64) -> Vec<u8> {
816        let mut b = vec![1u8];
817        b.extend_from_slice(&1u32.to_le_bytes());
818        b.extend_from_slice(&x.to_le_bytes());
819        b.extend_from_slice(&y.to_le_bytes());
820        b
821    }
822
823    #[test]
824    fn parses_point_and_exposes_header_and_body() {
825        let body = wkb_point(3.0, 4.0);
826        let blob = gpb(&body);
827        let g = GpbGeometry::parse(&blob).unwrap();
828        assert_eq!(g.header().srs_id, 4326);
829        assert_eq!(g.wkb_body(), body.as_slice());
830    }
831
832    #[test]
833    fn delegates_geometry_trait_to_wkb() {
834        let blob = gpb(&wkb_point(3.0, 4.0));
835        let g = GpbGeometry::parse(&blob).unwrap();
836        match g.as_type() {
837            GtType::Point(p) => {
838                use geo_traits::{CoordTrait, PointTrait};
839                let c = p.coord().unwrap();
840                // Compare bits: the round trip through WKB must be exact.
841                assert_eq!(c.x().to_bits(), 3.0_f64.to_bits());
842                assert_eq!(c.y().to_bits(), 4.0_f64.to_bits());
843            }
844            _ => panic!("expected a point"),
845        }
846    }
847
848    #[test]
849    fn arbitrary_bytes_error_never_panic() {
850        GpbGeometry::parse(b"").unwrap_err();
851        GpbGeometry::parse(b"GP").unwrap_err();
852        // Valid header, empty WKB body.
853        GpbGeometry::parse(&gpb(&[])).unwrap_err();
854        // Valid header, WKB byte-order marker only.
855        GpbGeometry::parse(&gpb(&[1])).unwrap_err();
856        // Valid header, truncated point coordinates.
857        GpbGeometry::parse(&gpb(&wkb_point(1.0, 2.0)[..10])).unwrap_err();
858    }
859
860    /// A little-endian WKB header of type `code` and a member count.
861    fn wkb_counted(code: u32, count: u32) -> Vec<u8> {
862        let mut b = vec![1u8];
863        b.extend_from_slice(&code.to_le_bytes());
864        b.extend_from_slice(&count.to_le_bytes());
865        b
866    }
867
868    #[test]
869    fn huge_declared_counts_and_deep_nesting_error_without_allocating() {
870        // `wkb` 0.9.2 reserved memory for each declared count before it read
871        // a member, and recursed once for each nested collection. These bodies
872        // aborted the process on an allocation failure or a stack overflow.
873        for code in [3, 5, 6, 7] {
874            GpbGeometry::parse(&gpb(&wkb_counted(code, u32::MAX))).unwrap_err();
875        }
876        let mut nested = Vec::new();
877        for _ in 0..100_000 {
878            nested.extend_from_slice(&wkb_counted(7, 1));
879        }
880        GpbGeometry::parse(&gpb(&nested)).unwrap_err();
881    }
882
883    #[test]
884    #[cfg(feature = "geo-types")]
885    fn to_geo_yields_geo_types() {
886        let blob = gpb(&wkb_point(3.0, 4.0));
887        let g = GpbGeometry::parse(&blob).unwrap();
888        let geo = g.to_geo().unwrap();
889        assert_eq!(
890            geo,
891            geo_types::Geometry::Point(geo_types::Point::new(3.0, 4.0))
892        );
893    }
894
895    #[test]
896    fn point_envelope_from_traversal() {
897        let blob = gpb(&wkb_point(3.0, -4.0));
898        let g = GpbGeometry::parse(&blob).unwrap();
899        assert_eq!(g.xy_envelope(), Some([3.0, 3.0, -4.0, -4.0]));
900        assert!(!g.is_empty());
901    }
902
903    #[test]
904    fn empty_point_is_empty_no_envelope() {
905        let blob = gpb(&wkb_point(f64::NAN, f64::NAN));
906        let g = GpbGeometry::parse(&blob).unwrap();
907        assert_eq!(g.xy_envelope(), None);
908        assert!(g.is_empty());
909    }
910
911    #[test]
912    fn header_empty_flag_reports_empty() {
913        // Header empty flag set, but the body still contains a finite point.
914        let mut blob = encode_header(4326, &Envelope::None, true, false);
915        blob.extend_from_slice(&wkb_point(1.0, 2.0));
916        let g = GpbGeometry::parse(&blob).unwrap();
917        assert!(g.is_empty());
918    }
919
920    #[cfg(feature = "geo-types")]
921    fn wkb_body(geom: &geo_types::Geometry<f64>, little_endian: bool) -> Vec<u8> {
922        use wkb::Endianness;
923        use wkb::writer::{WriteOptions, write_geometry};
924        let options = WriteOptions {
925            endianness: if little_endian {
926                Endianness::LittleEndian
927            } else {
928                Endianness::BigEndian
929            },
930        };
931        let mut buf = Vec::new();
932        write_geometry(&mut buf, geom, &options).unwrap();
933        buf
934    }
935
936    /// Build several geometries with known XY bounds, write them (little- and
937    /// big-endian), wrap them with that envelope in the header, and assert the
938    /// traversal reproduces the header envelope.
939    #[test]
940    #[cfg(feature = "geo-types")]
941    fn traversal_bounds_equal_header_envelope() {
942        use geo_types::{
943            Geometry, LineString, MultiLineString, MultiPolygon, Point, Polygon, coord,
944        };
945
946        let line: Geometry<f64> =
947            LineString::from(vec![(0.0, 0.0), (10.0, -3.0), (4.0, 8.0)]).into();
948        let polygon: Geometry<f64> = Polygon::new(
949            LineString::from(vec![
950                (0.0, 0.0),
951                (6.0, 0.0),
952                (6.0, 5.0),
953                (0.0, 5.0),
954                (0.0, 0.0),
955            ]),
956            vec![LineString::from(vec![
957                (1.0, 1.0),
958                (2.0, 1.0),
959                (2.0, 2.0),
960                (1.0, 1.0),
961            ])],
962        )
963        .into();
964        let multipolygon: Geometry<f64> = MultiPolygon::new(vec![
965            Polygon::new(
966                LineString::from(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]),
967                vec![],
968            ),
969            Polygon::new(
970                LineString::from(vec![(-5.0, -5.0), (-4.0, -5.0), (-4.0, -4.0), (-5.0, -5.0)]),
971                vec![],
972            ),
973        ])
974        .into();
975        let multiline: Geometry<f64> = MultiLineString::new(vec![
976            LineString::from(vec![(0.0, 0.0), (3.0, 3.0)]),
977            LineString::from(vec![(-2.0, 7.0), (9.0, -1.0)]),
978        ])
979        .into();
980        let point: Geometry<f64> = Point::from(coord! { x: 2.5, y: -6.0 }).into();
981
982        let cases: [(Geometry<f64>, [f64; 4]); 5] = [
983            (point, [2.5, 2.5, -6.0, -6.0]),
984            (line, [0.0, 10.0, -3.0, 8.0]),
985            (polygon, [0.0, 6.0, 0.0, 5.0]),
986            (multipolygon, [-5.0, 1.0, -5.0, 1.0]),
987            (multiline, [-2.0, 9.0, -1.0, 7.0]),
988        ];
989
990        for (geom, bounds) in cases {
991            for little_endian in [true, false] {
992                let mut blob = encode_header(4326, &Envelope::Xy(bounds), false, false);
993                blob.extend_from_slice(&wkb_body(&geom, little_endian));
994                let g = GpbGeometry::parse(&blob).unwrap();
995                let (bx0, bx1, by0, by1) = g.header().envelope.xy_bounds().unwrap();
996                assert_eq!(
997                    g.xy_envelope(),
998                    Some([bx0, bx1, by0, by1]),
999                    "traversal must match header envelope (little_endian={little_endian})"
1000                );
1001                assert!(!g.is_empty());
1002            }
1003        }
1004    }
1005
1006    #[test]
1007    #[cfg(feature = "geo-types")]
1008    fn geometry_collection_traversal() {
1009        use geo_types::{Geometry, GeometryCollection, LineString, Point};
1010        let members: Vec<Geometry<f64>> = vec![
1011            Geometry::Point(Point::new(1.0, 1.0)),
1012            Geometry::LineString(LineString::from(vec![(-3.0, 0.0), (4.0, 9.0)])),
1013        ];
1014        let gc = Geometry::GeometryCollection(GeometryCollection::new_from(members));
1015        let mut blob = encode_header(4326, &Envelope::None, false, false);
1016        blob.extend_from_slice(&wkb_body(&gc, true));
1017        let g = GpbGeometry::parse(&blob).unwrap();
1018        assert_eq!(g.xy_envelope(), Some([-3.0, 4.0, 0.0, 9.0]));
1019    }
1020
1021    /// A hand-built big-endian WKB LineString body, no `geo-types` needed.
1022    #[test]
1023    fn big_endian_linestring_traversal() {
1024        let mut body = vec![0u8]; // big-endian
1025        body.extend_from_slice(&2u32.to_be_bytes()); // LineString
1026        body.extend_from_slice(&2u32.to_be_bytes()); // 2 points
1027        for (x, y) in [(1.0f64, 2.0f64), (5.0, -1.0)] {
1028            body.extend_from_slice(&x.to_be_bytes());
1029            body.extend_from_slice(&y.to_be_bytes());
1030        }
1031        let blob = gpb(&body);
1032        let g = GpbGeometry::parse(&blob).unwrap();
1033        assert_eq!(g.xy_envelope(), Some([1.0, 5.0, -1.0, 2.0]));
1034    }
1035
1036    #[test]
1037    fn declared_type_matching_rules() {
1038        use GeometryType::*;
1039        // Exact match.
1040        assert!(geometry_type_matches(Point, Point));
1041        assert!(geometry_type_matches(LineString, LineString));
1042        // GEOMETRY accepts anything.
1043        assert!(geometry_type_matches(Point, Geometry));
1044        assert!(geometry_type_matches(CircularString, Geometry));
1045        // GEOMETRYCOLLECTION accepts collections only.
1046        assert!(geometry_type_matches(MultiPoint, GeometryCollection));
1047        assert!(geometry_type_matches(MultiSurface, GeometryCollection));
1048        assert!(geometry_type_matches(
1049            GeometryCollection,
1050            GeometryCollection
1051        ));
1052        assert!(!geometry_type_matches(Point, GeometryCollection));
1053        // A LINESTRING does not satisfy a MULTILINESTRING column.
1054        assert!(!geometry_type_matches(LineString, MultiLineString));
1055        assert!(!geometry_type_matches(Point, LineString));
1056    }
1057
1058    #[test]
1059    fn reads_wkb_type_code_including_curves() {
1060        // POINT (ISO type 1), little-endian.
1061        assert_eq!(
1062            wkb_geometry_type(&wkb_point(0.0, 0.0)).unwrap(),
1063            GeometryType::Point
1064        );
1065        // POINT ZM (ISO type 3001) still classifies as POINT.
1066        let mut point_zm = vec![1u8];
1067        point_zm.extend_from_slice(&3001u32.to_le_bytes());
1068        assert_eq!(wkb_geometry_type(&point_zm).unwrap(), GeometryType::Point);
1069        // CIRCULARSTRING (ISO type 8): the wkb reader cannot parse this, but
1070        // the raw type-code reader classifies it.
1071        let mut circular = vec![1u8];
1072        circular.extend_from_slice(&8u32.to_le_bytes());
1073        assert_eq!(
1074            wkb_geometry_type(&circular).unwrap(),
1075            GeometryType::CircularString
1076        );
1077        // Truncated and unknown-code inputs are typed errors, not panics.
1078        assert!(matches!(
1079            wkb_geometry_type(b"\x01\x00"),
1080            Err(GeometryError::TruncatedWkb)
1081        ));
1082        let mut unknown = vec![1u8];
1083        unknown.extend_from_slice(&99u32.to_le_bytes());
1084        assert!(matches!(
1085            wkb_geometry_type(&unknown),
1086            Err(GeometryError::UnknownWkbType(99))
1087        ));
1088    }
1089
1090    #[test]
1091    fn matches_declared_on_parsed_geometry() {
1092        let blob = gpb(&wkb_point(1.0, 2.0));
1093        let g = GpbGeometry::parse(&blob).unwrap();
1094        assert_eq!(g.geometry_type(), GeometryType::Point);
1095        assert!(g.matches_declared(GeometryType::Point));
1096        assert!(g.matches_declared(GeometryType::Geometry));
1097        assert!(!g.matches_declared(GeometryType::LineString));
1098    }
1099
1100    /// A little-endian ISO WKB `POINT Z` body (type code 1001).
1101    fn wkb_point_z(x: f64, y: f64, z: f64) -> Vec<u8> {
1102        let mut b = vec![1u8];
1103        b.extend_from_slice(&1001u32.to_le_bytes());
1104        b.extend_from_slice(&x.to_le_bytes());
1105        b.extend_from_slice(&y.to_le_bytes());
1106        b.extend_from_slice(&z.to_le_bytes());
1107        b
1108    }
1109
1110    #[test]
1111    #[cfg(feature = "geo-types")]
1112    fn encode_gpb_xy_point_roundtrips() {
1113        let point = geo_types::Point::new(3.0, 4.0);
1114        let (blob, xy) = encode_gpb(&point, 4326).unwrap();
1115        assert_eq!(xy, Some([3.0, 3.0, 4.0, 4.0]));
1116        let g = GpbGeometry::parse(&blob).unwrap();
1117        assert_eq!(g.header().srs_id, 4326);
1118        assert_eq!(g.header().envelope, Envelope::Xy([3.0, 3.0, 4.0, 4.0]));
1119        assert!(!g.header().empty);
1120        assert_eq!(g.geometry_type(), GeometryType::Point);
1121    }
1122
1123    #[test]
1124    #[cfg(feature = "geo-types")]
1125    fn encode_gpb_linestring_envelope() {
1126        let ls = geo_types::LineString::from(vec![(0.0, 0.0), (10.0, -3.0), (4.0, 8.0)]);
1127        let (blob, xy) = encode_gpb(&ls, 4326).unwrap();
1128        assert_eq!(xy, Some([0.0, 10.0, -3.0, 8.0]));
1129        let g = GpbGeometry::parse(&blob).unwrap();
1130        assert_eq!(g.header().envelope, Envelope::Xy([0.0, 10.0, -3.0, 8.0]));
1131        assert_eq!(g.geometry_type(), GeometryType::LineString);
1132    }
1133
1134    #[test]
1135    fn encode_gpb_z_point_writes_xyz_envelope() {
1136        // A Z geometry (built as a GpbGeometry) re-encodes with an XYZ envelope
1137        // and an XYZ WKB body: the writer always emits an envelope, widened to
1138        // Z when the geometry has one.
1139        let src_blob = gpb(&wkb_point_z(1.0, 2.0, 9.0));
1140        let src = GpbGeometry::parse(&src_blob).unwrap();
1141        assert_eq!(src.dim(), Dimensions::Xyz);
1142        let (blob, xy) = encode_gpb(&src, 4326).unwrap();
1143        assert_eq!(xy, Some([1.0, 1.0, 2.0, 2.0]));
1144        let g = GpbGeometry::parse(&blob).unwrap();
1145        assert_eq!(
1146            g.header().envelope,
1147            Envelope::Xyz([1.0, 1.0, 2.0, 2.0, 9.0, 9.0])
1148        );
1149        assert_eq!(g.dim(), Dimensions::Xyz);
1150        assert_eq!(g.geometry_type(), GeometryType::Point);
1151    }
1152
1153    #[test]
1154    fn write_envelope_empty_point_is_empty() {
1155        let blob = gpb(&wkb_point(f64::NAN, f64::NAN));
1156        let g = GpbGeometry::parse(&blob).unwrap();
1157        let (envelope, empty) = write_envelope(&g);
1158        assert_eq!(envelope, Envelope::None);
1159        assert!(empty);
1160        // Re-encoding preserves the empty flag and omits the envelope.
1161        let (reblob, xy) = encode_gpb(&g, 4326).unwrap();
1162        assert_eq!(xy, None);
1163        let re = GpbGeometry::parse(&reblob).unwrap();
1164        assert_eq!(re.header().envelope, Envelope::None);
1165        assert!(re.header().empty);
1166    }
1167}
1168
1169#[cfg(test)]
1170mod encode_from_wkb_tests {
1171    use super::*;
1172    use geo_types::{Geometry, LineString, Point};
1173
1174    /// For a body the re-serialising encoder wrote, the pass-through encoder
1175    /// must produce the same output. The pass-through encoder rests on this:
1176    /// the GPB body is ISO WKB, so copying it is not a shortcut with different
1177    /// semantics.
1178    #[test]
1179    fn agrees_with_encode_gpb() {
1180        let cases: Vec<Geometry<f64>> = vec![
1181            Geometry::Point(Point::new(1.5, -2.5)),
1182            Geometry::LineString(LineString::from(vec![(0.0, 0.0), (10.0, 5.0), (-3.0, 7.5)])),
1183        ];
1184        for geometry in cases {
1185            let (expected, expected_xy) = encode_gpb(&geometry, 4326).unwrap();
1186            // Take the body the round-trip encoder wrote, and feed it back.
1187            let (_, offset) = gpb::parse_header(&expected).unwrap();
1188            let actual = encode_gpb_from_wkb(&expected[offset..], 4326).unwrap();
1189            assert_eq!(actual.blob, expected, "blob differs");
1190            assert_eq!(actual.xy_envelope, expected_xy, "envelope differs");
1191        }
1192    }
1193
1194    #[test]
1195    fn trailing_bytes_are_not_copied() {
1196        let (blob, _) = encode_gpb(&Point::new(3.0, 4.0), 4326).unwrap();
1197        let (_, offset) = gpb::parse_header(&blob).unwrap();
1198        let mut body = blob[offset..].to_vec();
1199        let clean = encode_gpb_from_wkb(&body, 4326).unwrap().blob;
1200        body.extend_from_slice(b"trailing rubbish");
1201        let padded = encode_gpb_from_wkb(&body, 4326).unwrap().blob;
1202        assert_eq!(clean, padded, "trailing bytes reached the blob");
1203    }
1204
1205    #[test]
1206    fn a_body_that_is_not_wkb_is_rejected() {
1207        encode_gpb_from_wkb(&[], 4326).unwrap_err();
1208        encode_gpb_from_wkb(b"not wkb at all", 4326).unwrap_err();
1209        // A valid byte-order marker followed by an unknown geometry type.
1210        encode_gpb_from_wkb(&[1, 0xff, 0xff, 0, 0], 4326).unwrap_err();
1211    }
1212}