use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq)]
pub enum GeoJsonError {
Json(String),
UnexpectedEof,
ExpectedType,
UnknownGeometryType(String),
MalformedCoordinates,
UnsupportedType(String),
}
impl core::fmt::Display for GeoJsonError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
GeoJsonError::Json(msg) => write!(f, "invalid JSON: {msg}"),
GeoJsonError::UnexpectedEof => f.write_str("unexpected end of input"),
GeoJsonError::ExpectedType => f.write_str("missing GeoJSON \"type\" member"),
GeoJsonError::UnknownGeometryType(s) => {
write!(f, "unknown GeoJSON geometry type {s:?}")
}
GeoJsonError::MalformedCoordinates => f.write_str("malformed or missing coordinates"),
GeoJsonError::UnsupportedType(s) => write!(f, "unsupported GeoJSON type {s:?}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for GeoJsonError {}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum JsonValue {
Null,
Bool(bool),
Number(f64),
Str(String),
Array(Vec<JsonValue>),
Position([f64; 2]),
Object(Vec<(String, JsonValue)>),
}
impl JsonValue {
pub(crate) fn as_str(&self) -> Option<&str> {
match self {
JsonValue::Str(s) => Some(s),
_ => None,
}
}
pub(crate) fn as_f64(&self) -> Option<f64> {
match self {
JsonValue::Number(n) => Some(*n),
_ => None,
}
}
pub(crate) fn as_array(&self) -> Option<&[JsonValue]> {
match self {
JsonValue::Array(a) => Some(a),
_ => None,
}
}
pub(crate) fn as_position(&self) -> Option<[f64; 2]> {
match self {
JsonValue::Position(position) => Some(*position),
_ => None,
}
}
pub(crate) fn get(&self, key: &str) -> Option<&JsonValue> {
match self {
JsonValue::Object(members) => members.iter().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
}
#[cfg(test)]
pub(crate) fn parse_json(input: &str) -> Result<JsonValue, GeoJsonError> {
parse_document(input, false)
}
pub(crate) fn parse_geojson(input: &str) -> Result<JsonValue, GeoJsonError> {
parse_document(input, true)
}
fn parse_document(input: &str, inline_positions: bool) -> Result<JsonValue, GeoJsonError> {
let mut p = JsonParser {
bytes: input.as_bytes(),
pos: 0,
inline_positions,
};
p.skip_ws();
let value = p.parse_value(0)?;
p.skip_ws();
if p.pos != p.bytes.len() {
return Err(GeoJsonError::Json(
"trailing characters after value".to_string(),
));
}
Ok(value)
}
const MAX_DEPTH: usize = 128;
struct JsonParser<'a> {
bytes: &'a [u8],
pos: usize,
inline_positions: bool,
}
impl JsonParser<'_> {
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn skip_ws(&mut self) {
while let Some(b) = self.peek() {
if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
self.pos += 1;
} else {
break;
}
}
}
fn expect_literal(&mut self, word: &str, value: JsonValue) -> Result<JsonValue, GeoJsonError> {
let w = word.as_bytes();
if self.bytes[self.pos..].starts_with(w) {
self.pos += w.len();
Ok(value)
} else {
Err(GeoJsonError::Json(alloc::format!("expected `{word}`")))
}
}
fn parse_value(&mut self, depth: usize) -> Result<JsonValue, GeoJsonError> {
match self.peek() {
None => Err(GeoJsonError::UnexpectedEof),
Some(b'{') => self.parse_object(depth),
Some(b'[') => self.parse_array(depth),
Some(b'"') => Ok(JsonValue::Str(self.parse_string()?)),
Some(b't') => self.expect_literal("true", JsonValue::Bool(true)),
Some(b'f') => self.expect_literal("false", JsonValue::Bool(false)),
Some(b'n') => self.expect_literal("null", JsonValue::Null),
Some(b) if b == b'-' || b.is_ascii_digit() => {
Ok(JsonValue::Number(self.parse_number()?))
}
Some(b) => Err(GeoJsonError::Json(alloc::format!(
"unexpected character {:?}",
b as char
))),
}
}
fn parse_object(&mut self, depth: usize) -> Result<JsonValue, GeoJsonError> {
if depth >= MAX_DEPTH {
return Err(GeoJsonError::Json("nesting too deep".to_string()));
}
self.pos += 1; let mut members = Vec::new();
self.skip_ws();
if self.peek() == Some(b'}') {
self.pos += 1;
return Ok(JsonValue::Object(members));
}
loop {
self.skip_ws();
if self.peek() != Some(b'"') {
return Err(GeoJsonError::Json("expected string key".to_string()));
}
let key = self.parse_string()?;
self.skip_ws();
if self.peek() != Some(b':') {
return Err(GeoJsonError::Json("expected `:` after key".to_string()));
}
self.pos += 1;
self.skip_ws();
let value = self.parse_value(depth + 1)?;
members.push((key, value));
self.skip_ws();
match self.peek() {
Some(b',') => self.pos += 1,
Some(b'}') => {
self.pos += 1;
return Ok(JsonValue::Object(members));
}
Some(_) => {
return Err(GeoJsonError::Json("expected `,` or `}`".to_string()));
}
None => return Err(GeoJsonError::UnexpectedEof),
}
}
}
fn parse_array(&mut self, depth: usize) -> Result<JsonValue, GeoJsonError> {
if depth >= MAX_DEPTH {
return Err(GeoJsonError::Json("nesting too deep".to_string()));
}
self.pos += 1; let mut items = Vec::new();
self.skip_ws();
if self.peek() == Some(b']') {
self.pos += 1;
return Ok(JsonValue::Array(items));
}
if self.inline_positions {
if let Some(position) = self.parse_position()? {
return Ok(JsonValue::Position(position));
}
}
loop {
self.skip_ws();
items.push(self.parse_value(depth + 1)?);
self.skip_ws();
match self.peek() {
Some(b',') => self.pos += 1,
Some(b']') => {
self.pos += 1;
return Ok(JsonValue::Array(items));
}
Some(_) => {
return Err(GeoJsonError::Json("expected `,` or `]`".to_string()));
}
None => return Err(GeoJsonError::UnexpectedEof),
}
}
}
fn parse_position(&mut self) -> Result<Option<[f64; 2]>, GeoJsonError> {
let start = self.pos;
if !matches!(self.peek(), Some(b'-' | b'0'..=b'9')) {
return Ok(None);
}
let x = self.parse_number()?;
self.skip_ws();
if self.peek() != Some(b',') {
self.pos = start;
return Ok(None);
}
self.pos += 1;
self.skip_ws();
if !matches!(self.peek(), Some(b'-' | b'0'..=b'9')) {
self.pos = start;
return Ok(None);
}
let y = self.parse_number()?;
self.skip_ws();
if self.peek() == Some(b']') {
self.pos += 1;
return Ok(Some([x, y]));
}
self.pos = start;
Ok(None)
}
fn parse_string(&mut self) -> Result<String, GeoJsonError> {
self.pos += 1; let mut out = String::new();
loop {
match self.peek() {
None => return Err(GeoJsonError::UnexpectedEof),
Some(b'"') => {
self.pos += 1;
return Ok(out);
}
Some(b'\\') => {
self.pos += 1;
match self.peek() {
None => return Err(GeoJsonError::UnexpectedEof),
Some(b'"') => out.push('"'),
Some(b'\\') => out.push('\\'),
Some(b'/') => out.push('/'),
Some(b'n') => out.push('\n'),
Some(b't') => out.push('\t'),
Some(b'r') => out.push('\r'),
Some(b'b') => out.push('\u{0008}'),
Some(b'f') => out.push('\u{000C}'),
Some(other) => {
return Err(GeoJsonError::Json(alloc::format!(
"unsupported escape \\{:?}",
other as char
)));
}
}
self.pos += 1;
}
Some(_) => {
let rest = &self.bytes[self.pos..];
let ch_len = utf8_char_len(rest[0]);
let slice = rest.get(..ch_len).ok_or(GeoJsonError::UnexpectedEof)?;
let s = core::str::from_utf8(slice).expect(
"GeoJSON input is valid UTF-8 and the cursor advances by characters",
);
out.push_str(s);
self.pos += ch_len;
}
}
}
}
fn parse_number(&mut self) -> Result<f64, GeoJsonError> {
let start = self.pos;
if self.peek() == Some(b'-') {
self.pos += 1;
}
while let Some(b) = self.peek() {
if b.is_ascii_digit() || b == b'.' || b == b'e' || b == b'E' || b == b'+' || b == b'-' {
self.pos += 1;
} else {
break;
}
}
let slice = &self.bytes[start..self.pos];
let text = core::str::from_utf8(slice)
.expect("number tokens contain only ASCII bytes copied from valid UTF-8 input");
match text.parse::<f64>() {
Ok(number) => Ok(number),
Err(_) => Err(GeoJsonError::Json(alloc::format!(
"invalid number {text:?}"
))),
}
}
}
fn utf8_char_len(first: u8) -> usize {
if first < 0x80 {
1
} else if first < 0xE0 {
2
} else if first < 0xF0 {
3
} else {
4
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::float_cmp,
reason = "number literals in these fixtures are exact"
)]
use super::{GeoJsonError, JsonValue, parse_geojson, parse_json};
use alloc::string::ToString;
#[test]
fn parses_object_with_array_and_string() {
let v = parse_json(r#"{"a":[1,2.5,-3e2],"b":"x"}"#).unwrap();
let arr = v.get("a").unwrap().as_array().unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].as_f64(), Some(1.0));
assert_eq!(arr[1].as_f64(), Some(2.5));
assert_eq!(arr[2].as_f64(), Some(-300.0));
assert_eq!(v.get("b").unwrap().as_str(), Some("x"));
}
#[test]
fn parses_deeply_nested_arrays() {
let v = parse_json("[[[1,2]],[[3,4]]]").unwrap();
let outer = v.as_array().unwrap();
assert_eq!(outer.len(), 2);
let inner = outer[1].as_array().unwrap()[0].as_array().unwrap();
assert_eq!(inner[0].as_f64(), Some(3.0));
assert_eq!(inner[1].as_f64(), Some(4.0));
}
#[test]
fn geojson_positions_are_inline_with_general_array_fallback() {
assert_eq!(
parse_geojson("[1,2]").unwrap(),
JsonValue::Position([1.0, 2.0])
);
assert!(matches!(
parse_geojson("[1,2,3]").unwrap(),
JsonValue::Array(values) if values.len() == 3
));
assert!(matches!(
parse_json("[1,2]").unwrap(),
JsonValue::Array(values) if values.len() == 2
));
}
#[test]
fn decodes_string_escapes() {
let v = parse_json(r#""a\"b\\c\/d\ne""#).unwrap();
assert_eq!(v.as_str(), Some("a\"b\\c/d\ne"));
}
#[test]
fn parses_keywords() {
assert_eq!(parse_json("true").unwrap(), JsonValue::Bool(true));
assert_eq!(parse_json("false").unwrap(), JsonValue::Bool(false));
assert_eq!(parse_json("null").unwrap(), JsonValue::Null);
}
#[test]
fn rejects_trailing_characters() {
let err = parse_json("{} junk").unwrap_err();
assert_eq!(
err,
GeoJsonError::Json("trailing characters after value".to_string())
);
}
#[test]
fn reports_eof_on_truncated_input() {
assert_eq!(
parse_json("[1, 2").unwrap_err(),
GeoJsonError::UnexpectedEof
);
}
#[test]
fn rejects_malformed_number() {
assert!(matches!(
parse_json("1.2.3").unwrap_err(),
GeoJsonError::Json(_)
));
}
#[test]
fn error_display_covers_every_variant() {
use alloc::string::String;
let cases: Vec<(GeoJsonError, &str)> = vec![
(GeoJsonError::Json("boom".to_string()), "invalid JSON: boom"),
(GeoJsonError::UnexpectedEof, "unexpected end of input"),
(
GeoJsonError::ExpectedType,
"missing GeoJSON \"type\" member",
),
(
GeoJsonError::UnknownGeometryType("Xyz".to_string()),
"unknown GeoJSON geometry type \"Xyz\"",
),
(
GeoJsonError::MalformedCoordinates,
"malformed or missing coordinates",
),
(
GeoJsonError::UnsupportedType("Feature".to_string()),
"unsupported GeoJSON type \"Feature\"",
),
];
for (err, want) in cases {
let rendered: String = alloc::format!("{err}");
assert_eq!(rendered, want);
}
}
#[test]
fn accessors_return_none_on_mismatch() {
let n = JsonValue::Number(1.0);
assert_eq!(n.as_str(), None);
assert_eq!(n.as_array(), None);
assert_eq!(n.get("k"), None); let s = JsonValue::Bool(true);
assert_eq!(s.as_f64(), None);
}
#[test]
fn truncated_literal_is_reported() {
let err = parse_json("tru").unwrap_err();
assert_eq!(err, GeoJsonError::Json("expected `true`".to_string()));
}
#[test]
fn object_structure_errors_are_reported() {
assert_eq!(
parse_json("{1: 2}").unwrap_err(),
GeoJsonError::Json("expected string key".to_string())
);
assert_eq!(
parse_json(r#"{"a" 2}"#).unwrap_err(),
GeoJsonError::Json("expected `:` after key".to_string())
);
assert_eq!(
parse_json(r#"{"a": 1 "b": 2}"#).unwrap_err(),
GeoJsonError::Json("expected `,` or `}`".to_string())
);
}
#[test]
fn array_separator_error_is_reported() {
assert_eq!(
parse_json("[1 2]").unwrap_err(),
GeoJsonError::Json("expected `,` or `]`".to_string())
);
}
#[test]
fn control_escapes_decode_and_bad_escape_rejected() {
let v = parse_json(r#""a\rb\bc\fd""#).unwrap();
assert_eq!(v.as_str(), Some("a\rb\u{0008}c\u{000C}d"));
let err = parse_json(r#""\x""#).unwrap_err();
assert!(
matches!(&err, GeoJsonError::Json(m) if m.contains("unsupported escape")),
"got {err:?}"
);
}
#[test]
fn multibyte_utf8_in_string_survives() {
let v = parse_json("\"é€𝄞\"").unwrap();
assert_eq!(v.as_str(), Some("é€𝄞"));
}
#[test]
fn rejects_deeply_nested_input_without_overflow() {
let deep_arrays = "[".repeat(100_000);
assert!(matches!(
parse_json(&deep_arrays).unwrap_err(),
GeoJsonError::Json(_)
));
let mut deep_objects = String::new();
for _ in 0..100_000 {
deep_objects.push_str("{\"a\":");
}
assert!(matches!(
parse_json(&deep_objects).unwrap_err(),
GeoJsonError::Json(_)
));
let ok = "[".repeat(64) + "1" + &"]".repeat(64);
assert!(parse_json(&ok).is_ok());
}
}