use crate::error::{Error, Result};
use crate::gpb::{self, GpbHeader};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Coord3 {
pub x: f64,
pub y: f64,
pub z: Option<f64>,
}
pub fn map_coords(bytes: &[u8], f: &mut dyn FnMut(&mut Coord3)) -> Result<Vec<u8>> {
rewrite_blob(bytes, None, &mut |c, _first, _base, _ring| f(c))
}
pub fn has_m(bytes: &[u8]) -> Result<bool> {
let mut buf = if gpb::is_gpb(bytes) {
let header = GpbHeader::parse(bytes)?;
bytes[header.wkb_offset..].to_vec()
} else {
bytes.to_vec()
};
Ok(rewrite(&mut buf, &mut |_, _, _, _| {})?.has_m)
}
pub fn map_coords_relabelled(
bytes: &[u8],
srid: i32,
f: &mut dyn FnMut(&mut Coord3),
) -> Result<Vec<u8>> {
rewrite_blob(bytes, Some(srid), &mut |c, _first, _base, _ring| f(c))
}
fn rewrite_blob(bytes: &[u8], srid_override: Option<i32>, f: &mut Visitor<'_>) -> Result<Vec<u8>> {
if gpb::is_gpb(bytes) {
let header = GpbHeader::parse(bytes)?;
let mut wkb = bytes[header.wkb_offset..].to_vec();
rewrite(&mut wkb, f)?;
Ok(gpb::write_gpb(
&wkb,
srid_override.unwrap_or(header.srid),
None,
header.empty,
))
} else {
let mut wkb = bytes.to_vec();
let scan = rewrite(&mut wkb, f)?;
Ok(gpb::write_gpb(
&wkb,
srid_override.unwrap_or(scan.srid),
None,
scan.is_empty(),
))
}
}
pub fn for_each_coord(bytes: &[u8], f: &mut dyn FnMut(&Coord3)) -> Result<()> {
for_each_coord_in_runs(bytes, &mut |c, _first| f(c))
}
pub mod base {
pub const POINT: u32 = 1;
pub const LINESTRING: u32 = 2;
pub const POLYGON: u32 = 3;
pub const TRIANGLE: u32 = 17;
}
pub fn for_each_coord_typed(bytes: &[u8], f: &mut dyn FnMut(&Coord3, bool, u32)) -> Result<()> {
for_each_ring_run(bytes, &mut |c, first, base, _ring| f(c, first, base))
}
pub fn for_each_ring_run(bytes: &[u8], f: &mut dyn FnMut(&Coord3, bool, u32, u32)) -> Result<()> {
let mut scratch = if gpb::is_gpb(bytes) {
let header = GpbHeader::parse(bytes)?;
bytes[header.wkb_offset..].to_vec()
} else {
bytes.to_vec()
};
rewrite(&mut scratch, &mut |c, first, base, ring| {
f(c, first, base, ring)
})?;
Ok(())
}
pub fn for_each_coord_in_runs(bytes: &[u8], f: &mut dyn FnMut(&Coord3, bool)) -> Result<()> {
for_each_ring_run(bytes, &mut |c, first, _base, _ring| f(c, first))
}
#[derive(Debug, Default)]
pub struct ZIndex {
map: std::collections::HashMap<(u64, u64), Option<f64>>,
fallback: Option<f64>,
segments: Vec<Segment>,
cursor: std::cell::Cell<usize>,
}
#[derive(Debug, Clone, Copy)]
struct Segment {
ax: f64,
ay: f64,
az: f64,
bx: f64,
by: f64,
bz: f64,
source: u32,
}
const ON_SEGMENT_EPS: f64 = 1e-9;
const AGREEMENT_EPS: f64 = 1e-9;
impl ZIndex {
pub fn at(x: f64, y: f64, z: f64) -> Self {
let mut index = Self::default();
index.insert(x, y, z);
index
}
pub fn constant(z: f64) -> Self {
Self {
fallback: Some(z),
..Self::default()
}
}
pub fn get(&self, x: f64, y: f64) -> Option<f64> {
match self.map.get(&key(x, y)) {
Some(z) => return *z,
None => {
if let Some(z) = self.fallback {
return Some(z);
}
}
}
self.interpolate(x, y)
}
fn interpolate(&self, x: f64, y: f64) -> Option<f64> {
if self.segments.is_empty() {
return None;
}
let mut answer: Option<(f64, u32)> = None;
let start = self.cursor.get();
for step in 0..self.segments.len() {
let i = (start + step) % self.segments.len();
let s = &self.segments[i];
let Some(z) = s.height_at(x, y) else { continue };
match answer {
None => {
answer = Some((z, s.source));
self.cursor.set(i);
}
Some((seen, seen_source)) => {
let scale = seen.abs().max(z.abs()).max(1.0);
if (seen - z).abs() > AGREEMENT_EPS * scale {
let _ = seen_source;
return None;
}
}
}
}
answer.map(|(z, _)| z)
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
fn insert(&mut self, x: f64, y: f64, z: f64) {
use std::collections::hash_map::Entry;
match self.map.entry(key(x, y)) {
Entry::Vacant(v) => {
v.insert(Some(z));
}
Entry::Occupied(mut o) => {
if o.get().is_some_and(|seen| seen.to_bits() != z.to_bits()) {
o.insert(None); }
}
}
}
}
impl Segment {
fn height_at(&self, x: f64, y: f64) -> Option<f64> {
let (dx, dy) = (self.bx - self.ax, self.by - self.ay);
let len_sq = dx * dx + dy * dy;
if len_sq == 0.0 {
return None; }
let t = ((x - self.ax) * dx + (y - self.ay) * dy) / len_sq;
if !(-1e-12..=1.0 + 1e-12).contains(&t) {
return None;
}
let cross = (x - self.ax) * dy - (y - self.ay) * dx;
if cross.abs() > ON_SEGMENT_EPS * len_sq.sqrt() * len_sq.sqrt() {
return None;
}
Some(self.az + t * (self.bz - self.az))
}
}
fn key(x: f64, y: f64) -> (u64, u64) {
((x + 0.0).to_bits(), (y + 0.0).to_bits())
}
pub fn z_index(sources: &[&[u8]]) -> Result<Option<ZIndex>> {
let mut index = ZIndex::default();
for (source, bytes) in sources.iter().enumerate() {
let mut previous: Option<(f64, f64, f64)> = None;
for_each_coord_in_runs(bytes, &mut |c, first| {
let Some(z) = c.z else {
previous = None;
return;
};
index.insert(c.x, c.y, z);
if let Some((ax, ay, az)) = previous.filter(|_| !first) {
index.segments.push(Segment {
ax,
ay,
az,
bx: c.x,
by: c.y,
bz: z,
source: source as u32,
});
}
previous = Some((c.x, c.y, z));
})?;
}
Ok((!index.is_empty()).then_some(index))
}
pub fn write_wkb_z(
g: &geo_types::Geometry<f64>,
index: &ZIndex,
func: &'static str,
) -> Result<Vec<u8>> {
let mut out = Vec::new();
put_geometry(&mut out, g, index, func)?;
Ok(out)
}
fn header(out: &mut Vec<u8>, base: u32) {
out.push(0x01); out.extend_from_slice(&(1000 + base).to_le_bytes());
}
fn put_coord(
out: &mut Vec<u8>,
c: geo_types::Coord<f64>,
index: &ZIndex,
func: &'static str,
) -> Result<()> {
let Some(z) = index.get(c.x, c.y) else {
return Err(Error::Unsupported {
func,
reason: format!(
"this operation would have to invent a Z for ({} {}), which was not \
a vertex of the input; flatten with ST_Force2D first if a 2D result \
is what you want",
c.x, c.y
),
});
};
out.extend_from_slice(&c.x.to_le_bytes());
out.extend_from_slice(&c.y.to_le_bytes());
out.extend_from_slice(&z.to_le_bytes());
Ok(())
}
fn put_ring(
out: &mut Vec<u8>,
ring: &geo_types::LineString<f64>,
index: &ZIndex,
func: &'static str,
) -> Result<()> {
out.extend_from_slice(&(ring.0.len() as u32).to_le_bytes());
for c in &ring.0 {
put_coord(out, *c, index, func)?;
}
Ok(())
}
fn put_polygon(
out: &mut Vec<u8>,
p: &geo_types::Polygon<f64>,
index: &ZIndex,
func: &'static str,
) -> Result<()> {
out.extend_from_slice(&(1 + p.interiors().len() as u32).to_le_bytes());
put_ring(out, p.exterior(), index, func)?;
for r in p.interiors() {
put_ring(out, r, index, func)?;
}
Ok(())
}
fn put_geometry(
out: &mut Vec<u8>,
g: &geo_types::Geometry<f64>,
index: &ZIndex,
func: &'static str,
) -> Result<()> {
use geo_types::Geometry as G;
match g {
G::Point(p) => {
header(out, 1);
put_coord(out, p.0, index, func)?;
}
G::Line(l) => {
header(out, 2);
out.extend_from_slice(&2u32.to_le_bytes());
put_coord(out, l.start, index, func)?;
put_coord(out, l.end, index, func)?;
}
G::LineString(ls) => {
header(out, 2);
out.extend_from_slice(&(ls.0.len() as u32).to_le_bytes());
for c in &ls.0 {
put_coord(out, *c, index, func)?;
}
}
G::Polygon(p) => {
header(out, 3);
put_polygon(out, p, index, func)?;
}
G::Rect(r) => {
header(out, 3);
put_polygon(out, &r.to_polygon(), index, func)?;
}
G::Triangle(t) => {
header(out, 3);
put_polygon(out, &t.to_polygon(), index, func)?;
}
G::MultiPoint(mp) => {
header(out, 4);
out.extend_from_slice(&(mp.0.len() as u32).to_le_bytes());
for p in &mp.0 {
header(out, 1);
put_coord(out, p.0, index, func)?;
}
}
G::MultiLineString(ml) => {
header(out, 5);
out.extend_from_slice(&(ml.0.len() as u32).to_le_bytes());
for ls in &ml.0 {
put_geometry(
out,
&geo_types::Geometry::LineString(ls.clone()),
index,
func,
)?;
}
}
G::MultiPolygon(mp) => {
header(out, 6);
out.extend_from_slice(&(mp.0.len() as u32).to_le_bytes());
for p in &mp.0 {
header(out, 3);
put_polygon(out, p, index, func)?;
}
}
G::GeometryCollection(gc) => {
header(out, 7);
out.extend_from_slice(&(gc.0.len() as u32).to_le_bytes());
for member in &gc.0 {
put_geometry(out, member, index, func)?;
}
}
}
Ok(())
}
struct Scan {
srid: i32,
coords: usize,
nan_point: bool,
has_m: bool,
}
impl Scan {
fn is_empty(&self) -> bool {
self.coords == 0 || self.nan_point
}
}
type Visitor<'a> = dyn FnMut(&mut Coord3, bool, u32, u32) + 'a;
fn rewrite(buf: &mut [u8], f: &mut Visitor<'_>) -> Result<Scan> {
let mut scan = Scan {
srid: 0,
coords: 0,
nan_point: false,
has_m: false,
};
let mut pos = 0usize;
walk(buf, &mut pos, 0, f, &mut scan)?;
Ok(scan)
}
fn walk(
buf: &mut [u8],
pos: &mut usize,
depth: u8,
f: &mut Visitor<'_>,
scan: &mut Scan,
) -> Result<()> {
if depth > 32 {
return Err(bad("geometry nesting too deep"));
}
need(buf.len(), *pos, 5)?;
let le = match buf[*pos] {
0 => false,
1 => true,
b => return Err(bad(&format!("invalid byte-order marker {b:#04x}"))),
};
let ty = rd_u32(buf, *pos + 1, le);
*pos += 5;
if ty & 0x2000_0000 != 0 {
need(buf.len(), *pos, 4)?;
if depth == 0 {
scan.srid = rd_u32(buf, *pos, le) as i32;
}
*pos += 4; }
let (has_z, has_m) = match (ty & 0x0000_FFFF) / 1000 {
1 => (true, false),
2 => (false, true),
3 => (true, true),
_ => (ty & 0x8000_0000 != 0, ty & 0x4000_0000 != 0),
};
let dims = 2 + usize::from(has_z) + usize::from(has_m);
scan.has_m |= has_m;
let top_level = depth == 0;
let base = (ty & 0x0000_FFFF) % 1000;
match base {
1 => run(buf, pos, 1, dims, has_z, le, f, scan, top_level, base, 0)?,
2 => {
let n = count(buf, pos, le)?;
run(buf, pos, n, dims, has_z, le, f, scan, false, base, 0)?;
}
3 | 17 => {
let rings = count(buf, pos, le)?;
for ring in 0..rings {
let n = count(buf, pos, le)?;
run(
buf,
pos,
n,
dims,
has_z,
le,
f,
scan,
false,
base,
ring as u32,
)?;
}
}
4..=7 | 15 | 16 => {
let n = count(buf, pos, le)?;
for _ in 0..n {
walk(buf, pos, depth + 1, f, scan)?;
}
}
_ => return Err(bad("unknown WKB geometry type")),
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run(
buf: &mut [u8],
pos: &mut usize,
n: usize,
dims: usize,
has_z: bool,
le: bool,
f: &mut Visitor<'_>,
scan: &mut Scan,
top_level: bool,
base: u32,
ring: u32,
) -> Result<()> {
let stride = 8 * dims;
let run_start = *pos;
let total = n.checked_mul(stride).ok_or_else(|| bad("count overflow"))?;
need(buf.len(), *pos, total)?;
for _ in 0..n {
let at = *pos;
let mut c = Coord3 {
x: rd_f64(buf, at, le),
y: rd_f64(buf, at + 8, le),
z: if has_z {
Some(rd_f64(buf, at + 16, le))
} else {
None
},
};
if top_level && c.x.is_nan() && c.y.is_nan() {
scan.nan_point = true;
}
f(&mut c, *pos == run_start, base, ring);
wr_f64(buf, at, le, c.x);
wr_f64(buf, at + 8, le, c.y);
if has_z && let Some(z) = c.z {
wr_f64(buf, at + 16, le, z);
}
scan.coords += 1;
*pos = at + stride;
}
Ok(())
}
fn count(buf: &[u8], pos: &mut usize, le: bool) -> Result<usize> {
need(buf.len(), *pos, 4)?;
let n = rd_u32(buf, *pos, le) as usize;
*pos += 4;
Ok(n)
}
fn need(len: usize, pos: usize, want: usize) -> Result<()> {
if pos.checked_add(want).is_none_or(|end| end > len) {
return Err(bad("truncated WKB (element count exceeds available bytes)"));
}
Ok(())
}
fn bad(msg: &str) -> Error {
Error::InvalidWkb(msg.into())
}
fn rd_u32(buf: &[u8], at: usize, le: bool) -> u32 {
let raw: [u8; 4] = buf[at..at + 4].try_into().expect("bounds checked");
if le {
u32::from_le_bytes(raw)
} else {
u32::from_be_bytes(raw)
}
}
fn rd_f64(buf: &[u8], at: usize, le: bool) -> f64 {
let raw: [u8; 8] = buf[at..at + 8].try_into().expect("bounds checked");
if le {
f64::from_le_bytes(raw)
} else {
f64::from_be_bytes(raw)
}
}
fn wr_f64(buf: &mut [u8], at: usize, le: bool, v: f64) {
let raw = if le { v.to_le_bytes() } else { v.to_be_bytes() };
buf[at..at + 8].copy_from_slice(&raw);
}
#[cfg(test)]
mod tests {
use super::*;
fn point_m() -> Vec<u8> {
let mut v = vec![0x01];
v.extend_from_slice(&2001u32.to_le_bytes());
for value in [1.0f64, 2.0, 99.0] {
v.extend_from_slice(&value.to_le_bytes());
}
v
}
fn point_zm() -> Vec<u8> {
let mut v = vec![0x01];
v.extend_from_slice(&3001u32.to_le_bytes());
for value in [1.0f64, 2.0, 3.0, 99.0] {
v.extend_from_slice(&value.to_le_bytes());
}
v
}
fn point_z_be() -> Vec<u8> {
let mut v = vec![0x00];
v.extend_from_slice(&1001u32.to_be_bytes());
for value in [1.0f64, 2.0, 3.0] {
v.extend_from_slice(&value.to_be_bytes());
}
v
}
fn shift(c: &mut Coord3) {
c.x += 10.0;
c.y += 20.0;
if let Some(z) = c.z.as_mut() {
*z += 30.0;
}
}
fn payload(blob: &[u8]) -> Vec<u8> {
let h = GpbHeader::parse(blob).unwrap();
blob[h.wkb_offset..].to_vec()
}
#[test]
fn m_is_never_mistaken_for_z() {
let out = payload(&map_coords(&point_m(), &mut shift).unwrap());
assert_eq!(out[..5], point_m()[..5], "type code must be unchanged");
let read = |at: usize| f64::from_le_bytes(out[at..at + 8].try_into().unwrap());
assert_eq!((read(5), read(13), read(21)), (11.0, 22.0, 99.0));
}
#[test]
fn zm_transforms_the_z_and_leaves_the_m() {
let out = payload(&map_coords(&point_zm(), &mut shift).unwrap());
let read = |at: usize| f64::from_le_bytes(out[at..at + 8].try_into().unwrap());
assert_eq!(
(read(5), read(13), read(21), read(29)),
(11.0, 22.0, 33.0, 99.0)
);
}
#[test]
fn big_endian_input_stays_big_endian() {
let out = payload(&map_coords(&point_z_be(), &mut shift).unwrap());
assert_eq!(out[0], 0x00, "byte-order marker must survive");
let read = |at: usize| f64::from_be_bytes(out[at..at + 8].try_into().unwrap());
assert_eq!((read(5), read(13), read(21)), (11.0, 22.0, 33.0));
}
#[test]
fn a_2d_geometry_cannot_grow_a_z() {
let flat = crate::functions::io::st_geom_from_text("POINT(1 2)", None).unwrap();
let out = map_coords(&flat, &mut |c| {
c.x += 1.0;
c.z = Some(999.0);
})
.unwrap();
assert_eq!(
crate::functions::io::st_as_text(&out).unwrap(),
"POINT(2 2)"
);
assert!(!crate::functions::threed::st_has_z(&out).unwrap());
}
#[test]
fn surface_collections_go_through_untouched_in_structure() {
let cube = crate::functions::surface::fixtures::cube(6);
let moved = map_coords(&cube, &mut shift).unwrap();
assert_eq!(
crate::functions::surface::st_num_patches(&moved).unwrap(),
Some(6)
);
assert_eq!(
crate::functions::surface::is_closed(&moved).unwrap(),
Some(true)
);
assert_eq!(
crate::functions::rtree::st_min_x(&moved).unwrap(),
Some(10.0)
);
assert_eq!(
crate::functions::threed::st_zmin(&moved).unwrap(),
Some(30.0)
);
}
#[test]
fn nested_geometries_are_all_visited() {
let g = crate::functions::io::st_geom_from_text(
"GEOMETRYCOLLECTION(POINT(1 2),MULTIPOLYGON(((0 0,1 0,1 1,0 0))))",
None,
)
.unwrap();
let out = map_coords(&g, &mut shift).unwrap();
assert_eq!(
crate::functions::io::st_as_text(&out).unwrap(),
"GEOMETRYCOLLECTION(POINT(11 22),MULTIPOLYGON(((10 20,11 20,11 21,10 20))))"
);
}
#[test]
fn the_empty_flag_survives_both_containers() {
let empty = crate::functions::io::st_geom_from_text("LINESTRING EMPTY", None).unwrap();
let out = map_coords(&empty, &mut shift).unwrap();
assert!(GpbHeader::parse(&out).unwrap().empty);
let mut nan_wkb = vec![0x01, 0x01, 0x00, 0x00, 0x00];
nan_wkb.extend_from_slice(&f64::NAN.to_le_bytes());
nan_wkb.extend_from_slice(&f64::NAN.to_le_bytes());
assert!(
GpbHeader::parse(&map_coords(&nan_wkb, &mut shift).unwrap())
.unwrap()
.empty
);
}
#[test]
fn an_ewkb_srid_is_carried_over() {
let mut ewkb = vec![0x01];
ewkb.extend_from_slice(&(1u32 | 0x2000_0000).to_le_bytes());
ewkb.extend_from_slice(&4326i32.to_le_bytes());
ewkb.extend_from_slice(&1.0f64.to_le_bytes());
ewkb.extend_from_slice(&2.0f64.to_le_bytes());
let out = map_coords(&ewkb, &mut shift).unwrap();
assert_eq!(GpbHeader::parse(&out).unwrap().srid, 4326);
}
#[test]
fn hostile_and_truncated_input_errors_instead_of_walking_off() {
let mut wkb = vec![0x01, 0x02, 0x00, 0x00, 0x00];
wkb.extend_from_slice(&0xFFFF_FFF0u32.to_le_bytes());
assert!(map_coords(&wkb, &mut shift).is_err());
let full = crate::functions::surface::fixtures::cube(6);
for cut in 1..full.len() {
let _ = map_coords(&full[..cut], &mut shift);
}
let mut junk = vec![0x01];
junk.extend_from_slice(&99u32.to_le_bytes());
let err = map_coords(&junk, &mut shift).unwrap_err().to_string();
assert!(err.contains("unknown WKB geometry type"), "{err}");
}
}