use crate::adapter::json::{
self, JNode, JsonMatch, JsonModel, K_ARRAY, K_NULL, K_NUMBER, K_OBJECT, K_STRING,
};
use crate::error::{Error, ErrorClass, Result};
use crate::limits::Limits;
pub const MODEL_VERSION: u8 = 1;
pub const MAX_MODEL_NODES: u32 = 1 << 24;
pub const MAX_MODEL_ANCHORS: u32 = 1 << 24;
pub const G_POINT: u8 = 0;
pub const G_MULTIPOINT: u8 = 1;
pub const G_LINESTRING: u8 = 2;
pub const G_MULTILINESTRING: u8 = 3;
pub const G_POLYGON: u8 = 4;
pub const G_MULTIPOLYGON: u8 = 5;
pub const G_GEOMETRYCOLLECTION: u8 = 6;
pub const G_FEATURE: u8 = 7;
pub const G_FEATURE_COLLECTION: u8 = 8;
pub const TYPE_NAMES: [&str; 9] = [
"Point",
"MultiPoint",
"LineString",
"MultiLineString",
"Polygon",
"MultiPolygon",
"GeometryCollection",
"Feature",
"FeatureCollection",
];
pub const fn class_name(class: u8) -> &'static str {
match class {
G_POINT => "Point",
G_MULTIPOINT => "MultiPoint",
G_LINESTRING => "LineString",
G_MULTILINESTRING => "MultiLineString",
G_POLYGON => "Polygon",
G_MULTIPOLYGON => "MultiPolygon",
G_GEOMETRYCOLLECTION => "GeometryCollection",
G_FEATURE => "Feature",
G_FEATURE_COLLECTION => "FeatureCollection",
_ => "unknown",
}
}
pub fn class_of(name: &str) -> Option<u8> {
TYPE_NAMES.iter().position(|&n| n == name).map(|i| i as u8)
}
pub const fn is_geometry_class(class: u8) -> bool {
class <= G_GEOMETRYCOLLECTION
}
pub fn is_spec_key(class: u8, key: &str) -> bool {
match class {
G_FEATURE => matches!(key, "type" | "geometry" | "properties" | "id" | "bbox"),
G_FEATURE_COLLECTION => matches!(key, "type" | "features" | "bbox"),
G_GEOMETRYCOLLECTION => matches!(key, "type" | "geometries" | "bbox"),
_ => matches!(key, "type" | "coordinates" | "bbox"),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeojsonModel {
pub class: u8,
pub root: u32,
pub type_node: u32,
pub geometries: Vec<u32>,
pub features: Vec<u32>,
pub json: JsonModel,
}
impl GeojsonModel {
pub fn node(&self, index: u32) -> Option<&JNode> {
self.json.node(index)
}
pub fn geometry_count(&self) -> u32 {
self.geometries.len() as u32
}
pub fn feature_count(&self) -> u32 {
self.features.len() as u32
}
pub fn encode(&self) -> Vec<u8> {
let json_bytes = self.json.encode();
let mut out = Vec::with_capacity(
48 + json_bytes.len() + (self.geometries.len() + self.features.len()) * 4,
);
out.extend_from_slice(b"GEOJ");
out.push(MODEL_VERSION);
out.push(self.class);
out.extend_from_slice(&[0, 0]); out.extend_from_slice(&self.doc_len().to_le_bytes());
out.extend_from_slice(&self.root.to_le_bytes());
out.extend_from_slice(&self.type_node.to_le_bytes());
out.extend_from_slice(&(self.geometries.len() as u32).to_le_bytes());
for g in &self.geometries {
out.extend_from_slice(&g.to_le_bytes());
}
out.extend_from_slice(&(self.features.len() as u32).to_le_bytes());
for f in &self.features {
out.extend_from_slice(&f.to_le_bytes());
}
out.extend_from_slice(&(json_bytes.len() as u32).to_le_bytes());
out.extend_from_slice(&json_bytes);
out
}
pub fn decode(bytes: &[u8]) -> Result<GeojsonModel> {
let mut r = Reader::new(bytes);
if r.bytes(4)? != b"GEOJ" {
return Err(corrupt("bad model magic"));
}
if r.u8()? != MODEL_VERSION {
return Err(corrupt("unsupported model version"));
}
let class = r.u8()?;
if class > G_FEATURE_COLLECTION {
return Err(corrupt("unknown GeoJSON class"));
}
if r.bytes(2)? != [0, 0] {
return Err(corrupt("unknown model flags"));
}
let doc_len = r.u64()?;
let root = r.u32()?;
let type_node = r.u32()?;
let gn = r.u32()?;
if gn > MAX_MODEL_ANCHORS {
return Err(corrupt("geometry count is implausible"));
}
let mut geometries = Vec::with_capacity(gn as usize);
for _ in 0..gn {
geometries.push(r.u32()?);
}
let fn_ = r.u32()?;
if fn_ > MAX_MODEL_ANCHORS {
return Err(corrupt("feature count is implausible"));
}
let mut features = Vec::with_capacity(fn_ as usize);
for _ in 0..fn_ {
features.push(r.u32()?);
}
let json_len = r.u32()?;
let json_bytes = r.bytes(json_len as usize)?;
if !r.at_end() {
return Err(corrupt("model has trailing bytes"));
}
let json = JsonModel::decode(json_bytes)?;
if json.doc_len != doc_len {
return Err(corrupt("model doc_len disagrees with the JSON model"));
}
let nnodes = json.nodes.len() as u32;
if root >= nnodes || type_node >= nnodes {
return Err(corrupt("model anchor is out of range"));
}
for &g in &geometries {
if g >= nnodes {
return Err(corrupt("geometry index is out of range"));
}
}
for &f in &features {
if f >= nnodes {
return Err(corrupt("feature index is out of range"));
}
}
Ok(GeojsonModel {
class,
root,
type_node,
geometries,
features,
json,
})
}
pub fn doc_len(&self) -> u64 {
self.json.doc_len
}
pub fn root_class(&self) -> u8 {
self.class
}
}
pub type GeojsonMatch = JsonMatch;
pub fn detect(source: &[u8], limits: Limits) -> bool {
if source.len() as u64 > limits.max_geojson_document_bytes {
return false;
}
if !json::detect(source, limits) {
return false;
}
parse(source, limits, false).is_ok()
}
pub fn classify(source: &[u8], limits: Limits) -> Result<u8> {
Ok(parse(source, limits, false)?.class)
}
pub fn parse(source: &[u8], limits: Limits, build: bool) -> Result<GeojsonModel> {
if source.len() as u64 > limits.max_geojson_document_bytes {
return Err(Error::resource_limit(format!(
"GeoJSON source is {} bytes, above the {}-byte document cap",
source.len(),
limits.max_geojson_document_bytes
)));
}
let json = json::parse(source, limits, true).map_err(|e| match e.class() {
ErrorClass::ResourceLimit => e,
_ => invalid_geojson(format!(
"GeoJSON source is not a single JSON value: {}",
e.message()
)),
})?;
if json.nodes.len() as u64 > limits.max_geojson_nodes as u64 {
return Err(Error::resource_limit(format!(
"GeoJSON document exceeds the {}-node cap",
limits.max_geojson_nodes
)));
}
let (class, type_node, geometries_all, features_all) = analyze(&json, source, limits)?;
Ok(GeojsonModel {
class,
root: json.root,
type_node,
geometries: if build { geometries_all } else { Vec::new() },
features: if build { features_all } else { Vec::new() },
json,
})
}
pub fn build_geojson_model(source: &[u8], limits: Limits) -> Result<Vec<u8>> {
Ok(parse(source, limits, true)?.encode())
}
pub fn object_member(
model: &GeojsonModel,
source: &[u8],
obj: u32,
key: &str,
) -> Result<Option<u32>> {
let node = model
.node(obj)
.ok_or_else(|| corrupt("object index is out of range"))?;
if node.kind != K_OBJECT {
return Err(corrupt("node is not a JSON object"));
}
let mut i = 0usize;
while i + 1 < node.children.len() {
let k = node.children[i];
let v = node.children[i + 1];
i += 2;
let kn = model
.node(k)
.ok_or_else(|| corrupt("object key index is out of range"))?;
if kn.kind == K_STRING && json::decode_string(source, kn)? == key {
return Ok(Some(v));
}
}
Ok(None)
}
pub fn object_members(model: &GeojsonModel, source: &[u8], obj: u32) -> Result<Vec<(String, u32)>> {
let node = model
.node(obj)
.ok_or_else(|| corrupt("object index is out of range"))?;
if node.kind != K_OBJECT {
return Err(corrupt("node is not a JSON object"));
}
let mut out = Vec::with_capacity(node.children.len() / 2);
let mut i = 0usize;
while i + 1 < node.children.len() {
let k = node.children[i];
let v = node.children[i + 1];
i += 2;
let kn = model
.node(k)
.ok_or_else(|| corrupt("object key index is out of range"))?;
let key = json::decode_string(source, kn)?;
out.push((key, v));
}
Ok(out)
}
pub fn token_bytes<'a>(source: &'a [u8], node: &JNode) -> Result<&'a [u8]> {
json::token_bytes(source, node)
}
pub fn decode_string(source: &[u8], node: &JNode) -> Result<String> {
json::decode_string(source, node)
}
pub fn type_of_object(model: &GeojsonModel, source: &[u8], obj: u32) -> Result<Option<String>> {
match object_member(model, source, obj, "type")? {
Some(v) => {
let n = model
.node(v)
.ok_or_else(|| corrupt("type value index is out of range"))?;
if n.kind == K_STRING {
Ok(Some(json::decode_string(source, n)?))
} else {
Ok(None)
}
}
None => Ok(None),
}
}
pub fn node_span(model: &GeojsonModel, index: u32) -> Result<(u64, u64)> {
let n = model
.node(index)
.ok_or_else(|| corrupt("node index is out of range"))?;
Ok((n.start, n.end))
}
pub fn coordinates_node(model: &GeojsonModel, source: &[u8], geom: u32) -> Result<Option<u32>> {
object_member(model, source, geom, "coordinates")
}
pub fn geometries_node(model: &GeojsonModel, source: &[u8], geom: u32) -> Result<Option<u32>> {
object_member(model, source, geom, "geometries")
}
pub fn coordinate_numbers(model: &GeojsonModel, node: u32, limits: Limits) -> Result<Vec<u32>> {
let mut out: Vec<u32> = Vec::new();
collect_number_leaves(model, node, limits, &mut out, 1)?;
Ok(out)
}
fn collect_number_leaves(
model: &GeojsonModel,
index: u32,
limits: Limits,
out: &mut Vec<u32>,
depth: u32,
) -> Result<()> {
if depth > limits.max_geojson_depth {
return Err(Error::resource_limit(format!(
"GeoJSON coordinate nesting exceeds the {}-level cap",
limits.max_geojson_depth
)));
}
let n = model
.node(index)
.ok_or_else(|| corrupt("coordinate node index is out of range"))?;
match n.kind {
K_NUMBER => {
if out.len() as u64 >= limits.max_geojson_coordinates {
return Err(Error::resource_limit(format!(
"GeoJSON coordinates exceed the {}-number cap",
limits.max_geojson_coordinates
)));
}
out.push(index);
}
K_ARRAY | K_OBJECT => {
for &c in &n.children {
collect_number_leaves(model, c, limits, out, depth + 1)?;
}
}
_ => {}
}
Ok(())
}
pub fn foreign_members(
model: &GeojsonModel,
source: &[u8],
obj: u32,
) -> Result<Vec<(String, u32)>> {
let class = match type_of_object(model, source, obj)? {
Some(name) => class_of(&name).unwrap_or(G_FEATURE_COLLECTION),
None => G_FEATURE_COLLECTION,
};
let mut out = Vec::new();
for (key, val) in object_members(model, source, obj)? {
if !is_spec_key(class, &key) {
out.push((key, val));
}
}
Ok(out)
}
pub fn find(
model: &GeojsonModel,
source: &[u8],
pattern: &str,
limits: Limits,
) -> Result<Vec<GeojsonMatch>> {
json::find(&model.json, source, pattern, limits)
}
pub fn canonical_text(model: &GeojsonModel, source: &[u8]) -> Result<String> {
json::canonical_text(&model.json, source)
}
fn analyze(
json: &JsonModel,
source: &[u8],
limits: Limits,
) -> Result<(u8, u32, Vec<u32>, Vec<u32>)> {
let root = json.root;
let root_node = json
.node(root)
.ok_or_else(|| corrupt("root index is out of range"))?;
if root_node.kind != K_OBJECT {
return Err(invalid_geojson(
"GeoJSON root must be a JSON object with a string \"type\" member",
));
}
let type_node = object_member_json(json, source, root, "type")?
.ok_or_else(|| invalid_geojson("GeoJSON root object has no \"type\" member"))?;
let tn = json
.node(type_node)
.ok_or_else(|| corrupt("type node index is out of range"))?;
if tn.kind != K_STRING {
return Err(invalid_geojson(
"GeoJSON root \"type\" member is not a string",
));
}
let type_name = json::decode_string(source, tn)?;
let class = class_of(&type_name).ok_or_else(|| {
invalid_geojson(format!(
"GeoJSON root \"type\" {type_name:?} is not a GeoJSON type name"
))
})?;
let mut geometries: Vec<u32> = Vec::new();
let mut features: Vec<u32> = Vec::new();
match class {
G_FEATURE => {
let geom = object_member_json(json, source, root, "geometry")?
.ok_or_else(|| invalid_geojson("GeoJSON Feature has no \"geometry\" member"))?;
let gn = json
.node(geom)
.ok_or_else(|| corrupt("Feature geometry index is out of range"))?;
let props = object_member_json(json, source, root, "properties")?
.ok_or_else(|| invalid_geojson("GeoJSON Feature has no \"properties\" member"))?;
let pn = json
.node(props)
.ok_or_else(|| corrupt("Feature properties index is out of range"))?;
if pn.kind != K_OBJECT && pn.kind != K_NULL {
return Err(invalid_geojson(
"GeoJSON Feature \"properties\" must be an object or null",
));
}
features.push(root);
if gn.kind == K_OBJECT {
let gclass = geometry_class_of_object(json, source, geom)?;
collect_geometry(json, source, geom, gclass, limits, 2, &mut geometries)?;
} else if gn.kind != K_NULL {
return Err(invalid_geojson(
"GeoJSON Feature \"geometry\" must be a geometry object or null",
));
}
}
G_FEATURE_COLLECTION => {
let feats = object_member_json(json, source, root, "features")?.ok_or_else(|| {
invalid_geojson("GeoJSON FeatureCollection has no \"features\" member")
})?;
let fn_ = json
.node(feats)
.ok_or_else(|| corrupt("FeatureCollection features index is out of range"))?;
if fn_.kind != K_ARRAY {
return Err(invalid_geojson(
"GeoJSON FeatureCollection \"features\" must be an array",
));
}
for &feat in &fn_.children {
if features.len() as u64 >= limits.max_geojson_features as u64 {
return Err(Error::resource_limit(format!(
"GeoJSON FeatureCollection exceeds the {}-feature cap",
limits.max_geojson_features
)));
}
analyze_feature(json, source, feat, limits, &mut geometries, &mut features)?;
}
}
_ => {
collect_geometry(json, source, root, class, limits, 1, &mut geometries)?;
}
}
if features.len() as u64 > limits.max_geojson_features as u64 {
return Err(Error::resource_limit(format!(
"GeoJSON exceeds the {}-feature cap",
limits.max_geojson_features
)));
}
Ok((class, type_node, geometries, features))
}
fn analyze_feature(
json: &JsonModel,
source: &[u8],
feat: u32,
limits: Limits,
geometries: &mut Vec<u32>,
features: &mut Vec<u32>,
) -> Result<()> {
let n = json
.node(feat)
.ok_or_else(|| corrupt("feature index is out of range"))?;
if n.kind != K_OBJECT {
return Err(invalid_geojson(
"GeoJSON FeatureCollection element is not an object",
));
}
let tnode = object_member_json(json, source, feat, "type")?
.ok_or_else(|| invalid_geojson("GeoJSON Feature has no \"type\" member"))?;
let tval = json
.node(tnode)
.ok_or_else(|| corrupt("feature type index is out of range"))?;
if tval.kind != K_STRING || json::decode_string(source, tval)? != "Feature" {
return Err(invalid_geojson(
"GeoJSON FeatureCollection element is not a Feature",
));
}
let geom = object_member_json(json, source, feat, "geometry")?
.ok_or_else(|| invalid_geojson("GeoJSON Feature has no \"geometry\" member"))?;
let props = object_member_json(json, source, feat, "properties")?
.ok_or_else(|| invalid_geojson("GeoJSON Feature has no \"properties\" member"))?;
let gn = json
.node(geom)
.ok_or_else(|| corrupt("feature geometry index is out of range"))?;
let pn = json
.node(props)
.ok_or_else(|| corrupt("feature properties index is out of range"))?;
if pn.kind != K_OBJECT && pn.kind != K_NULL {
return Err(invalid_geojson(
"GeoJSON Feature \"properties\" must be an object or null",
));
}
features.push(feat);
if gn.kind == K_OBJECT {
let gclass = geometry_class_of_object(json, source, geom)?;
collect_geometry(json, source, geom, gclass, limits, 3, geometries)?;
} else if gn.kind != K_NULL {
return Err(invalid_geojson(
"GeoJSON Feature \"geometry\" must be a geometry object or null",
));
}
Ok(())
}
fn geometry_class_of_object(json: &JsonModel, source: &[u8], obj: u32) -> Result<u8> {
let tnode = object_member_json(json, source, obj, "type")?
.ok_or_else(|| invalid_geojson("GeoJSON geometry has no \"type\" member"))?;
let tval = json
.node(tnode)
.ok_or_else(|| corrupt("geometry type index is out of range"))?;
if tval.kind != K_STRING {
return Err(invalid_geojson("GeoJSON geometry \"type\" is not a string"));
}
let name = json::decode_string(source, tval)?;
match class_of(&name) {
Some(c) if is_geometry_class(c) => Ok(c),
_ => Err(invalid_geojson(format!(
"GeoJSON geometry \"type\" {name:?} is not a geometry type name"
))),
}
}
fn collect_geometry(
json: &JsonModel,
source: &[u8],
node: u32,
class: u8,
limits: Limits,
depth: u32,
out: &mut Vec<u32>,
) -> Result<()> {
if depth > limits.max_geojson_depth {
return Err(Error::resource_limit(format!(
"GeoJSON geometry nesting exceeds the {}-level cap",
limits.max_geojson_depth
)));
}
if out.len() as u64 >= limits.max_geojson_geometries as u64 {
return Err(Error::resource_limit(format!(
"GeoJSON exceeds the {}-geometry cap",
limits.max_geojson_geometries
)));
}
if class == G_GEOMETRYCOLLECTION {
let g = object_member_json(json, source, node, "geometries")?.ok_or_else(|| {
invalid_geojson("GeoJSON GeometryCollection has no \"geometries\" member")
})?;
let gn = json
.node(g)
.ok_or_else(|| corrupt("GeometryCollection geometries index is out of range"))?;
if gn.kind != K_ARRAY {
return Err(invalid_geojson(
"GeoJSON GeometryCollection \"geometries\" must be an array",
));
}
} else {
let c = object_member_json(json, source, node, "coordinates")?
.ok_or_else(|| invalid_geojson("GeoJSON geometry has no \"coordinates\" member"))?;
let cn = json
.node(c)
.ok_or_else(|| corrupt("geometry coordinates index is out of range"))?;
if cn.kind != K_ARRAY {
return Err(invalid_geojson(
"GeoJSON geometry \"coordinates\" must be an array",
));
}
}
out.push(node);
if class == G_GEOMETRYCOLLECTION {
let g = object_member_json(json, source, node, "geometries")?.ok_or_else(|| {
invalid_geojson("GeoJSON GeometryCollection has no \"geometries\" member")
})?;
let gn = json
.node(g)
.ok_or_else(|| corrupt("GeometryCollection geometries index is out of range"))?;
for &child in &gn.children {
let cclass = geometry_class_of_object(json, source, child)?;
collect_geometry(json, source, child, cclass, limits, depth + 1, out)?;
}
}
Ok(())
}
fn object_member_json(json: &JsonModel, source: &[u8], obj: u32, key: &str) -> Result<Option<u32>> {
let node = json
.node(obj)
.ok_or_else(|| corrupt("object index is out of range"))?;
if node.kind != K_OBJECT {
return Err(corrupt("node is not a JSON object"));
}
let mut i = 0usize;
while i + 1 < node.children.len() {
let k = node.children[i];
let v = node.children[i + 1];
i += 2;
let kn = json
.node(k)
.ok_or_else(|| corrupt("object key index is out of range"))?;
if kn.kind == K_STRING && json::decode_string(source, kn)? == key {
return Ok(Some(v));
}
}
Ok(None)
}
fn corrupt(msg: &str) -> Error {
Error::invalid_model(format!("corrupt GeoJSON model: {msg}"))
}
fn invalid_geojson(msg: impl Into<String>) -> Error {
Error::invalid_geojson_structure(msg)
}
struct Reader<'a> {
b: &'a [u8],
at: usize,
}
impl<'a> Reader<'a> {
fn new(b: &'a [u8]) -> Self {
Reader { b, at: 0 }
}
fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.at
.checked_add(n)
.ok_or_else(|| corrupt("reader overflow"))?;
let s = self
.b
.get(self.at..end)
.ok_or_else(|| corrupt("reader underrun"))?;
self.at = end;
Ok(s)
}
fn u8(&mut self) -> Result<u8> {
Ok(self.bytes(1)?[0])
}
fn u32(&mut self) -> Result<u32> {
let b = self.bytes(4)?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn u64(&mut self) -> Result<u64> {
let b = self.bytes(8)?;
Ok(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn at_end(&self) -> bool {
self.at == self.b.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
const FEATURE_COLLECTION: &[u8] = br#"{
"type": "FeatureCollection",
"bbox": [100.0, 0.0, 105.0, 1.0],
"features": [
{"type":"Feature","id":1,
"geometry":{"type":"Point","coordinates":[1.25,-2.5e1]},
"properties":{"name":"A","name":"B","n":1e3},
"vendor":"x"},
{"type":"Feature","id":2,
"geometry":{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,0]]]},
"properties":null}
],
"title": "kept"
}"#;
fn model(src: &[u8]) -> GeojsonModel {
parse(src, Limits::DEFAULT, true).unwrap()
}
#[test]
fn detects_geojson_and_rejects_json_and_junk() {
assert!(detect(FEATURE_COLLECTION, Limits::DEFAULT));
assert!(detect(
br#"{"type":"GeometryCollection","geometries":[]}"#,
Limits::DEFAULT
));
assert!(detect(
b"{\"type\":\"Point\",\"coordinates\":[0,0]}",
Limits::DEFAULT
));
assert!(!detect(b"{\"a\":1}", Limits::DEFAULT));
assert!(!detect(b"[1,2,3]", Limits::DEFAULT));
assert!(!detect(b"{\"type\":\"object\"}", Limits::DEFAULT));
assert!(!detect(b"{\"type\":\"widget\",\"x\":1}", Limits::DEFAULT));
assert!(!detect(b"{\"type\":\"Point\"}", Limits::DEFAULT));
assert!(!detect(
b"{\"type\":\"FeatureCollection\"}",
Limits::DEFAULT
));
assert!(!detect(
b"{\"type\":\"Feature\",\"geometry\":null}",
Limits::DEFAULT
));
assert!(!detect(b"the quick brown fox", Limits::DEFAULT));
assert!(!detect(b"", Limits::DEFAULT));
}
#[test]
fn root_type_and_anchors_are_preserved() {
let m = model(FEATURE_COLLECTION);
assert_eq!(m.class, G_FEATURE_COLLECTION);
assert_eq!(m.root_class(), G_FEATURE_COLLECTION);
assert_eq!(m.feature_count(), 2);
assert_eq!(m.geometry_count(), 2);
let tn = m.node(m.type_node).unwrap();
assert_eq!(
token_bytes(FEATURE_COLLECTION, tn).unwrap(),
br#""FeatureCollection""#
);
assert_eq!(
type_of_object(&m, FEATURE_COLLECTION, m.root)
.unwrap()
.unwrap(),
"FeatureCollection"
);
let f0 = m.node(m.features[0]).unwrap();
let f1 = m.node(m.features[1]).unwrap();
assert!(f0.start < f1.start);
}
#[test]
fn coordinates_preserve_spelling_and_spans() {
let m = model(FEATURE_COLLECTION);
let point = m.geometries[0];
let coords = coordinates_node(&m, FEATURE_COLLECTION, point)
.unwrap()
.unwrap();
let nums = coordinate_numbers(&m, coords, Limits::DEFAULT).unwrap();
assert_eq!(nums.len(), 2);
let t0 = token_bytes(FEATURE_COLLECTION, m.node(nums[0]).unwrap()).unwrap();
let t1 = token_bytes(FEATURE_COLLECTION, m.node(nums[1]).unwrap()).unwrap();
assert_eq!(t0, b"1.25");
assert_eq!(t1, b"-2.5e1");
}
#[test]
fn properties_keep_order_and_duplicate_keys() {
let m = model(FEATURE_COLLECTION);
let props = object_member(&m, FEATURE_COLLECTION, m.features[0], "properties")
.unwrap()
.unwrap();
let members = object_members(&m, FEATURE_COLLECTION, props).unwrap();
let keys: Vec<&str> = members.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(keys, vec!["name", "name", "n"]);
}
#[test]
fn foreign_members_are_reported_not_dropped() {
let m = model(FEATURE_COLLECTION);
let root_foreign = foreign_members(&m, FEATURE_COLLECTION, m.root).unwrap();
assert_eq!(root_foreign.len(), 1);
assert_eq!(root_foreign[0].0, "title");
let f_foreign = foreign_members(&m, FEATURE_COLLECTION, m.features[0]).unwrap();
assert_eq!(f_foreign.len(), 1);
assert_eq!(f_foreign[0].0, "vendor");
}
#[test]
fn all_seven_geometry_types_are_recognized() {
let cases: [&[u8]; 7] = [
br#"{"type":"Point","coordinates":[0,0]}"#,
br#"{"type":"MultiPoint","coordinates":[[0,0]]}"#,
br#"{"type":"LineString","coordinates":[[0,0],[1,1]]}"#,
br#"{"type":"MultiLineString","coordinates":[[[0,0],[1,1]]]}"#,
br#"{"type":"Polygon","coordinates":[[[0,0],[1,0],[1,1],[0,0]]]}"#,
br#"{"type":"MultiPolygon","coordinates":[[[[0,0],[1,0],[1,1],[0,0]]]]}"#,
br#"{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[0,0]}]}"#,
];
for (i, src) in cases.iter().enumerate() {
assert!(detect(src, Limits::DEFAULT), "case {i} not detected");
let m = model(src);
assert_eq!(m.class as usize, i);
assert_eq!(m.geometry_count(), if i == 6 { 2 } else { 1 });
}
}
#[test]
fn model_roundtrips_and_rejects_corruption() {
let m = model(FEATURE_COLLECTION);
let bytes = m.encode();
assert_eq!(GeojsonModel::decode(&bytes).unwrap(), m);
for cut in 0..bytes.len() {
let _ = GeojsonModel::decode(&bytes[..cut]);
}
let mut bad = bytes.clone();
bad[0] ^= 0xFF;
assert!(GeojsonModel::decode(&bad).is_err());
}
#[test]
fn malformed_and_inconsistent_decline_typed() {
for src in [
&b"{\"type\":\"object\"}"[..],
&b"{\"type\":\"Point\"}"[..],
&b"{\"type\":\"Feature\",\"geometry\":null}"[..],
&b"[]"[..],
] {
let e = parse(src, Limits::DEFAULT, true).unwrap_err();
assert_eq!(
e.class(),
ErrorClass::InvalidGeojsonStructure,
"src={src:?}"
);
}
let e = parse(b"{", Limits::DEFAULT, true).unwrap_err();
assert_eq!(e.class(), ErrorClass::InvalidGeojsonStructure);
}
#[test]
fn caps_decline_typed() {
let tight = Limits {
max_geojson_features: 1,
..Limits::DEFAULT
};
assert!(!detect(FEATURE_COLLECTION, tight));
let e = parse(FEATURE_COLLECTION, tight, true).unwrap_err();
assert_eq!(e.class(), ErrorClass::ResourceLimit);
let tight_bytes = Limits {
max_geojson_document_bytes: 4,
..Limits::DEFAULT
};
assert!(!detect(FEATURE_COLLECTION, tight_bytes));
let mut big = Vec::new();
big.extend_from_slice(b"{\"type\":\"LineString\",\"coordinates\":[");
for i in 0..64 {
if i > 0 {
big.push(b',');
}
big.extend_from_slice(b"[0,0]");
}
big.extend_from_slice(b"]}");
let tight_coords = Limits {
max_geojson_coordinates: 8,
..Limits::DEFAULT
};
let m = parse(&big, Limits::DEFAULT, true).unwrap();
let coords = coordinates_node(&m, &big, m.geometries[0])
.unwrap()
.unwrap();
let e = coordinate_numbers(&m, coords, tight_coords).unwrap_err();
assert_eq!(e.class(), ErrorClass::ResourceLimit);
}
#[test]
fn find_reuses_json_vocabulary() {
let m = model(FEATURE_COLLECTION);
let ms = find(&m, FEATURE_COLLECTION, "name", Limits::DEFAULT).unwrap();
assert_eq!(ms.len(), 2);
assert!(
ms.iter()
.all(|m| m.role == crate::adapter::json::MatchRole::Key)
);
}
#[test]
fn random_bytes_never_panic() {
let mut x: u64 = 0x9E37_79B9_7F4A_7C15;
for _ in 0..512 {
let mut buf = vec![0u8; 256];
for b in buf.iter_mut() {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*b = (x & 0xFF) as u8;
}
let _ = detect(&buf, Limits::STRICT);
if let Ok(m) = parse(&buf, Limits::STRICT, true) {
let _ = m.encode();
let _ = canonical_text(&m, &buf);
let _ = find(&m, &buf, "a", Limits::STRICT);
for &g in &m.geometries {
let _ = coordinates_node(&m, &buf, g);
}
}
}
}
}