use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Box3d {
pub min: [f64; 3],
pub max: [f64; 3],
}
pub fn looks_like_text(bytes: &[u8]) -> bool {
if crate::gpb::is_gpb(bytes) {
return false;
}
bytes
.iter()
.find(|b| !b.is_ascii_whitespace())
.is_some_and(u8::is_ascii_graphic)
}
pub fn parse(bytes: &[u8], func: &'static str) -> Result<Box3d> {
let text = std::str::from_utf8(bytes).map_err(|_| bad(func, "not valid UTF-8"))?;
parse_str(text, func)
}
pub fn parse_str(text: &str, func: &'static str) -> Result<Box3d> {
let s = text.trim();
let rest = strip_prefix_ci(s, "BOX3D")
.or_else(|| strip_prefix_ci(s, "BOX"))
.ok_or_else(|| bad(func, "expected a box literal starting with BOX3D( or BOX("))?;
let rest = rest.trim_start();
let inner = rest
.strip_prefix('(')
.and_then(|r| r.strip_suffix(')'))
.ok_or_else(|| bad(func, "expected parentheses around the two corners"))?;
let (lo, hi) = inner
.split_once(',')
.ok_or_else(|| bad(func, "expected two comma-separated corners"))?;
let lo = ordinates(lo, func)?;
let hi = ordinates(hi, func)?;
if lo.len() != hi.len() {
return Err(bad(
func,
"both corners must have the same number of ordinates",
));
}
let z = |c: &[f64]| c.get(2).copied().unwrap_or(0.0);
let (lo3, hi3) = ([lo[0], lo[1], z(&lo)], [hi[0], hi[1], z(&hi)]);
Ok(Box3d {
min: [lo3[0].min(hi3[0]), lo3[1].min(hi3[1]), lo3[2].min(hi3[2])],
max: [lo3[0].max(hi3[0]), lo3[1].max(hi3[1]), lo3[2].max(hi3[2])],
})
}
pub fn min_ordinate(bytes: &[u8], n: usize, func: &'static str) -> Result<Option<f64>> {
Ok(Some(parse(bytes, func)?.min[n]))
}
pub fn max_ordinate(bytes: &[u8], n: usize, func: &'static str) -> Result<Option<f64>> {
Ok(Some(parse(bytes, func)?.max[n]))
}
fn ordinates(corner: &str, func: &'static str) -> Result<Vec<f64>> {
let parts: Vec<&str> = corner.split_ascii_whitespace().collect();
if parts.len() != 2 && parts.len() != 3 {
return Err(bad(
func,
"each corner needs 2 or 3 space-separated ordinates",
));
}
parts
.iter()
.map(|p| {
p.parse::<f64>()
.map_err(|_| bad(func, &format!("`{p}` is not a number")))
})
.collect()
}
fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let (head, tail) = s.split_at_checked(prefix.len())?;
head.eq_ignore_ascii_case(prefix).then_some(tail)
}
fn bad(func: &'static str, why: &str) -> Error {
Error::Unsupported {
func,
reason: format!(
"got TEXT that is not a box literal ({why}); \
expected a geometry BLOB, `BOX3D(minx miny minz,maxx maxy maxz)` \
or `BOX(minx miny,maxx maxy)` \
(for a geometry given as text, wrap it in ST_GeomFromText)"
),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> Box3d {
parse_str(s, "ST_MinX").unwrap()
}
#[test]
fn parses_the_three_d_form() {
assert_eq!(
p("BOX3D(1 2 3,4 5 6)"),
Box3d {
min: [1.0, 2.0, 3.0],
max: [4.0, 5.0, 6.0]
}
);
}
#[test]
fn two_d_forms_get_zero_z() {
let b = Box3d {
min: [1.0, 2.0, 0.0],
max: [4.0, 5.0, 0.0],
};
assert_eq!(p("BOX3D(1 2,4 5)"), b);
assert_eq!(p("BOX(1 2,4 5)"), b);
}
#[test]
fn corners_are_normalized_per_axis() {
assert_eq!(
p("BOX3D(4 5 6,1 2 3)"),
Box3d {
min: [1.0, 2.0, 3.0],
max: [4.0, 5.0, 6.0]
}
);
}
#[test]
fn number_spellings_postgis_accepts() {
assert_eq!(p("BOX3D(1e2 2 3,4 5 6)").max[0], 100.0);
assert_eq!(p("BOX3D(+1 2 3,4 5 6)").min[0], 1.0);
assert_eq!(p("BOX3D(.5 2 3,4 5 6)").min[0], 0.5);
assert_eq!(p("BOX3D(-1.5 -2 -3,4 5 6)").min[0], -1.5);
}
#[test]
fn whitespace_and_case_are_kenro_leniencies() {
let b = p("BOX3D(1 2 3,4 5 6)");
assert_eq!(p(" box3d( 1 2 3 , 4 5 6 ) "), b);
assert_eq!(p("BOX3D (1 2 3,4 5 6)"), b);
assert_eq!(p("Box3D(1 2 3,4 5 6)"), b);
}
#[test]
fn sscanf_accidents_are_rejected() {
for s in [
"BOX3D(1 2 3,4 5 6)junk", "BOX3D(1 2 3,4 5 6", "BOX3D(1 2,4 5 6)", ] {
assert!(parse_str(s, "ST_MinX").is_err(), "{s} should be rejected");
}
}
#[test]
fn malformed_boxes_postgis_rejects_too() {
for s in [
"BOX3D(1 2 3,4 5)",
"BOX3D(1 2 3 9,4 5 6 9)",
"BOX3D(1 2 3)",
"BOX3D()",
"BOX3D EMPTY",
"BOX3D(a b c,d e f)",
"POINT(1 2)",
"",
] {
assert!(parse_str(s, "ST_MinX").is_err(), "{s} should be rejected");
}
}
#[test]
fn the_error_names_both_ways_in() {
let e = parse_str("BOX3D EMPTY", "ST_MinX").unwrap_err().to_string();
assert!(e.contains("BOX3D(minx miny minz"), "{e}");
assert!(e.contains("ST_GeomFromText"), "{e}");
}
#[test]
fn geometry_encodings_never_look_like_box_text() {
assert!(!looks_like_text(b"GP\x00\x01"));
assert!(!looks_like_text(&[0x00, 0x00, 0x00, 0x00, 0x01]));
assert!(!looks_like_text(&[0x01, 0x01, 0x00, 0x00, 0x00]));
assert!(!looks_like_text(b""));
assert!(looks_like_text(b"BOX3D(1 2 3,4 5 6)"));
assert!(looks_like_text(b" box(1 2,3 4)"));
}
}