use crate::{Error, Result};
use serde_json::{Map, Value};
use std::io::{BufRead, BufReader, Read};
fn bad(msg: impl std::fmt::Display) -> Error {
Error::InvalidData(format!("GeoJSON: {msg}"))
}
struct Scanner<R: Read> {
r: BufReader<R>,
}
impl<R: Read> Scanner<R> {
fn skip_ws(&mut self) -> Result<Option<u8>> {
loop {
let buf = self.r.fill_buf()?;
if buf.is_empty() {
return Ok(None);
}
match buf.iter().position(|b| !(b.is_ascii_whitespace() || matches!(b, 0xEF | 0xBB | 0xBF))) {
Some(i) => {
let b = buf[i];
self.r.consume(i);
return Ok(Some(b));
}
None => {
let n = buf.len();
self.r.consume(n);
}
}
}
}
fn bump(&mut self) {
self.r.consume(1);
}
fn value(&mut self, out: &mut Vec<u8>) -> Result<()> {
let first = self.skip_ws()?.ok_or_else(|| bad("unexpected end of file"))?;
let structured = matches!(first, b'{' | b'[' | b'"');
let mut depth = 0usize;
let mut in_string = false;
let mut escaped = false;
let mut started = false;
loop {
let buf = self.r.fill_buf()?;
if buf.is_empty() {
if !structured && started {
return Ok(());
}
return Err(bad("unexpected end of file"));
}
let mut end = None;
for (i, &b) in buf.iter().enumerate() {
if !structured {
if matches!(b, b',' | b'}' | b']') || b.is_ascii_whitespace() {
end = Some((i, false));
break;
}
started = true;
continue;
}
if in_string {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'"' {
in_string = false;
if depth == 0 {
end = Some((i + 1, true));
break;
}
}
continue;
}
match b {
b'"' => in_string = true,
b'{' | b'[' => depth += 1,
b'}' | b']' => {
depth = depth.checked_sub(1).ok_or_else(|| bad("unbalanced brackets"))?;
if depth == 0 {
end = Some((i + 1, true));
break;
}
}
_ => {}
}
}
match end {
Some((n, _)) => {
out.extend_from_slice(&buf[..n]);
self.r.consume(n);
return Ok(());
}
None => {
let n = buf.len();
out.extend_from_slice(buf);
self.r.consume(n);
}
}
}
}
}
#[derive(PartialEq)]
enum State {
Members,
Features,
Done,
}
pub(crate) struct Collection<R: Read> {
s: Scanner<R>,
state: State,
pub members: Map<String, Value>,
pub has_features: bool,
}
impl<R: Read> Collection<R> {
pub(crate) fn new(r: R) -> Result<Self> {
let mut s = Scanner { r: BufReader::with_capacity(1 << 20, r) };
match s.skip_ws()? {
Some(b'{') => s.bump(),
_ => return Err(bad("the document is not a JSON object")),
}
Ok(Self { s, state: State::Members, members: Map::new(), has_features: false })
}
pub(crate) fn next_feature(&mut self, out: &mut Vec<u8>) -> Result<bool> {
loop {
match self.state {
State::Done => return Ok(false),
State::Members => match self.s.skip_ws()? {
None => return Err(bad("unexpected end of file")),
Some(b'}') => {
self.s.bump();
self.state = State::Done;
}
Some(b',') => self.s.bump(),
Some(_) => {
let mut raw = Vec::new();
self.s.value(&mut raw)?;
let key: String = serde_json::from_slice(&raw).map_err(bad)?;
if self.s.skip_ws()? != Some(b':') {
return Err(bad("expected ':'"));
}
self.s.bump();
if key == "features" && self.s.skip_ws()? == Some(b'[') {
self.s.bump();
self.has_features = true;
self.state = State::Features;
continue;
}
raw.clear();
self.s.value(&mut raw)?;
let v: Value = serde_json::from_slice(&raw).map_err(bad)?;
self.members.insert(key, v);
}
},
State::Features => match self.s.skip_ws()? {
None => return Err(bad("unexpected end of file")),
Some(b']') => {
self.s.bump();
self.state = State::Members;
}
Some(b',') => self.s.bump(),
Some(_) => {
out.clear();
self.s.value(out)?;
return Ok(true);
}
},
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scans_features_and_members() {
let text = br#"{"type":"FeatureCollection","features":[ {"type":"Feature","properties":{"a":"x]}\"y"},"geometry":null} ,
{"type":"Feature","properties":{"b":[1,{"c":2}]},"geometry":{"type":"Point","coordinates":[1,2]}}],"name":"n","crs":null}"#;
let mut c = Collection::new(&text[..]).unwrap();
let mut out = Vec::new();
let mut features = Vec::new();
while c.next_feature(&mut out).unwrap() {
features.push(serde_json::from_slice::<Value>(&out).unwrap());
}
assert_eq!(features.len(), 2);
assert_eq!(features[0]["properties"]["a"], "x]}\"y");
assert_eq!(features[1]["properties"]["b"][1]["c"], 2);
assert_eq!(c.members["name"], "n");
assert_eq!(c.members["type"], "FeatureCollection");
assert!(c.members["crs"].is_null());
assert!(c.has_features);
}
}