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