#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[allow(clippy::upper_case_acronyms)]
pub enum Dim {
XY,
XYZ,
XYM,
XYZM,
}
impl Dim {
fn from_code(c: u32) -> Option<Dim> {
match c {
0 => Some(Dim::XY),
1 => Some(Dim::XYZ),
2 => Some(Dim::XYM),
3 => Some(Dim::XYZM),
_ => None,
}
}
pub fn size(self) -> usize {
match self {
Dim::XY => 2,
Dim::XYZ | Dim::XYM => 3,
Dim::XYZM => 4,
}
}
pub fn suffix(self) -> &'static str {
match self {
Dim::XY => "",
Dim::XYZ => " Z",
Dim::XYM => " M",
Dim::XYZM => " ZM",
}
}
}
pub const BASE_NAMES: [&str; 7] = [
"Point",
"LineString",
"Polygon",
"MultiPoint",
"MultiLineString",
"MultiPolygon",
"GeometryCollection",
];
pub fn type_name(code: u32) -> Option<String> {
let dim = Dim::from_code(code / 1000)?;
let base = code % 1000;
if !(1..=7).contains(&base) {
return None;
}
Some(format!(
"{}{}",
BASE_NAMES[(base - 1) as usize],
dim.suffix()
))
}
#[derive(Debug)]
pub struct Geom {
pub base: u32,
pub dim: Dim,
pub range: [Option<(f64, f64)>; 4],
pub xy: Vec<(f64, f64)>,
pub polygons: Vec<Vec<(usize, usize)>>,
}
impl Geom {
pub fn type_name(&self) -> String {
format!(
"{}{}",
BASE_NAMES[(self.base - 1) as usize],
self.dim.suffix()
)
}
}
const MAX_DEPTH: u32 = 1000;
const HEADER: u64 = 5;
struct Cur<'a> {
b: &'a [u8],
pos: usize,
}
impl<'a> Cur<'a> {
fn remaining(&self) -> u64 {
(self.b.len() - self.pos) as u64
}
fn take(&mut self, n: usize) -> Result<&'a [u8], String> {
if self.pos + n > self.b.len() {
return Err(format!(
"truncated WKB: need {n} bytes at offset {}, have {}",
self.pos,
self.b.len()
));
}
let s = &self.b[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn u8(&mut self) -> Result<u8, String> {
Ok(self.take(1)?[0])
}
fn u32(&mut self, le: bool) -> Result<u32, String> {
let s: [u8; 4] = self.take(4)?.try_into().unwrap();
Ok(if le {
u32::from_le_bytes(s)
} else {
u32::from_be_bytes(s)
})
}
fn f64(&mut self, le: bool) -> Result<f64, String> {
let s: [u8; 8] = self.take(8)?.try_into().unwrap();
Ok(if le {
f64::from_le_bytes(s)
} else {
f64::from_be_bytes(s)
})
}
fn count(&mut self, le: bool, what: &str, min_item_bytes: u64) -> Result<u32, String> {
let n = self.u32(le)?;
if n as u64 * min_item_bytes > self.remaining() {
return Err(format!(
"WKB claims {n} {what} but only {} bytes remain",
self.remaining()
));
}
Ok(n)
}
}
pub fn parse(bytes: &[u8]) -> Result<Geom, String> {
let mut cur = Cur { b: bytes, pos: 0 };
let mut g = Geom {
base: 0,
dim: Dim::XY,
range: [None; 4],
xy: Vec::new(),
polygons: Vec::new(),
};
parse_geom(&mut cur, &mut g, None, None, 0)?;
if cur.pos != bytes.len() {
return Err(format!(
"{} trailing bytes after the geometry",
bytes.len() - cur.pos
));
}
Ok(g)
}
fn parse_geom(
cur: &mut Cur,
g: &mut Geom,
expected: Option<u32>,
parent_dim: Option<Dim>,
depth: u32,
) -> Result<(), String> {
if depth > MAX_DEPTH {
return Err(format!(
"geometry nesting deeper than {MAX_DEPTH}; not decoded"
));
}
let le = match cur.u8()? {
0 => false,
1 => true,
b => return Err(format!("invalid byte order marker {b}")),
};
let t = cur.u32(le)?;
if t & 0xE000_0000 != 0 {
return Err(format!(
"EWKB flag bits in type 0x{t:08x}; ISO WKB uses 1000/2000/3000 offsets"
));
}
let dim = Dim::from_code(t / 1000).ok_or_else(|| format!("unknown geometry type code {t}"))?;
let base = t % 1000;
if !(1..=7).contains(&base) {
return Err(format!("unknown geometry type code {t}"));
}
if let Some(e) = expected
&& base != e
{
return Err(format!(
"Multi{} member is a {}",
BASE_NAMES[(e - 1) as usize],
BASE_NAMES[(base - 1) as usize]
));
}
if let Some(pd) = parent_dim
&& dim != pd
{
return Err(format!(
"member dimension{} differs from its parent's{}",
dim.suffix(),
pd.suffix()
));
}
if depth == 0 {
g.base = base;
g.dim = dim;
}
let point_bytes = dim.size() as u64 * 8;
match base {
1 => read_point(cur, le, dim, g, true)?,
2 => {
let n = cur.count(le, "points", point_bytes)?;
for _ in 0..n {
read_point(cur, le, dim, g, false)?;
}
}
3 => {
let nrings = cur.count(le, "rings", 4)?;
let mut rings = Vec::new();
for _ in 0..nrings {
let npts = cur.count(le, "ring points", point_bytes)?;
let start = g.xy.len();
for _ in 0..npts {
read_point(cur, le, dim, g, false)?;
}
rings.push((start, g.xy.len() - start));
}
g.polygons.push(rings);
}
_ => {
let n = cur.count(le, "members", HEADER)?;
let member = match base {
4 => Some(1),
5 => Some(2),
6 => Some(3),
_ => None,
};
for _ in 0..n {
parse_geom(cur, g, member, Some(dim), depth + 1)?;
}
}
}
Ok(())
}
fn read_point(
cur: &mut Cur,
le: bool,
dim: Dim,
g: &mut Geom,
standalone: bool,
) -> Result<(), String> {
let x = cur.f64(le)?;
let y = cur.f64(le)?;
let (z, m) = match dim {
Dim::XY => (None, None),
Dim::XYZ => (Some(cur.f64(le)?), None),
Dim::XYM => (None, Some(cur.f64(le)?)),
Dim::XYZM => {
let z = cur.f64(le)?;
(Some(z), Some(cur.f64(le)?))
}
};
if x.is_nan() || y.is_nan() {
if standalone {
return Ok(()); }
return Err("NaN coordinate in a LineString or Polygon vertex".into());
}
g.xy.push((x, y));
for (i, v) in [Some(x), Some(y), z, m].into_iter().enumerate() {
if let Some(v) = v
&& !v.is_nan()
{
g.range[i] = Some(match g.range[i] {
None => (v, v),
Some((lo, hi)) => (lo.min(v), hi.max(v)),
});
}
}
Ok(())
}
pub fn signed_area2(pts: &[(f64, f64)]) -> f64 {
let Some(&(ox, oy)) = pts.first() else {
return 0.0;
};
pts.iter()
.zip(pts.iter().cycle().skip(1))
.map(|(a, b)| (a.0 - ox) * (b.1 - oy) - (b.0 - ox) * (a.1 - oy))
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn le(t: u32, body: &[f64]) -> Vec<u8> {
let mut v = vec![1u8];
v.extend(t.to_le_bytes());
for f in body {
v.extend(f.to_le_bytes());
}
v
}
#[test]
fn point_and_point_z() {
let g = parse(&le(1, &[1.0, 2.0])).unwrap();
assert_eq!(g.type_name(), "Point");
assert_eq!(g.range[0], Some((1.0, 1.0)));
let g = parse(&le(1001, &[1.0, 2.0, 3.0])).unwrap();
assert_eq!(g.type_name(), "Point Z");
assert_eq!(g.range[2], Some((3.0, 3.0)));
assert_eq!(type_name(3006).as_deref(), Some("MultiPolygon ZM"));
assert_eq!(type_name(8), None);
}
#[test]
fn empty_point_is_fine_but_nan_vertex_is_not() {
let g = parse(&le(1, &[f64::NAN, f64::NAN])).unwrap();
assert!(g.xy.is_empty());
let mut v = vec![1u8];
v.extend(2u32.to_le_bytes());
v.extend(2u32.to_le_bytes());
for f in [0.0, 0.0, f64::NAN, 1.0] {
v.extend(f.to_le_bytes());
}
assert!(parse(&v).unwrap_err().contains("NaN"));
}
#[test]
fn polygon_orientation_survives_large_offsets() {
let ring = [
(1e7, 1e7),
(1e7 + 1e-6, 1e7),
(1e7 + 1e-6, 1e7 + 1e-6),
(1e7, 1e7 + 1e-6),
(1e7, 1e7),
];
assert!(signed_area2(&ring) > 0.0);
let cw: Vec<_> = ring.iter().rev().copied().collect();
assert!(signed_area2(&cw) < 0.0);
}
#[test]
fn rejects_ewkb_truncation_trailing_and_bombs() {
let mut ewkb = le(0x2000_0001, &[]);
ewkb.extend(4326u32.to_le_bytes());
assert!(parse(&ewkb).unwrap_err().contains("EWKB"));
assert!(parse(&le(1, &[1.0])).unwrap_err().contains("truncated"));
let mut trailing = le(1, &[1.0, 2.0]);
trailing.push(0);
assert!(parse(&trailing).unwrap_err().contains("trailing"));
let mut bomb = vec![1u8];
bomb.extend(3u32.to_le_bytes());
bomb.extend(u32::MAX.to_le_bytes());
assert!(parse(&bomb).unwrap_err().contains("claims"));
}
#[test]
fn multi_members_must_match_type_and_dimension() {
let mut mp = vec![1u8];
mp.extend(6u32.to_le_bytes()); mp.extend(1u32.to_le_bytes());
mp.extend(le(1, &[0.0, 0.0])); assert!(parse(&mp).unwrap_err().contains("member is a Point"));
let mut mpt = vec![1u8];
mpt.extend(4u32.to_le_bytes()); mpt.extend(1u32.to_le_bytes());
mpt.extend(le(1001, &[0.0, 0.0, 1.0])); assert!(parse(&mpt).unwrap_err().contains("dimension"));
let mut gc = Vec::new();
for _ in 0..40 {
gc.push(1u8);
gc.extend(7u32.to_le_bytes());
gc.extend(1u32.to_le_bytes());
}
gc.extend(le(1, &[0.0, 0.0]));
assert_eq!(parse(&gc).unwrap().type_name(), "GeometryCollection");
}
}