use geo::{Coord, CoordsIter, Geometry, Point, Rect};
use geozero::wkb::Ewkb;
use geozero::{CoordDimensions, ToGeo, ToWkb};
use crate::core::error::{Result, SqliteGisError};
pub const EWKB_SRID_FLAG: u32 = 0x20000000;
pub const EWKB_Z_FLAG: u32 = 0x80000000;
pub const EWKB_M_FLAG: u32 = 0x40000000;
pub const WKB_POINT: u32 = 1;
pub const WKB_LINESTRING: u32 = 2;
pub const WKB_POLYGON: u32 = 3;
pub const WKB_MULTIPOINT: u32 = 4;
pub const WKB_MULTILINESTRING: u32 = 5;
pub const WKB_MULTIPOLYGON: u32 = 6;
pub const WKB_GEOMETRYCOLLECTION: u32 = 7;
pub const MAX_NESTING_DEPTH: usize = 32;
fn read_f64(bytes: [u8; 8], little_endian: bool) -> f64 {
if little_endian {
f64::from_le_bytes(bytes)
} else {
f64::from_be_bytes(bytes)
}
}
pub fn ensure_xy_only(has_z: bool, has_m: bool) -> Result<()> {
let dimensions = if has_z && has_m {
"ZM"
} else if has_z {
"Z"
} else if has_m {
"M"
} else {
return Ok(());
};
Err(SqliteGisError::UnsupportedDimensions { dimensions })
}
fn point_is_empty_with_header(blob: &[u8], header: &EwkbHeader) -> Result<bool> {
if header.geom_type != WKB_POINT {
return Ok(false);
}
let dims = 2 + usize::from(header.has_z) + usize::from(header.has_m);
let needed = header.data_offset + 8 * dims;
if blob.len() < needed {
return Err(SqliteGisError::InvalidEwkb(format!(
"point payload truncated: got {} bytes",
blob.len()
)));
}
let mut x_bytes = [0u8; 8];
x_bytes.copy_from_slice(&blob[header.data_offset..header.data_offset + 8]);
let mut y_bytes = [0u8; 8];
y_bytes.copy_from_slice(&blob[header.data_offset + 8..header.data_offset + 16]);
let x = read_f64(x_bytes, header.little_endian);
let y = read_f64(y_bytes, header.little_endian);
Ok(x.is_nan() && y.is_nan())
}
pub fn is_empty_point_blob(blob: &[u8]) -> Result<bool> {
let header = parse_ewkb_header(blob)?;
point_is_empty_with_header(blob, &header)
}
pub fn validate_ewkb_payload(blob: &[u8]) -> Result<EwkbHeader> {
let header = parse_ewkb_header(blob)?;
validate_payload_structure(blob, &header)?;
Ok(header)
}
pub fn validate_xy_ewkb_payload(blob: &[u8]) -> Result<EwkbHeader> {
let header = validate_ewkb_payload(blob)?;
ensure_xy_only(header.has_z, header.has_m)?;
if !point_is_empty_with_header(blob, &header)? {
let _: Geometry<f64> = Ewkb(blob).to_geo()?;
}
Ok(header)
}
#[derive(Debug, Clone)]
pub struct EwkbHeader {
pub geom_type: u32,
pub srid: Option<i32>,
pub has_z: bool,
pub has_m: bool,
pub data_offset: usize,
pub little_endian: bool,
}
pub fn parse_ewkb_header(blob: &[u8]) -> Result<EwkbHeader> {
if blob.len() < 5 {
return Err(SqliteGisError::InvalidEwkb(format!(
"blob too short: got {} bytes, need at least 5",
blob.len()
)));
}
let little_endian = match blob[0] {
0x01 => true,
0x00 => false,
_ => {
return Err(SqliteGisError::InvalidEwkb(
"invalid byte order marker".to_string(),
))
}
};
let read_u32 = |bytes: [u8; 4]| {
if little_endian {
u32::from_le_bytes(bytes)
} else {
u32::from_be_bytes(bytes)
}
};
let read_i32 = |bytes: [u8; 4]| {
if little_endian {
i32::from_le_bytes(bytes)
} else {
i32::from_be_bytes(bytes)
}
};
let raw_type = read_u32([blob[1], blob[2], blob[3], blob[4]]);
let has_srid = (raw_type & EWKB_SRID_FLAG) != 0;
let has_z = (raw_type & EWKB_Z_FLAG) != 0;
let has_m = (raw_type & EWKB_M_FLAG) != 0;
let geom_type = raw_type & 0x1FFFFFFF;
let mut offset = 5usize;
let srid = if has_srid {
if blob.len() < 9 {
return Err(SqliteGisError::InvalidEwkb(
"SRID flag set but blob too short".to_string(),
));
}
let s = read_i32([blob[5], blob[6], blob[7], blob[8]]);
offset += 4;
Some(s)
} else {
None
};
Ok(EwkbHeader {
geom_type,
srid,
has_z,
has_m,
data_offset: offset,
little_endian,
})
}
pub fn extract_srid(blob: &[u8]) -> Option<i32> {
parse_ewkb_header(blob).ok().and_then(|h| h.srid)
}
pub fn ensure_matching_srid(left: Option<i32>, right: Option<i32>) -> Result<Option<i32>> {
let l = left.unwrap_or(0);
let r = right.unwrap_or(0);
if l != r {
return Err(SqliteGisError::InvalidInput(format!(
"operation on mixed SRID geometries ({l} != {r})"
)));
}
if left.is_none() && right.is_none() {
Ok(None)
} else {
Ok(Some(l))
}
}
pub fn parse_ewkb(blob: &[u8]) -> Result<(Geometry<f64>, Option<i32>)> {
let header = parse_ewkb_header(blob)?;
ensure_xy_only(header.has_z, header.has_m)?;
if point_is_empty_with_header(blob, &header)? {
return Ok((Geometry::Point(Point::new(f64::NAN, f64::NAN)), header.srid));
}
validate_payload_structure(blob, &header)?;
let geom = Ewkb(blob).to_geo()?;
reject_non_finite_coords(&geom)?;
Ok((geom, header.srid))
}
fn reject_non_finite_coords(geom: &Geometry<f64>) -> Result<()> {
if geom
.coords_iter()
.any(|c| !c.x.is_finite() || !c.y.is_finite())
{
return Err(SqliteGisError::InvalidInput(
"geometry has non-finite (NaN or infinite) coordinates".to_string(),
));
}
Ok(())
}
pub fn parse_ewkb_pair(a: &[u8], b: &[u8]) -> Result<(Geometry<f64>, Geometry<f64>, Option<i32>)> {
let (ga, srid_a) = parse_ewkb(a)?;
let (gb, srid_b) = parse_ewkb(b)?;
let srid = ensure_matching_srid(srid_a, srid_b)?;
Ok((ga, gb, srid))
}
pub fn extract_mbr(blob: &[u8]) -> Result<Option<Rect<f64>>> {
let header = parse_ewkb_header(blob)?;
let mut acc: BboxAcc = None;
walk_for_mbr(
blob,
header.data_offset,
header.geom_type,
header.has_z,
header.has_m,
header.little_endian,
0,
&mut acc,
)?;
Ok(acc
.map(|(mnx, mny, mxx, mxy)| Rect::new(Coord { x: mnx, y: mny }, Coord { x: mxx, y: mxy })))
}
type BboxAcc = Option<(f64, f64, f64, f64)>;
fn update_bbox(acc: &mut BboxAcc, x: f64, y: f64) {
if x.is_nan() || y.is_nan() {
return;
}
match acc {
Some((mnx, mny, mxx, mxy)) => {
if x < *mnx {
*mnx = x;
}
if y < *mny {
*mny = y;
}
if x > *mxx {
*mxx = x;
}
if y > *mxy {
*mxy = y;
}
}
None => *acc = Some((x, y, x, y)),
}
}
fn read_f64_at(blob: &[u8], offset: usize, little_endian: bool) -> Result<f64> {
if blob.len() < offset + 8 {
return Err(SqliteGisError::InvalidEwkb(format!(
"blob truncated reading f64 at offset {offset}"
)));
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&blob[offset..offset + 8]);
Ok(read_f64(bytes, little_endian))
}
fn read_u32_at(blob: &[u8], offset: usize, little_endian: bool) -> Result<u32> {
if blob.len() < offset + 4 {
return Err(SqliteGisError::InvalidEwkb(format!(
"blob truncated reading u32 at offset {offset}"
)));
}
let bytes = [
blob[offset],
blob[offset + 1],
blob[offset + 2],
blob[offset + 3],
];
Ok(if little_endian {
u32::from_le_bytes(bytes)
} else {
u32::from_be_bytes(bytes)
})
}
fn advance_past(blob: &[u8], offset: usize, n_bytes: usize) -> Result<usize> {
let end = offset.checked_add(n_bytes).ok_or_else(|| {
SqliteGisError::InvalidEwkb("EWKB coordinate run overflows the address space".to_string())
})?;
if end > blob.len() {
return Err(SqliteGisError::InvalidEwkb(format!(
"EWKB payload truncated: need {end} bytes, have {}",
blob.len()
)));
}
Ok(end)
}
fn validate_nesting(
blob: &[u8],
mut offset: usize,
geom_type: u32,
has_z: bool,
has_m: bool,
little_endian: bool,
depth: usize,
) -> Result<usize> {
if depth > MAX_NESTING_DEPTH {
return Err(SqliteGisError::InvalidEwkb(format!(
"EWKB nesting depth exceeds limit of {MAX_NESTING_DEPTH}"
)));
}
let coord_size = 16 + 8 * usize::from(has_z) + 8 * usize::from(has_m);
let coord_run = |count: usize| -> Result<usize> {
count.checked_mul(coord_size).ok_or_else(|| {
SqliteGisError::InvalidEwkb("EWKB coordinate count overflows the address space".into())
})
};
match geom_type {
WKB_POINT => offset = advance_past(blob, offset, coord_size)?,
WKB_LINESTRING => {
let npoints = read_u32_at(blob, offset, little_endian)? as usize;
offset += 4;
offset = advance_past(blob, offset, coord_run(npoints)?)?;
}
WKB_POLYGON => {
let nrings = read_u32_at(blob, offset, little_endian)? as usize;
offset += 4;
for _ in 0..nrings {
let npoints = read_u32_at(blob, offset, little_endian)? as usize;
offset += 4;
offset = advance_past(blob, offset, coord_run(npoints)?)?;
}
}
WKB_MULTIPOINT | WKB_MULTILINESTRING | WKB_MULTIPOLYGON | WKB_GEOMETRYCOLLECTION => {
let expected_element: Option<u32> = match geom_type {
WKB_MULTIPOINT => Some(WKB_POINT),
WKB_MULTILINESTRING => Some(WKB_LINESTRING),
WKB_MULTIPOLYGON => Some(WKB_POLYGON),
_ => None,
};
let count = read_u32_at(blob, offset, little_endian)? as usize;
offset += 4;
for _ in 0..count {
if blob.len() < offset + 5 {
return Err(SqliteGisError::InvalidEwkb(format!(
"nested WKB header truncated at offset {offset}"
)));
}
let nested_le = match blob[offset] {
0x01 => true,
0x00 => false,
other => {
return Err(SqliteGisError::InvalidEwkb(format!(
"invalid nested byte-order marker {other} at offset {offset}"
)));
}
};
let nested_type = read_u32_at(blob, offset + 1, nested_le)?;
let nested_geom_type = nested_type & 0x1FFFFFFF;
let nested_has_z = (nested_type & EWKB_Z_FLAG) != 0;
let nested_has_m = (nested_type & EWKB_M_FLAG) != 0;
if let Some(expected) = expected_element {
if nested_geom_type != expected {
return Err(SqliteGisError::InvalidEwkb(format!(
"{} element has wrong type code {nested_geom_type}, expected {expected}",
geom_type_name(geom_type),
)));
}
}
offset += 5;
if nested_type & EWKB_SRID_FLAG != 0 {
offset = advance_past(blob, offset, 4)?;
}
offset = validate_nesting(
blob,
offset,
nested_geom_type,
nested_has_z,
nested_has_m,
nested_le,
depth + 1,
)?;
}
}
other => {
return Err(SqliteGisError::InvalidEwkb(format!(
"unsupported geometry type code {other} during EWKB validation"
)));
}
}
Ok(offset)
}
pub fn ensure_ewkb_nesting_ok(blob: &[u8]) -> Result<()> {
let header = parse_ewkb_header(blob)?;
validate_payload_structure(blob, &header)
}
fn validate_payload_structure(blob: &[u8], header: &EwkbHeader) -> Result<()> {
if point_is_empty_with_header(blob, header)? {
return Ok(());
}
let end = validate_nesting(
blob,
header.data_offset,
header.geom_type,
header.has_z,
header.has_m,
header.little_endian,
0,
)?;
if end != blob.len() {
return Err(SqliteGisError::InvalidEwkb(format!(
"EWKB has {} trailing byte(s) after the geometry",
blob.len().saturating_sub(end)
)));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn walk_for_mbr(
blob: &[u8],
mut offset: usize,
geom_type: u32,
has_z: bool,
has_m: bool,
little_endian: bool,
depth: usize,
acc: &mut BboxAcc,
) -> Result<usize> {
if depth > MAX_NESTING_DEPTH {
return Err(SqliteGisError::InvalidEwkb(format!(
"EWKB nesting depth exceeds limit of {MAX_NESTING_DEPTH}"
)));
}
let coord_size = 16 + 8 * usize::from(has_z) + 8 * usize::from(has_m);
match geom_type {
WKB_POINT => {
let x = read_f64_at(blob, offset, little_endian)?;
let y = read_f64_at(blob, offset + 8, little_endian)?;
update_bbox(acc, x, y);
offset += coord_size;
}
WKB_LINESTRING => {
let npoints = read_u32_at(blob, offset, little_endian)? as usize;
offset += 4;
for _ in 0..npoints {
let x = read_f64_at(blob, offset, little_endian)?;
let y = read_f64_at(blob, offset + 8, little_endian)?;
update_bbox(acc, x, y);
offset += coord_size;
}
}
WKB_POLYGON => {
let nrings = read_u32_at(blob, offset, little_endian)? as usize;
offset += 4;
for _ in 0..nrings {
let npoints = read_u32_at(blob, offset, little_endian)? as usize;
offset += 4;
for _ in 0..npoints {
let x = read_f64_at(blob, offset, little_endian)?;
let y = read_f64_at(blob, offset + 8, little_endian)?;
update_bbox(acc, x, y);
offset += coord_size;
}
}
}
WKB_MULTIPOINT | WKB_MULTILINESTRING | WKB_MULTIPOLYGON | WKB_GEOMETRYCOLLECTION => {
let count = read_u32_at(blob, offset, little_endian)? as usize;
offset += 4;
for _ in 0..count {
if blob.len() < offset + 5 {
return Err(SqliteGisError::InvalidEwkb(format!(
"nested WKB header truncated at offset {offset}"
)));
}
let nested_le = match blob[offset] {
0x01 => true,
0x00 => false,
other => {
return Err(SqliteGisError::InvalidEwkb(format!(
"invalid nested byte-order marker {other} at offset {offset}"
)));
}
};
let nested_type = read_u32_at(blob, offset + 1, nested_le)?;
let nested_geom_type = nested_type & 0x1FFFFFFF;
let nested_has_z = (nested_type & EWKB_Z_FLAG) != 0;
let nested_has_m = (nested_type & EWKB_M_FLAG) != 0;
offset += 5;
offset = walk_for_mbr(
blob,
offset,
nested_geom_type,
nested_has_z,
nested_has_m,
nested_le,
depth + 1,
acc,
)?;
}
}
other => {
return Err(SqliteGisError::InvalidEwkb(format!(
"unsupported geometry type code {other} during MBR extraction"
)));
}
}
Ok(offset)
}
fn patch_wkb_with_srid(iso_wkb: &[u8], srid_val: i32) -> Result<Vec<u8>> {
if iso_wkb.len() < 5 {
return Err(SqliteGisError::InvalidEwkb(
"WKB output too short".to_string(),
));
}
let little_endian = match iso_wkb[0] {
0x01 => true,
0x00 => false,
_ => {
return Err(SqliteGisError::InvalidEwkb(
"invalid byte order marker".to_string(),
))
}
};
let raw_type = if little_endian {
u32::from_le_bytes([iso_wkb[1], iso_wkb[2], iso_wkb[3], iso_wkb[4]])
} else {
u32::from_be_bytes([iso_wkb[1], iso_wkb[2], iso_wkb[3], iso_wkb[4]])
};
let ewkb_type = raw_type | EWKB_SRID_FLAG;
let mut out = Vec::with_capacity(iso_wkb.len() + 4);
out.push(iso_wkb[0]);
if little_endian {
out.extend_from_slice(&ewkb_type.to_le_bytes());
out.extend_from_slice(&srid_val.to_le_bytes());
} else {
out.extend_from_slice(&ewkb_type.to_be_bytes());
out.extend_from_slice(&srid_val.to_be_bytes());
}
out.extend_from_slice(&iso_wkb[5..]);
Ok(out)
}
pub fn write_ewkb(geom: &Geometry<f64>, srid: Option<i32>) -> Result<Vec<u8>> {
if let Geometry::Point(p) = geom {
if p.x().is_nan() && p.y().is_nan() {
let mut out = Vec::with_capacity(if srid.is_some() { 25 } else { 21 });
out.push(0x01);
let mut geom_type = WKB_POINT;
if srid.is_some() {
geom_type |= EWKB_SRID_FLAG;
}
out.extend_from_slice(&geom_type.to_le_bytes());
if let Some(srid_val) = srid {
out.extend_from_slice(&srid_val.to_le_bytes());
}
out.extend_from_slice(&f64::NAN.to_le_bytes());
out.extend_from_slice(&f64::NAN.to_le_bytes());
return Ok(out);
}
}
let iso_wkb = geom
.to_wkb(CoordDimensions::xy())
.map_err(SqliteGisError::Geozero)?;
if let Some(srid_val) = srid {
patch_wkb_with_srid(&iso_wkb, srid_val)
} else {
Ok(iso_wkb)
}
}
pub fn set_srid(blob: &[u8], new_srid: i32) -> Result<Vec<u8>> {
let header = validate_ewkb_payload(blob)?;
let mut out = Vec::with_capacity(blob.len() + 4);
out.push(if header.little_endian { 0x01 } else { 0x00 });
let raw_type = if header.little_endian {
u32::from_le_bytes([blob[1], blob[2], blob[3], blob[4]])
} else {
u32::from_be_bytes([blob[1], blob[2], blob[3], blob[4]])
};
let ewkb_type = raw_type | EWKB_SRID_FLAG;
if header.little_endian {
out.extend_from_slice(&ewkb_type.to_le_bytes());
out.extend_from_slice(&new_srid.to_le_bytes());
} else {
out.extend_from_slice(&ewkb_type.to_be_bytes());
out.extend_from_slice(&new_srid.to_be_bytes());
}
out.extend_from_slice(&blob[header.data_offset..]);
Ok(out)
}
pub fn concat_multipolygon_bodies(a: &[u8], b: &[u8]) -> Result<Vec<u8>> {
let ha = parse_ewkb_header(a)?;
let hb = parse_ewkb_header(b)?;
ensure_xy_only(ha.has_z, ha.has_m)?;
ensure_xy_only(hb.has_z, hb.has_m)?;
let srid = ensure_matching_srid(ha.srid, hb.srid)?;
let mut out = Vec::with_capacity(a.len() + b.len() + 16);
out.push(0x01u8);
let type_word: u32 = WKB_MULTIPOLYGON | if srid.is_some() { EWKB_SRID_FLAG } else { 0 };
out.extend_from_slice(&type_word.to_le_bytes());
if let Some(s) = srid {
out.extend_from_slice(&s.to_le_bytes());
}
let count_pos = out.len();
out.extend_from_slice(&0u32.to_le_bytes());
let count_a = append_subpolygons(&mut out, a, &ha)?;
let count_b = append_subpolygons(&mut out, b, &hb)?;
let total = count_a.checked_add(count_b).ok_or_else(|| {
SqliteGisError::InvalidInput("concat_multipolygon_bodies: polygon count overflow".into())
})?;
out[count_pos..count_pos + 4].copy_from_slice(&total.to_le_bytes());
Ok(out)
}
fn append_subpolygons(out: &mut Vec<u8>, blob: &[u8], header: &EwkbHeader) -> Result<u32> {
match header.geom_type {
WKB_POLYGON => {
let endian_byte = if header.little_endian { 0x01u8 } else { 0x00u8 };
out.push(endian_byte);
let type_bytes = if header.little_endian {
WKB_POLYGON.to_le_bytes()
} else {
WKB_POLYGON.to_be_bytes()
};
out.extend_from_slice(&type_bytes);
out.extend_from_slice(&blob[header.data_offset..]);
Ok(1)
}
WKB_MULTIPOLYGON => {
if blob.len() < header.data_offset + 4 {
return Err(SqliteGisError::InvalidEwkb(
"MultiPolygon body too short to hold polygon count".into(),
));
}
let count_bytes: [u8; 4] = blob[header.data_offset..header.data_offset + 4]
.try_into()
.expect("slice length checked above");
let count = if header.little_endian {
u32::from_le_bytes(count_bytes)
} else {
u32::from_be_bytes(count_bytes)
};
out.extend_from_slice(&blob[header.data_offset + 4..]);
Ok(count)
}
other => Err(SqliteGisError::InvalidInput(format!(
"concat_multipolygon_bodies: expected Polygon or MultiPolygon, got {}",
geom_type_name(other),
))),
}
}
pub fn geometry_type_name(geom: &Geometry<f64>) -> &'static str {
match geom {
Geometry::Point(_) => "Point",
Geometry::Line(_) => "Line",
Geometry::LineString(_) => "LineString",
Geometry::Polygon(_) => "Polygon",
Geometry::MultiPoint(_) => "MultiPoint",
Geometry::MultiLineString(_) => "MultiLineString",
Geometry::MultiPolygon(_) => "MultiPolygon",
Geometry::GeometryCollection(_) => "GeometryCollection",
Geometry::Rect(_) => "Rect",
Geometry::Triangle(_) => "Triangle",
}
}
pub fn geom_type_name(raw_type: u32) -> &'static str {
match raw_type & 0x1FFF_FFFF {
WKB_POINT => "ST_Point",
WKB_LINESTRING => "ST_LineString",
WKB_POLYGON => "ST_Polygon",
WKB_MULTIPOINT => "ST_MultiPoint",
WKB_MULTILINESTRING => "ST_MultiLineString",
WKB_MULTIPOLYGON => "ST_MultiPolygon",
WKB_GEOMETRYCOLLECTION => "ST_GeometryCollection",
_ => "ST_Unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::functions::io::geom_from_text;
#[test]
fn header_blob_too_short() {
assert!(parse_ewkb_header(&[0x01, 0x02]).is_err());
assert!(parse_ewkb_header(&[]).is_err());
}
#[test]
fn header_big_endian_point_without_srid() {
let mut blob = vec![0x00];
blob.extend_from_slice(&WKB_POINT.to_be_bytes());
blob.extend_from_slice(&1.0f64.to_be_bytes());
blob.extend_from_slice(&2.0f64.to_be_bytes());
let hdr = parse_ewkb_header(&blob).unwrap();
assert_eq!(hdr.geom_type, WKB_POINT);
assert_eq!(hdr.srid, None);
assert!(!hdr.has_z);
assert!(!hdr.has_m);
assert_eq!(hdr.data_offset, 5);
assert!(!hdr.little_endian);
}
#[test]
fn header_big_endian_point_with_srid() {
let mut blob = vec![0x00];
let typ = WKB_POINT | EWKB_SRID_FLAG;
blob.extend_from_slice(&typ.to_be_bytes());
blob.extend_from_slice(&4326i32.to_be_bytes());
blob.extend_from_slice(&1.0f64.to_be_bytes());
blob.extend_from_slice(&2.0f64.to_be_bytes());
let hdr = parse_ewkb_header(&blob).unwrap();
assert_eq!(hdr.geom_type, WKB_POINT);
assert_eq!(hdr.srid, Some(4326));
assert_eq!(hdr.data_offset, 9);
assert!(!hdr.little_endian);
}
#[test]
fn header_invalid_byte_order_marker() {
assert!(parse_ewkb_header(&[0x02, 0x01, 0x00, 0x00, 0x00]).is_err());
}
#[test]
fn header_srid_flag_but_truncated() {
let mut blob = vec![0x01];
let raw_type = WKB_POINT | EWKB_SRID_FLAG;
blob.extend_from_slice(&raw_type.to_le_bytes());
assert!(parse_ewkb_header(&blob).is_err());
}
#[test]
fn header_valid_point_with_srid() {
let blob = geom_from_text("POINT(1 2)", Some(4326)).unwrap();
let hdr = parse_ewkb_header(&blob).unwrap();
assert_eq!(hdr.geom_type, WKB_POINT);
assert_eq!(hdr.srid, Some(4326));
assert!(!hdr.has_z);
assert!(!hdr.has_m);
assert_eq!(hdr.data_offset, 9); }
#[test]
fn header_valid_point_without_srid() {
let blob = geom_from_text("POINT(1 2)", None).unwrap();
let hdr = parse_ewkb_header(&blob).unwrap();
assert_eq!(hdr.geom_type, WKB_POINT);
assert_eq!(hdr.srid, None);
assert_eq!(hdr.data_offset, 5); }
#[test]
fn extract_srid_empty_blob() {
assert_eq!(extract_srid(&[]), None);
}
#[test]
fn extract_srid_malformed_blob() {
assert_eq!(extract_srid(&[0xFF, 0xFF]), None);
}
#[test]
fn write_ewkb_without_srid() {
let geom = geo::Geometry::Point(geo::Point::new(1.0, 2.0));
let blob = write_ewkb(&geom, None).unwrap();
assert_eq!(extract_srid(&blob), None);
assert_eq!(blob.len(), 21);
}
#[test]
fn write_ewkb_with_srid() {
let geom = geo::Geometry::Point(geo::Point::new(1.0, 2.0));
let blob = write_ewkb(&geom, Some(4326)).unwrap();
assert_eq!(extract_srid(&blob), Some(4326));
assert_eq!(blob.len(), 25);
}
#[test]
fn set_srid_replaces_existing() {
let blob = geom_from_text("POINT(1 2)", Some(4326)).unwrap();
let updated = set_srid(&blob, 3857).unwrap();
assert_eq!(extract_srid(&updated), Some(3857));
let (_, srid) = parse_ewkb(&updated).unwrap();
assert_eq!(srid, Some(3857));
}
#[test]
fn set_srid_adds_to_blob_without_srid() {
let blob = geom_from_text("POINT(1 2)", None).unwrap();
let updated = set_srid(&blob, 4326).unwrap();
assert_eq!(extract_srid(&updated), Some(4326));
}
#[test]
fn set_srid_rejects_truncated_point_payload() {
let mut truncated = vec![0x01];
truncated.extend_from_slice(&WKB_POINT.to_le_bytes());
truncated.extend_from_slice(&1.0f64.to_le_bytes());
set_srid(&truncated, 4326).expect_err("truncated payload must error");
}
#[test]
fn set_srid_rejects_malformed_non_empty_payload() {
let mut malformed = vec![0x01];
malformed.extend_from_slice(&WKB_LINESTRING.to_le_bytes());
malformed.extend_from_slice(&1u32.to_le_bytes());
set_srid(&malformed, 3857).expect_err("malformed payload must error");
}
#[test]
fn set_srid_allows_valid_empty_point_blob() {
let empty = geom_from_text("POINT EMPTY", None).unwrap();
let updated = set_srid(&empty, 4326).unwrap();
let (geom, srid) = parse_ewkb(&updated).unwrap();
assert_eq!(srid, Some(4326));
match geom {
Geometry::Point(p) => {
assert!(p.x().is_nan());
assert!(p.y().is_nan());
}
other => panic!("expected Point, got {other:?}"),
}
}
#[test]
fn geom_type_name_all_types() {
assert_eq!(geom_type_name(WKB_POINT), "ST_Point");
assert_eq!(geom_type_name(WKB_LINESTRING), "ST_LineString");
assert_eq!(geom_type_name(WKB_POLYGON), "ST_Polygon");
assert_eq!(geom_type_name(WKB_MULTIPOINT), "ST_MultiPoint");
assert_eq!(geom_type_name(WKB_MULTILINESTRING), "ST_MultiLineString");
assert_eq!(geom_type_name(WKB_MULTIPOLYGON), "ST_MultiPolygon");
assert_eq!(
geom_type_name(WKB_GEOMETRYCOLLECTION),
"ST_GeometryCollection"
);
assert_eq!(geom_type_name(42), "ST_Unknown");
}
#[test]
fn parse_ewkb_roundtrip() {
let blob = geom_from_text("LINESTRING(0 0, 1 1, 2 2)", Some(4326)).unwrap();
let (geom, srid) = parse_ewkb(&blob).unwrap();
assert_eq!(srid, Some(4326));
let blob2 = write_ewkb(&geom, srid).unwrap();
let (geom2, srid2) = parse_ewkb(&blob2).unwrap();
assert_eq!(srid, srid2);
assert_eq!(format!("{geom:?}"), format!("{geom2:?}"));
}
#[test]
fn parse_big_endian_ewkb_point() {
let mut blob = vec![0x00];
let typ = WKB_POINT | EWKB_SRID_FLAG;
blob.extend_from_slice(&typ.to_be_bytes());
blob.extend_from_slice(&4326i32.to_be_bytes());
blob.extend_from_slice(&10.0f64.to_be_bytes());
blob.extend_from_slice(&(-20.0f64).to_be_bytes());
let (geom, srid) = parse_ewkb(&blob).unwrap();
assert_eq!(srid, Some(4326));
assert_eq!(
geom,
Geometry::Point(geo::Point::new(10.0, -20.0)),
"big-endian EWKB should parse into XY geometry"
);
}
#[test]
fn parse_ewkb_with_zm_point_is_rejected() {
let mut blob = vec![0x01];
let typ = WKB_POINT | EWKB_Z_FLAG | EWKB_M_FLAG;
blob.extend_from_slice(&typ.to_le_bytes());
blob.extend_from_slice(&1.0f64.to_le_bytes());
blob.extend_from_slice(&2.0f64.to_le_bytes());
blob.extend_from_slice(&3.0f64.to_le_bytes()); blob.extend_from_slice(&4.0f64.to_le_bytes());
let err = parse_ewkb(&blob).expect_err("Z/M payloads must not be flattened to XY");
assert!(format!("{err}").contains("unsupported coordinate dimensions"));
}
#[test]
fn set_srid_preserves_big_endian_header_order() {
let mut blob = vec![0x00];
blob.extend_from_slice(&WKB_POINT.to_be_bytes());
blob.extend_from_slice(&7.0f64.to_be_bytes());
blob.extend_from_slice(&8.0f64.to_be_bytes());
let updated = set_srid(&blob, 4326).unwrap();
assert_eq!(updated[0], 0x00, "byte-order marker must stay big-endian");
assert_eq!(extract_srid(&updated), Some(4326));
let (geom, srid) = parse_ewkb(&updated).unwrap();
assert_eq!(srid, Some(4326));
assert_eq!(geom, Geometry::Point(geo::Point::new(7.0, 8.0)));
}
#[test]
fn parse_ewkb_invalid_blob() {
assert!(parse_ewkb(&[0x01, 0x02]).is_err());
}
#[test]
fn ensure_matching_srid_accepts_equal() {
assert_eq!(
ensure_matching_srid(Some(4326), Some(4326)).unwrap(),
Some(4326)
);
assert_eq!(ensure_matching_srid(None, None).unwrap(), None);
}
#[test]
fn ensure_matching_srid_treats_unknown_and_zero_as_compatible() {
assert_eq!(ensure_matching_srid(None, Some(0)).unwrap(), Some(0));
assert_eq!(ensure_matching_srid(Some(0), None).unwrap(), Some(0));
}
#[test]
fn ensure_matching_srid_rejects_mismatch() {
assert!(ensure_matching_srid(Some(4326), Some(3857)).is_err());
assert!(ensure_matching_srid(Some(4326), None).is_err());
}
#[test]
fn parse_ewkb_pair_requires_matching_srid() {
let a = crate::core::functions::io::geom_from_text("POINT(0 0)", Some(4326)).unwrap();
let b = crate::core::functions::io::geom_from_text("POINT(1 1)", Some(4326)).unwrap();
assert!(parse_ewkb_pair(&a, &b).is_ok());
let mixed = crate::core::functions::io::geom_from_text("POINT(1 1)", Some(3857)).unwrap();
assert!(parse_ewkb_pair(&a, &mixed).is_err());
}
#[test]
fn parse_ewkb_pair_accepts_unknown_and_zero_srid() {
let a = crate::core::functions::io::geom_from_text("POINT(0 0)", None).unwrap();
let b = crate::core::functions::io::geom_from_text("POINT(1 1)", Some(0)).unwrap();
let pair = parse_ewkb_pair(&a, &b).expect("None and SRID=0 should be compatible");
assert_eq!(pair.2, Some(0));
}
#[test]
fn parse_empty_point() {
let blob =
write_ewkb(&Geometry::Point(Point::new(f64::NAN, f64::NAN)), Some(4326)).unwrap();
let (geom, srid) = parse_ewkb(&blob).unwrap();
assert_eq!(srid, Some(4326));
match geom {
Geometry::Point(p) => {
assert!(p.x().is_nan());
assert!(p.y().is_nan());
}
other => panic!("expected point, got {other:?}"),
}
assert!(is_empty_point_blob(&blob).unwrap());
}
#[test]
fn patch_wkb_with_srid_little_endian() {
let mut iso = vec![0x01];
iso.extend_from_slice(&WKB_POINT.to_le_bytes());
iso.extend_from_slice(&1.0f64.to_le_bytes());
iso.extend_from_slice(&2.0f64.to_le_bytes());
let ewkb = patch_wkb_with_srid(&iso, 4326).unwrap();
let hdr = parse_ewkb_header(&ewkb).unwrap();
assert!(hdr.little_endian);
assert_eq!(hdr.srid, Some(4326));
}
#[test]
fn patch_wkb_with_srid_big_endian() {
let mut iso = vec![0x00];
iso.extend_from_slice(&WKB_POINT.to_be_bytes());
iso.extend_from_slice(&1.0f64.to_be_bytes());
iso.extend_from_slice(&2.0f64.to_be_bytes());
let ewkb = patch_wkb_with_srid(&iso, 4326).unwrap();
let hdr = parse_ewkb_header(&ewkb).unwrap();
assert!(!hdr.little_endian);
assert_eq!(hdr.srid, Some(4326));
let (geom, srid) = parse_ewkb(&ewkb).unwrap();
assert_eq!(srid, Some(4326));
assert_eq!(geom, Geometry::Point(Point::new(1.0, 2.0)));
}
#[test]
fn patch_wkb_with_srid_rejects_short_input() {
assert!(patch_wkb_with_srid(&[0x01], 4326).is_err());
}
#[test]
fn patch_wkb_with_srid_rejects_invalid_byte_order_marker() {
let mut blob = vec![0x02];
blob.extend_from_slice(&WKB_POINT.to_le_bytes());
let err = patch_wkb_with_srid(&blob, 4326).expect_err("must reject 0x02");
assert!(
matches!(err, SqliteGisError::InvalidEwkb(ref s) if s.contains("byte order marker"))
);
}
#[test]
fn validate_ewkb_payload_accepts_valid_blob() {
let blob = crate::core::functions::io::geom_from_text("LINESTRING(0 0,1 1)", Some(4326))
.expect("valid EWKB");
let header = validate_ewkb_payload(&blob).expect("valid payload");
assert_eq!(header.geom_type, WKB_LINESTRING);
assert_eq!(header.srid, Some(4326));
}
#[test]
fn validate_ewkb_payload_rejects_malformed_non_empty_blob() {
let mut malformed = vec![0x01];
malformed.extend_from_slice(&WKB_LINESTRING.to_le_bytes());
malformed.extend_from_slice(&1u32.to_le_bytes());
validate_ewkb_payload(&malformed).expect_err("malformed payload must error");
}
#[test]
fn validate_xy_ewkb_payload_rejects_zm_blob() {
let mut blob = vec![0x01];
let typ = WKB_POINT | EWKB_Z_FLAG | EWKB_M_FLAG;
blob.extend_from_slice(&typ.to_le_bytes());
blob.extend_from_slice(&1.0f64.to_le_bytes());
blob.extend_from_slice(&2.0f64.to_le_bytes());
blob.extend_from_slice(&3.0f64.to_le_bytes());
blob.extend_from_slice(&4.0f64.to_le_bytes());
let err = validate_xy_ewkb_payload(&blob).expect_err("Z/M payload must be rejected");
assert!(format!("{err}").contains("unsupported coordinate dimensions"));
}
fn assert_mbr_matches_reference(wkt: &str) {
use geo::BoundingRect;
let blob = geom_from_text(wkt, None).expect("seed blob from WKT");
let fast = extract_mbr(&blob).expect("fast MBR path must succeed on valid blob");
let (geom, _) = parse_ewkb(&blob).expect("reference parse must succeed");
let reference = geom.bounding_rect();
match (fast, reference) {
(None, None) => {}
(Some(f), Some(r)) => {
assert!(
(f.min().x - r.min().x).abs() < 1e-12
&& (f.min().y - r.min().y).abs() < 1e-12
&& (f.max().x - r.max().x).abs() < 1e-12
&& (f.max().y - r.max().y).abs() < 1e-12,
"MBR mismatch for {wkt:?}: fast={f:?}, reference={r:?}",
);
}
other => panic!("MBR presence mismatch for {wkt:?}: {other:?}"),
}
}
#[test]
fn extract_mbr_point_matches_reference() {
assert_mbr_matches_reference("POINT(1 2)");
assert_mbr_matches_reference("POINT(-180 -90)");
assert_mbr_matches_reference("POINT(180 90)");
}
#[test]
fn extract_mbr_empty_point_returns_none() {
let blob = geom_from_text("POINT EMPTY", None).expect("seed");
assert!(extract_mbr(&blob).expect("ok").is_none());
}
#[test]
fn extract_mbr_linestring_matches_reference() {
assert_mbr_matches_reference("LINESTRING(0 0, 10 0, 10 5, 0 5)");
assert_mbr_matches_reference("LINESTRING(-5 -10, 5 10)");
}
#[test]
fn extract_mbr_polygon_with_hole_matches_reference() {
assert_mbr_matches_reference(
"POLYGON((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 4 2, 4 4, 2 4, 2 2))",
);
}
#[test]
fn extract_mbr_multipoint_matches_reference() {
assert_mbr_matches_reference("MULTIPOINT((1 2), (5 5), (-3 4))");
}
#[test]
fn extract_mbr_multilinestring_matches_reference() {
assert_mbr_matches_reference("MULTILINESTRING((0 0, 1 1), (5 5, 6 7, -2 3))");
}
#[test]
fn extract_mbr_multipolygon_matches_reference() {
assert_mbr_matches_reference(
"MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)), ((10 10, 20 10, 20 20, 10 20, 10 10)))",
);
}
#[test]
fn extract_mbr_geometrycollection_matches_reference() {
assert_mbr_matches_reference(
"GEOMETRYCOLLECTION(POINT(1 2), LINESTRING(0 0, 5 5), POLYGON((0 0, 2 0, 2 2, 0 2, 0 0)))",
);
}
fn nested_geometrycollection(wrappers: usize) -> Vec<u8> {
let mut blob = vec![0x01u8];
blob.extend_from_slice(&WKB_GEOMETRYCOLLECTION.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
for _ in 0..wrappers {
let mut outer = Vec::with_capacity(blob.len() + 9);
outer.push(0x01u8);
outer.extend_from_slice(&WKB_GEOMETRYCOLLECTION.to_le_bytes());
outer.extend_from_slice(&1u32.to_le_bytes());
outer.extend_from_slice(&blob);
blob = outer;
}
blob
}
#[test]
fn deeply_nested_ewkb_is_rejected_not_overflowed() {
let bomb = nested_geometrycollection(100_000);
assert!(matches!(
extract_mbr(&bomb),
Err(SqliteGisError::InvalidEwkb(_))
));
assert!(matches!(
parse_ewkb(&bomb),
Err(SqliteGisError::InvalidEwkb(_))
));
assert!(matches!(
validate_ewkb_payload(&bomb),
Err(SqliteGisError::InvalidEwkb(_))
));
assert!(matches!(
ensure_ewkb_nesting_ok(&bomb),
Err(SqliteGisError::InvalidEwkb(_))
));
}
#[test]
fn nesting_depth_boundary_is_inclusive() {
let at_limit = nested_geometrycollection(MAX_NESTING_DEPTH);
extract_mbr(&at_limit).expect("nesting at the limit must be accepted");
ensure_ewkb_nesting_ok(&at_limit).expect("nesting at the limit must be accepted");
let over_limit = nested_geometrycollection(MAX_NESTING_DEPTH + 1);
assert!(extract_mbr(&over_limit).is_err());
assert!(ensure_ewkb_nesting_ok(&over_limit).is_err());
}
#[test]
fn malformed_z_blob_does_not_oom() {
const BLOB: &[u8] = &[
1, 6, 0, 0, 128, 4, 0, 0, 0, 0, 0, 0, 0, 1, 6, 128, 0, 255, 254, 255, 127, 0, 6, 0, 0,
131, 93, 0, 1, 1, 0, 0, 0, 0, 1, 6, 0, 1, 0, 64, 1, 6, 0, 1, 1, 0, 128, 93, 0, 1, 1, 0,
0, 0, 0, 1, 6, 0, 0, 1, 1, 0, 128, 0, 6, 0, 0, 128, 93, 0, 1, 1, 0, 0, 0, 0, 1, 6, 0,
45, 250, 255, 255, 0, 6, 0, 1, 1, 0, 0, 0, 247, 1, 6, 0, 0, 0, 0, 0, 0, 1, 6, 0, 0,
128,
];
let _ = validate_ewkb_payload(BLOB);
assert!(
validate_xy_ewkb_payload(BLOB).is_err(),
"Z geometry must be rejected before the geozero decode"
);
}
#[test]
fn shallow_nesting_round_trips() {
let blob = geom_from_text(
"GEOMETRYCOLLECTION(POINT(1 2), LINESTRING(0 0,1 1))",
Some(4326),
)
.expect("seed");
ensure_ewkb_nesting_ok(&blob).expect("shallow nesting is fine");
let (_geom, srid) = parse_ewkb(&blob).expect("shallow nesting parses");
assert_eq!(srid, Some(4326));
}
#[test]
fn extract_mbr_respects_big_endian_byte_order() {
let mut blob = vec![0x00];
blob.extend_from_slice(&WKB_POINT.to_be_bytes());
blob.extend_from_slice(&3.0f64.to_be_bytes());
blob.extend_from_slice(&4.0f64.to_be_bytes());
let mbr = extract_mbr(&blob).expect("ok").expect("non-empty");
assert_eq!(mbr.min().x, 3.0);
assert_eq!(mbr.min().y, 4.0);
assert_eq!(mbr.max().x, 3.0);
assert_eq!(mbr.max().y, 4.0);
}
#[test]
fn extract_mbr_respects_srid_flag_offset() {
let blob = geom_from_text("POINT(7 8)", Some(4326)).expect("seed");
let mbr = extract_mbr(&blob).expect("ok").expect("non-empty");
assert_eq!(mbr.min().x, 7.0);
assert_eq!(mbr.max().y, 8.0);
}
#[test]
fn extract_mbr_rejects_truncated_point_blob() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_POINT.to_le_bytes());
assert!(extract_mbr(&blob).is_err());
}
#[test]
fn extract_mbr_rejects_truncated_polygon_blob() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_POLYGON.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes()); blob.extend_from_slice(&4u32.to_le_bytes()); assert!(extract_mbr(&blob).is_err());
}
fn area_round_trip(blob: &[u8]) -> f64 {
use crate::core::functions::measurement::st_area;
st_area(blob).expect("decode and area")
}
fn poly_count(blob: &[u8]) -> u32 {
let (g, _) = parse_ewkb(blob).expect("decode");
match g {
Geometry::Polygon(_) => 1,
Geometry::MultiPolygon(mp) => mp.0.len() as u32,
other => panic!("expected Polygon or MultiPolygon, got {other:?}"),
}
}
#[test]
fn concat_two_polygons_no_srid_yields_multipolygon() {
let a = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
let b = geom_from_text("POLYGON((10 10,11 10,11 11,10 11,10 10))", None).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
let hdr = parse_ewkb_header(&combined).unwrap();
assert_eq!(hdr.geom_type, WKB_MULTIPOLYGON);
assert_eq!(hdr.srid, None);
assert_eq!(poly_count(&combined), 2);
assert!((area_round_trip(&combined) - 2.0).abs() < 1e-10);
}
#[test]
fn concat_two_polygons_with_srid_preserves_srid() {
let a = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", Some(4326)).unwrap();
let b = geom_from_text("POLYGON((10 10,11 10,11 11,10 11,10 10))", Some(4326)).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
let hdr = parse_ewkb_header(&combined).unwrap();
assert_eq!(hdr.geom_type, WKB_MULTIPOLYGON);
assert_eq!(hdr.srid, Some(4326));
assert_eq!(poly_count(&combined), 2);
}
#[test]
fn concat_polygon_with_multipolygon_combines_counts() {
let a = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
let b = geom_from_text(
"MULTIPOLYGON(((10 10,11 10,11 11,10 11,10 10)),((20 20,21 20,21 21,20 21,20 20)))",
None,
)
.unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
assert_eq!(poly_count(&combined), 3);
assert!((area_round_trip(&combined) - 3.0).abs() < 1e-10);
}
#[test]
fn concat_two_multipolygons_combines_counts() {
let a = geom_from_text(
"MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)),((2 0,3 0,3 1,2 1,2 0)))",
None,
)
.unwrap();
let b = geom_from_text(
"MULTIPOLYGON(((10 10,11 10,11 11,10 11,10 10)),((20 20,21 20,21 21,20 21,20 20)))",
None,
)
.unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
assert_eq!(poly_count(&combined), 4);
assert!((area_round_trip(&combined) - 4.0).abs() < 1e-10);
}
#[test]
fn concat_empty_multipolygon_with_polygon_yields_single_polygon_multipoly() {
let a = geom_from_text("MULTIPOLYGON EMPTY", None).unwrap();
let b = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
assert_eq!(poly_count(&combined), 1);
assert!((area_round_trip(&combined) - 1.0).abs() < 1e-10);
}
#[test]
fn concat_rejects_srid_mismatch() {
let a = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", Some(4326)).unwrap();
let b = geom_from_text("POLYGON((10 10,11 10,11 11,10 11,10 10))", Some(3857)).unwrap();
assert!(concat_multipolygon_bodies(&a, &b).is_err());
}
#[test]
fn concat_rejects_non_polygon_input() {
let a = geom_from_text("LINESTRING(0 0,1 1)", None).unwrap();
let b = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
assert!(concat_multipolygon_bodies(&a, &b).is_err());
}
fn be_unit_square_polygon(srid: Option<i32>) -> Vec<u8> {
let mut blob = vec![0x00u8]; let type_word = WKB_POLYGON | if srid.is_some() { EWKB_SRID_FLAG } else { 0 };
blob.extend_from_slice(&type_word.to_be_bytes());
if let Some(s) = srid {
blob.extend_from_slice(&s.to_be_bytes());
}
blob.extend_from_slice(&1u32.to_be_bytes()); blob.extend_from_slice(&5u32.to_be_bytes()); for (x, y) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)] {
blob.extend_from_slice(&f64::to_be_bytes(x));
blob.extend_from_slice(&f64::to_be_bytes(y));
}
blob
}
fn be_unit_square_multipolygon(dx: f64, dy: f64, srid: Option<i32>) -> Vec<u8> {
let mut blob = vec![0x00u8];
let type_word = WKB_MULTIPOLYGON | if srid.is_some() { EWKB_SRID_FLAG } else { 0 };
blob.extend_from_slice(&type_word.to_be_bytes());
if let Some(s) = srid {
blob.extend_from_slice(&s.to_be_bytes());
}
blob.extend_from_slice(&1u32.to_be_bytes()); blob.push(0x00u8);
blob.extend_from_slice(&WKB_POLYGON.to_be_bytes());
blob.extend_from_slice(&1u32.to_be_bytes()); blob.extend_from_slice(&5u32.to_be_bytes()); for (x, y) in [
(dx, dy),
(dx + 1.0, dy),
(dx + 1.0, dy + 1.0),
(dx, dy + 1.0),
(dx, dy),
] {
blob.extend_from_slice(&f64::to_be_bytes(x));
blob.extend_from_slice(&f64::to_be_bytes(y));
}
blob
}
#[test]
fn concat_big_endian_polygon_inputs_preserve_sub_polygon_endianness() {
let a = be_unit_square_polygon(None);
let b = geom_from_text("POLYGON((10 10,11 10,11 11,10 11,10 10))", None).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
assert_eq!(combined[0], 0x01);
let hdr = parse_ewkb_header(&combined).unwrap();
assert_eq!(hdr.geom_type, WKB_MULTIPOLYGON);
assert!((area_round_trip(&combined) - 2.0).abs() < 1e-10);
assert_eq!(poly_count(&combined), 2);
}
#[test]
fn concat_big_endian_multipolygon_input_reads_count_correctly() {
let a = be_unit_square_multipolygon(0.0, 0.0, None);
let b = be_unit_square_multipolygon(10.0, 0.0, None);
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
assert_eq!(poly_count(&combined), 2);
assert!((area_round_trip(&combined) - 2.0).abs() < 1e-10);
}
#[test]
fn concat_mixed_endianness_inputs() {
let a = be_unit_square_polygon(Some(4326));
let b = geom_from_text("POLYGON((10 10,11 10,11 11,10 11,10 10))", Some(4326)).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
let hdr = parse_ewkb_header(&combined).unwrap();
assert_eq!(hdr.srid, Some(4326));
assert_eq!(poly_count(&combined), 2);
assert!((area_round_trip(&combined) - 2.0).abs() < 1e-10);
}
#[test]
fn concat_rejects_truncated_multipolygon_input() {
let mut a = vec![0x01u8];
a.extend_from_slice(&WKB_MULTIPOLYGON.to_le_bytes());
let b = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
assert!(concat_multipolygon_bodies(&a, &b).is_err());
}
#[test]
fn concat_rejects_z_dimension_inputs() {
let mut a = vec![0x01u8];
let type_word = WKB_POLYGON | EWKB_Z_FLAG;
a.extend_from_slice(&type_word.to_le_bytes());
a.extend_from_slice(&1u32.to_le_bytes());
a.extend_from_slice(&4u32.to_le_bytes());
for _ in 0..(4 * 3) {
a.extend_from_slice(&0f64.to_le_bytes());
}
let b = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
let err = concat_multipolygon_bodies(&a, &b).unwrap_err();
assert!(
matches!(
err,
SqliteGisError::UnsupportedDimensions { dimensions: "Z" }
),
"expected Z rejection, got {err:?}"
);
}
#[test]
fn concat_rejects_m_dimension_inputs() {
let mut b = vec![0x01u8];
let type_word = WKB_POLYGON | EWKB_M_FLAG;
b.extend_from_slice(&type_word.to_le_bytes());
b.extend_from_slice(&1u32.to_le_bytes());
b.extend_from_slice(&4u32.to_le_bytes());
for _ in 0..(4 * 3) {
b.extend_from_slice(&0f64.to_le_bytes());
}
let a = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
assert!(concat_multipolygon_bodies(&a, &b).is_err());
}
#[test]
fn concat_rejects_malformed_header_blob_a() {
let a = vec![0x01, 0x03, 0x00];
let b = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
assert!(concat_multipolygon_bodies(&a, &b).is_err());
}
#[test]
fn concat_rejects_malformed_header_blob_b() {
let a = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
let b = vec![0x42]; assert!(concat_multipolygon_bodies(&a, &b).is_err());
}
#[test]
fn concat_polygon_with_interior_ring_preserves_hole() {
let a =
geom_from_text("POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1))", None).unwrap();
let b = geom_from_text("POLYGON((10 10,11 10,11 11,10 11,10 10))", None).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
assert_eq!(poly_count(&combined), 2);
assert!((area_round_trip(&combined) - 16.0).abs() < 1e-10);
}
#[test]
fn concat_polygon_and_empty_multipolygon_yields_single_polygon() {
let a = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
let b = geom_from_text("MULTIPOLYGON EMPTY", None).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
assert_eq!(poly_count(&combined), 1);
assert!((area_round_trip(&combined) - 1.0).abs() < 1e-10);
}
#[test]
fn concat_two_empty_multipolygons_yields_empty_multipolygon() {
let a = geom_from_text("MULTIPOLYGON EMPTY", None).unwrap();
let b = geom_from_text("MULTIPOLYGON EMPTY", None).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
let hdr = parse_ewkb_header(&combined).unwrap();
assert_eq!(hdr.geom_type, WKB_MULTIPOLYGON);
assert_eq!(poly_count(&combined), 0);
}
#[test]
fn concat_one_null_srid_one_some_srid_treated_as_compatible() {
let a = geom_from_text("POLYGON((0 0,1 0,1 1,0 1,0 0))", None).unwrap();
let b = geom_from_text("POLYGON((10 10,11 10,11 11,10 11,10 10))", Some(0)).unwrap();
let combined = concat_multipolygon_bodies(&a, &b).unwrap();
assert_eq!(poly_count(&combined), 2);
}
#[test]
fn validate_xy_ewkb_payload_decodes_through_geozero() {
let blob = geom_from_text("POINT(5 10)", Some(4326)).unwrap();
let hdr = validate_xy_ewkb_payload(&blob).unwrap();
assert_eq!(hdr.srid, Some(4326));
assert!(!hdr.has_z && !hdr.has_m);
}
#[test]
fn read_u32_at_rejects_truncated_blob() {
let blob: Vec<u8> = vec![0x01, 0x03, 0x00, 0x00, 0x00];
assert!(validate_ewkb_payload(&blob).is_err());
}
#[test]
fn advance_past_catches_offset_overflow() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_LINESTRING.to_le_bytes());
blob.extend_from_slice(&u32::MAX.to_le_bytes());
assert!(validate_ewkb_payload(&blob).is_err());
}
#[test]
fn validate_nesting_rejects_truncated_nested_header() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_MULTIPOINT.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
assert!(validate_ewkb_payload(&blob).is_err());
}
#[test]
fn validate_nesting_rejects_invalid_nested_byte_order() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_MULTIPOINT.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.push(0xFF);
blob.extend_from_slice(&WKB_POINT.to_le_bytes());
blob.extend_from_slice(&1.0f64.to_le_bytes());
blob.extend_from_slice(&2.0f64.to_le_bytes());
assert!(validate_ewkb_payload(&blob).is_err());
}
#[test]
fn validate_nesting_rejects_unknown_type_code() {
let mut blob = vec![0x01];
blob.extend_from_slice(&0u32.to_le_bytes());
assert!(validate_ewkb_payload(&blob).is_err());
}
#[test]
fn validate_nesting_skips_nested_srid() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_GEOMETRYCOLLECTION.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.push(0x01);
blob.extend_from_slice(&(WKB_POINT | EWKB_SRID_FLAG).to_le_bytes());
blob.extend_from_slice(&4326i32.to_le_bytes());
blob.extend_from_slice(&1.0f64.to_le_bytes());
blob.extend_from_slice(&2.0f64.to_le_bytes());
assert!(validate_ewkb_payload(&blob).is_ok());
}
#[test]
fn walk_for_mbr_rejects_truncated_nested_header() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_MULTIPOINT.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
assert!(extract_mbr(&blob).is_err());
}
#[test]
fn walk_for_mbr_rejects_invalid_nested_byte_order() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_MULTIPOINT.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.push(0xFF);
blob.extend_from_slice(&WKB_POINT.to_le_bytes());
blob.extend_from_slice(&1.0f64.to_le_bytes());
blob.extend_from_slice(&2.0f64.to_le_bytes());
assert!(extract_mbr(&blob).is_err());
}
#[test]
fn walk_for_mbr_rejects_unknown_type() {
let mut blob = vec![0x01];
blob.extend_from_slice(&0u32.to_le_bytes());
assert!(extract_mbr(&blob).is_err());
}
#[test]
fn geometry_type_name_line() {
let line = geo::Line::new(geo::Point::new(0.0, 0.0), geo::Point::new(1.0, 1.0));
assert_eq!(geometry_type_name(&geo::Geometry::Line(line)), "Line");
}
#[test]
fn geometry_type_name_rect() {
let rect = geo::Rect::new(geo::Point::new(0.0, 0.0), geo::Point::new(1.0, 1.0));
assert_eq!(geometry_type_name(&geo::Geometry::Rect(rect)), "Rect");
}
#[test]
fn geometry_type_name_triangle() {
let tri = geo::Triangle::new(
geo::Coord { x: 0.0, y: 0.0 },
geo::Coord { x: 1.0, y: 0.0 },
geo::Coord { x: 0.0, y: 1.0 },
);
assert_eq!(
geometry_type_name(&geo::Geometry::Triangle(tri)),
"Triangle"
);
}
#[test]
fn validate_nesting_rejects_type_inconsistent_multipoint() {
let mut blob = vec![0x01];
blob.extend_from_slice(&WKB_MULTIPOINT.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.push(0x01);
blob.extend_from_slice(&WKB_LINESTRING.to_le_bytes());
blob.extend_from_slice(&2u32.to_le_bytes());
blob.extend_from_slice(&0.0f64.to_le_bytes());
blob.extend_from_slice(&0.0f64.to_le_bytes());
blob.extend_from_slice(&1.0f64.to_le_bytes());
blob.extend_from_slice(&1.0f64.to_le_bytes());
let err = validate_ewkb_payload(&blob).unwrap_err();
let err_str = format!("{err}");
assert!(
err_str.contains("wrong type"),
"unexpected error: {err_str}"
);
}
}