use core::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<Value>),
Object(Vec<(String, Value)>),
}
pub(crate) static NULL: Value = Value::Null;
impl Value {
#[must_use]
pub fn get(&self, key: &str) -> &Value {
match self {
Value::Object(entries) => entries
.iter()
.find(|(k, _)| k == key)
.map_or(&NULL, |(_, v)| v),
_ => &NULL,
}
}
#[must_use]
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
#[must_use]
pub fn as_f64(&self) -> Option<f64> {
match self {
Value::Number(n) => Some(*n),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&[Value]> {
match self {
Value::Array(items) => Some(items),
_ => None,
}
}
#[must_use]
pub fn as_object(&self) -> Option<&[(String, Value)]> {
match self {
Value::Object(entries) => Some(entries),
_ => None,
}
}
}
impl core::ops::Index<&str> for Value {
type Output = Value;
fn index(&self, key: &str) -> &Value {
self.get(key)
}
}
impl core::ops::Index<usize> for Value {
type Output = Value;
fn index(&self, index: usize) -> &Value {
match self {
Value::Array(items) => items.get(index).unwrap_or(&NULL),
_ => &NULL,
}
}
}
fn write_json_string(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
write!(f, "\"")?;
for c in s.chars() {
match c {
'"' => write!(f, "\\\"")?,
'\\' => write!(f, "\\\\")?,
'\n' => write!(f, "\\n")?,
'\r' => write!(f, "\\r")?,
'\t' => write!(f, "\\t")?,
'\u{08}' => write!(f, "\\b")?,
'\u{0C}' => write!(f, "\\f")?,
c if c < '\u{20}' => write!(f, "\\u{:04x}", c as u32)?,
c => write!(f, "{c}")?,
}
}
write!(f, "\"")
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Null => write!(f, "null"),
Value::Bool(b) => write!(f, "{b}"),
Value::Number(n) => {
if !n.is_finite() {
return write!(f, "null");
}
if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n < i64::MAX as f64 {
write!(f, "{}", *n as i64)
} else {
write!(f, "{n}")
}
}
Value::String(s) => write_json_string(f, s),
Value::Array(items) => {
write!(f, "[")?;
for (i, item) in items.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
write!(f, "{item}")?;
}
write!(f, "]")
}
Value::Object(entries) => {
write!(f, "{{")?;
for (i, (k, v)) in entries.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
write_json_string(f, k)?;
write!(f, ":{v}")?;
}
write!(f, "}}")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub message: String,
pub line: usize,
pub column: usize,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"RRJSON parse error at line {} column {}: {}",
self.line, self.column, self.message
)
}
}
impl std::error::Error for ParseError {}
#[derive(Debug)]
pub struct ParseResult {
pub value: Value,
pub warnings: Vec<String>,
}
#[must_use = "parse errors should not be silently discarded"]
pub fn parse_rrjson(input: &str) -> Result<Value, ParseError> {
parse_rrjson_with_warnings(input).map(|r| r.value)
}
#[must_use = "parse errors should not be silently discarded"]
pub fn parse_rrjson_with_warnings(input: &str) -> Result<ParseResult, ParseError> {
let mut parser = Parser::new(input);
parser.skip_ws_and_comments()?;
let value = if parser.peek() != Some('{') && parser.peek() != Some('[') {
parser.parse_bare_object()?
} else {
let value = parser.parse_value()?;
parser.skip_ws_and_comments()?;
if parser.pos < parser.input.len() {
return Err(parser.error("unexpected content after value"));
}
value
};
Ok(ParseResult {
value,
warnings: parser.warnings,
})
}
const MAX_NESTING_DEPTH: usize = 64;
const MAX_ENTRIES: usize = 10_000;
struct Parser<'a> {
input: &'a str,
pos: usize,
depth: usize,
warnings: Vec<String>,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Self {
input,
pos: 0,
depth: 0,
warnings: Vec::new(),
}
}
fn error(&self, message: &str) -> ParseError {
let (line, column) = self.line_col();
ParseError {
message: message.to_string(),
line,
column,
}
}
fn line_col(&self) -> (usize, usize) {
let consumed = &self.input[..self.pos];
let line = consumed.matches('\n').count() + 1;
let col = consumed.rfind('\n').map_or(self.pos, |i| self.pos - i - 1) + 1;
(line, col)
}
fn peek(&self) -> Option<char> {
self.input[self.pos..].chars().next()
}
fn advance(&mut self) -> Option<char> {
let ch = self.peek()?;
self.pos += ch.len_utf8();
Some(ch)
}
fn expect(&mut self, expected: char) -> Result<(), ParseError> {
match self.advance() {
Some(c) if c == expected => Ok(()),
Some(c) => Err(self.error(&format!("expected '{expected}', found '{c}'"))),
None => Err(self.error(&format!("expected '{expected}', found end of input"))),
}
}
fn skip_ws_and_comments(&mut self) -> Result<(), ParseError> {
loop {
while self.pos < self.input.len() {
let ch = self.input.as_bytes()[self.pos];
if ch == b' ' || ch == b'\t' || ch == b'\n' || ch == b'\r' {
self.pos += 1;
} else {
break;
}
}
if self.input[self.pos..].starts_with("//") {
if let Some(end) = self.input[self.pos..].find('\n') {
self.pos += end + 1;
continue;
} else {
self.pos = self.input.len();
break;
}
}
if self.input[self.pos..].starts_with("/*") {
let open_pos = self.pos;
self.pos += 2;
if let Some(end) = self.input[self.pos..].find("*/") {
self.pos += end + 2;
continue;
} else {
self.pos = open_pos;
return Err(self.error("unterminated block comment"));
}
}
if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b'#' {
if let Some(end) = self.input[self.pos..].find('\n') {
self.pos += end + 1;
continue;
} else {
self.pos = self.input.len();
break;
}
}
break;
}
Ok(())
}
fn parse_value(&mut self) -> Result<Value, ParseError> {
self.skip_ws_and_comments()?;
match self.peek() {
Some('{') => self.parse_object(),
Some('[') => self.parse_array(),
Some('"') => self.parse_string().map(Value::String),
Some('\'') => self.parse_single_quoted_string().map(Value::String),
Some('t') | Some('f') => self.parse_bool(),
Some('n') => self.parse_null(),
Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
Some(c) => Err(self.error(&format!("unexpected character '{c}'"))),
None => Err(self.error("unexpected end of input")),
}
}
fn parse_object(&mut self) -> Result<Value, ParseError> {
if self.depth >= MAX_NESTING_DEPTH {
return Err(self.error("maximum nesting depth exceeded"));
}
self.depth += 1;
self.expect('{')?;
let mut entries = Vec::new();
self.skip_ws_and_comments()?;
if self.peek() == Some('}') {
self.advance();
self.depth -= 1;
return Ok(Value::Object(entries));
}
loop {
self.skip_ws_and_comments()?;
if self.peek() == Some('}') {
break;
}
let key = self.parse_key()?;
self.skip_ws_and_comments()?;
match self.peek() {
Some(':') | Some('=') => {
self.advance();
}
_ => return Err(self.error("expected ':' or '=' after object key")),
}
let value = self.parse_value()?;
let dot_segments = key.chars().filter(|&c| c == '.').count();
if dot_segments >= MAX_NESTING_DEPTH {
return Err(self.error("dotted key exceeds maximum nesting depth"));
}
self.validate_dotted_key(&key)?;
self.insert_dotted_key(&mut entries, &key, value);
if entries.len() > MAX_ENTRIES {
return Err(self.error("maximum object entry count exceeded"));
}
self.skip_ws_and_comments()?;
match self.peek() {
Some(',') => {
self.advance();
}
Some('}') => {}
_ => return Err(self.error("expected ',' or '}' in object")),
}
}
self.expect('}')?;
self.depth -= 1;
Ok(Value::Object(entries))
}
fn parse_array(&mut self) -> Result<Value, ParseError> {
if self.depth >= MAX_NESTING_DEPTH {
return Err(self.error("maximum nesting depth exceeded"));
}
self.depth += 1;
self.expect('[')?;
let mut items = Vec::new();
self.skip_ws_and_comments()?;
if self.peek() == Some(']') {
self.advance();
self.depth -= 1;
return Ok(Value::Array(items));
}
loop {
self.skip_ws_and_comments()?;
if self.peek() == Some(']') {
break;
}
items.push(self.parse_value()?);
if items.len() > MAX_ENTRIES {
return Err(self.error("maximum array entry count exceeded"));
}
self.skip_ws_and_comments()?;
match self.peek() {
Some(',') => {
self.advance();
}
Some(']') => {}
_ => return Err(self.error("expected ',' or ']' in array")),
}
}
self.expect(']')?;
self.depth -= 1;
Ok(Value::Array(items))
}
fn parse_key(&mut self) -> Result<String, ParseError> {
match self.peek() {
Some('"') => self.parse_string(),
Some('\'') => self.parse_single_quoted_string(),
Some(c) if c.is_ascii_alphabetic() || c == '_' => self.parse_unquoted_key(),
_ => Err(self.error("expected object key")),
}
}
fn parse_unquoted_key(&mut self) -> Result<String, ParseError> {
let start = self.pos;
while let Some(c) = self.peek() {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' {
self.advance();
} else {
break;
}
}
if self.pos == start {
return Err(self.error("expected key"));
}
Ok(self.input[start..self.pos].to_string())
}
fn parse_string(&mut self) -> Result<String, ParseError> {
self.expect('"')?;
let mut s = String::new();
loop {
match self.advance() {
Some('\\') => {
s.push(self.parse_escape()?);
}
Some('"') => return Ok(s),
Some(c) => s.push(c),
None => return Err(self.error("unterminated string")),
}
}
}
fn parse_single_quoted_string(&mut self) -> Result<String, ParseError> {
self.expect('\'')?;
let mut s = String::new();
loop {
match self.advance() {
Some('\\') => {
s.push(self.parse_escape()?);
}
Some('\'') => return Ok(s),
Some(c) => s.push(c),
None => return Err(self.error("unterminated string")),
}
}
}
fn parse_escape(&mut self) -> Result<char, ParseError> {
match self.advance() {
Some('"') => Ok('"'),
Some('\'') => Ok('\''),
Some('\\') => Ok('\\'),
Some('/') => Ok('/'),
Some('n') => Ok('\n'),
Some('r') => Ok('\r'),
Some('t') => Ok('\t'),
Some('b') => Ok('\u{08}'),
Some('f') => Ok('\u{0C}'),
Some('u') => self.parse_unicode_escape(),
Some(c) => Err(self.error(&format!("invalid escape character '{c}'"))),
None => Err(self.error("unterminated escape sequence")),
}
}
fn parse_unicode_escape(&mut self) -> Result<char, ParseError> {
let high = self.parse_hex4()?;
if (0xD800..=0xDBFF).contains(&high) {
if self.peek() != Some('\\') {
return Err(self.error("expected low surrogate pair after high surrogate"));
}
self.advance(); if self.peek() != Some('u') {
return Err(self.error("expected low surrogate pair after high surrogate"));
}
self.advance();
let low = self.parse_hex4()?;
if !(0xDC00..=0xDFFF).contains(&low) {
return Err(self.error(&format!(
"invalid low surrogate U+{low:04X}, expected U+DC00..U+DFFF"
)));
}
let code_point = 0x10000 + ((high as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
char::from_u32(code_point).ok_or_else(|| {
self.error(&format!("invalid Unicode code point U+{code_point:04X}"))
})
} else if (0xDC00..=0xDFFF).contains(&high) {
Err(self.error(&format!(
"unexpected low surrogate U+{high:04X} without preceding high surrogate"
)))
} else {
char::from_u32(high as u32)
.ok_or_else(|| self.error(&format!("invalid Unicode code point U+{high:04X}")))
}
}
fn parse_hex4(&mut self) -> Result<u16, ParseError> {
let mut value: u16 = 0;
for i in 0..4 {
match self.advance() {
Some(c) if c.is_ascii_hexdigit() => {
let digit = c.to_digit(16).expect("validated by is_ascii_hexdigit") as u16;
value = value << 4 | digit;
}
Some(c) => {
return Err(self.error(&format!(
"invalid hex digit '{c}' in Unicode escape (digit {}/4)",
i + 1
)));
}
None => {
return Err(self.error("unterminated Unicode escape sequence"));
}
}
}
Ok(value)
}
fn parse_number(&mut self) -> Result<Value, ParseError> {
let start = self.pos;
if self.peek() == Some('-') {
self.advance();
}
let integer_start = self.pos;
while let Some(c) = self.peek() {
if c.is_ascii_digit() {
self.advance();
} else {
break;
}
}
let has_integer_digits = self.pos > integer_start;
let mut has_fraction_digits = false;
if self.peek() == Some('.') {
self.advance();
let frac_start = self.pos;
while let Some(c) = self.peek() {
if c.is_ascii_digit() {
self.advance();
} else {
break;
}
}
has_fraction_digits = self.pos > frac_start;
}
if !has_integer_digits && !has_fraction_digits {
let num_str = &self.input[start..self.pos];
return Err(self.error(&format!("expected digit after minus sign: {num_str}")));
}
if self.peek() == Some('e') || self.peek() == Some('E') {
self.advance();
if self.peek() == Some('+') || self.peek() == Some('-') {
self.advance();
}
while let Some(c) = self.peek() {
if c.is_ascii_digit() {
self.advance();
} else {
break;
}
}
}
let num_str = &self.input[start..self.pos];
let n: f64 = num_str
.parse()
.map_err(|_| self.error(&format!("invalid number: {num_str}")))?;
if !n.is_finite() {
return Err(self.error(&format!("number out of range: {num_str}")));
}
Ok(Value::Number(n))
}
fn is_word_char_at(&self, offset: usize) -> bool {
self.input[self.pos + offset..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn parse_bool(&mut self) -> Result<Value, ParseError> {
if self.input[self.pos..].starts_with("true") && !self.is_word_char_at(4) {
self.pos += 4;
Ok(Value::Bool(true))
} else if self.input[self.pos..].starts_with("false") && !self.is_word_char_at(5) {
self.pos += 5;
Ok(Value::Bool(false))
} else {
Err(self.error("expected 'true' or 'false'"))
}
}
fn parse_null(&mut self) -> Result<Value, ParseError> {
if self.input[self.pos..].starts_with("null") && !self.is_word_char_at(4) {
self.pos += 4;
Ok(Value::Null)
} else {
Err(self.error("expected 'null'"))
}
}
fn parse_bare_object(&mut self) -> Result<Value, ParseError> {
let mut entries = Vec::new();
loop {
self.skip_ws_and_comments()?;
if self.pos >= self.input.len() {
break;
}
if self.input[self.pos..].starts_with("include")
&& self
.input
.as_bytes()
.get(self.pos + 7)
.is_none_or(|&b| b == b' ' || b == b'\t' || b == b'"' || b == b'\'')
{
let rest_of_line = self.input[self.pos..].split('\n').next().unwrap_or("");
let has_separator = rest_of_line.contains(':') || rest_of_line.contains('=');
if !has_separator {
let (line, col) = self.line_col();
let directive_line = if let Some(end) = self.input[self.pos..].find('\n') {
let line_text = self.input[self.pos..self.pos + end].trim();
self.pos += end + 1;
line_text.to_string()
} else {
let line_text = self.input[self.pos..].trim().to_string();
self.pos = self.input.len();
line_text
};
self.warnings.push(format!(
"RRJSON include directives are not supported (line {line} column {col}): {directive_line}"
));
continue;
}
}
let key = self.parse_key()?;
self.skip_ws_and_comments()?;
match self.peek() {
Some(':') | Some('=') => {
self.advance();
}
_ => return Err(self.error("expected ':' or '=' after key")),
}
let value = self.parse_value()?;
let dot_segments = key.chars().filter(|&c| c == '.').count();
if dot_segments >= MAX_NESTING_DEPTH {
return Err(self.error("dotted key exceeds maximum nesting depth"));
}
self.validate_dotted_key(&key)?;
self.insert_dotted_key(&mut entries, &key, value);
if entries.len() > MAX_ENTRIES {
return Err(self.error("maximum object entry count exceeded"));
}
self.skip_ws_and_comments()?;
if self.peek() == Some(',') || self.peek() == Some(';') {
self.advance();
}
}
Ok(Value::Object(entries))
}
fn validate_dotted_key(&self, key: &str) -> Result<(), ParseError> {
if key.contains('.') {
for segment in key.split('.') {
if segment.is_empty() {
return Err(self.error(&format!("dotted key has empty segment: \"{key}\"")));
}
}
}
Ok(())
}
fn insert_dotted_key(&self, entries: &mut Vec<(String, Value)>, key: &str, value: Value) {
if let Some(dot_pos) = key.find('.') {
let first = &key[..dot_pos];
let rest = &key[dot_pos + 1..];
let existing = entries.iter_mut().find(|(k, _)| k == first);
match existing {
Some((_, Value::Object(inner))) => {
self.insert_dotted_key(inner, rest, value);
}
Some((_, existing_val)) => {
let mut inner = Vec::new();
self.insert_dotted_key(&mut inner, rest, value);
*existing_val = Value::Object(inner);
}
None => {
let mut inner = Vec::new();
self.insert_dotted_key(&mut inner, rest, value);
entries.push((first.to_string(), Value::Object(inner)));
}
}
} else {
if let Some((_, existing_val)) = entries.iter_mut().find(|(k, _)| k == key) {
*existing_val = value;
} else {
entries.push((key.to_string(), value));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_object() {
let v = parse_rrjson("{}").unwrap();
assert_eq!(v, Value::Object(vec![]));
}
#[test]
fn test_empty_array() {
let v = parse_rrjson("[]").unwrap();
assert_eq!(v, Value::Array(vec![]));
}
#[test]
fn test_string_value() {
let v = parse_rrjson(r#"{"key": "value"}"#).unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_number_value() {
let v = parse_rrjson(r#"{"n": 42}"#).unwrap();
assert_eq!(v["n"], Value::Number(42.0));
}
#[test]
fn test_float_value() {
let v = parse_rrjson(r#"{"n": 2.75}"#).unwrap();
assert_eq!(v["n"], Value::Number(2.75));
}
#[test]
fn test_negative_number() {
let v = parse_rrjson(r#"{"n": -5}"#).unwrap();
assert_eq!(v["n"], Value::Number(-5.0));
}
#[test]
fn test_bool_values() {
let v = parse_rrjson(r#"{"a": true, "b": false}"#).unwrap();
assert_eq!(v["a"], Value::Bool(true));
assert_eq!(v["b"], Value::Bool(false));
}
#[test]
fn test_null_value() {
let v = parse_rrjson(r#"{"x": null}"#).unwrap();
assert_eq!(v["x"], Value::Null);
}
#[test]
fn test_bool_word_boundary() {
assert!(parse_rrjson(r#"{"a": truex}"#).is_err());
assert!(parse_rrjson(r#"{"a": falsehood}"#).is_err());
assert!(parse_rrjson(r#"{"a": true_val}"#).is_err());
assert!(parse_rrjson(r#"{"a": false0}"#).is_err());
}
#[test]
fn test_null_word_boundary() {
assert!(parse_rrjson(r#"{"a": nullify}"#).is_err());
assert!(parse_rrjson(r#"{"a": null_val}"#).is_err());
assert!(parse_rrjson(r#"{"a": null0}"#).is_err());
}
#[test]
fn test_nested_object() {
let v = parse_rrjson(r#"{"a": {"b": 1}}"#).unwrap();
assert_eq!(v["a"]["b"], Value::Number(1.0));
}
#[test]
fn test_array_values() {
let v = parse_rrjson(r#"{"a": [1, 2, 3]}"#).unwrap();
assert_eq!(
v["a"],
Value::Array(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0)
])
);
}
#[test]
fn test_escape_sequences() {
let v = parse_rrjson(r#"{"s": "a\nb\tc"}"#).unwrap();
assert_eq!(v["s"], Value::String("a\nb\tc".to_string()));
}
#[test]
fn test_unicode_escape_basic_bmp() {
let v = parse_rrjson(r#"{"s": "\u00E9"}"#).unwrap();
assert_eq!(v["s"], Value::String("é".to_string()));
}
#[test]
fn test_unicode_escape_lowercase_hex() {
let v = parse_rrjson(r#"{"s": "\u00e9"}"#).unwrap();
assert_eq!(v["s"], Value::String("é".to_string()));
}
#[test]
fn test_unicode_escape_ascii_range() {
let v = parse_rrjson(r#"{"s": "\u0041\u0042\u0043"}"#).unwrap();
assert_eq!(v["s"], Value::String("ABC".to_string()));
}
#[test]
fn test_unicode_escape_cjk() {
let v = parse_rrjson(r#"{"s": "\u4E16\u754C"}"#).unwrap();
assert_eq!(v["s"], Value::String("世界".to_string()));
}
#[test]
fn test_unicode_escape_surrogate_pair() {
let v = parse_rrjson(r#"{"s": "\uD83D\uDE00"}"#).unwrap();
assert_eq!(v["s"], Value::String("😀".to_string()));
}
#[test]
fn test_unicode_escape_surrogate_pair_musical_symbol() {
let v = parse_rrjson(r#"{"s": "\uD834\uDD1E"}"#).unwrap();
assert_eq!(v["s"], Value::String("𝄞".to_string()));
}
#[test]
fn test_unicode_escape_mixed_with_text() {
let v = parse_rrjson(r#"{"s": "caf\u00E9 \u2603 snow"}"#).unwrap();
assert_eq!(v["s"], Value::String("café ☃ snow".to_string()));
}
#[test]
fn test_unicode_escape_null_char() {
let v = parse_rrjson(r#"{"s": "\u0000"}"#).unwrap();
assert_eq!(v["s"], Value::String("\0".to_string()));
}
#[test]
fn test_unicode_escape_in_single_quoted_string() {
let v = parse_rrjson(r"{'s': '\u00E9'}").unwrap();
assert_eq!(v["s"], Value::String("é".to_string()));
}
#[test]
fn test_unicode_escape_incomplete_hex() {
let result = parse_rrjson(r#"{"s": "\u00E"}"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("hex digit"),
"expected hex digit error, got: {err}"
);
}
#[test]
fn test_unicode_escape_non_hex_chars() {
let result = parse_rrjson(r#"{"s": "\u00GG"}"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("hex digit"),
"expected hex digit error, got: {err}"
);
}
#[test]
fn test_unicode_escape_truncated() {
let result = parse_rrjson(r#"{"s": "\u00"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("unterminated"),
"expected unterminated error, got: {err}"
);
}
#[test]
fn test_unicode_escape_lone_high_surrogate() {
let result = parse_rrjson(r#"{"s": "\uD83D"}"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("surrogate"),
"expected surrogate error, got: {err}"
);
}
#[test]
fn test_unicode_escape_lone_low_surrogate() {
let result = parse_rrjson(r#"{"s": "\uDE00"}"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("surrogate"),
"expected surrogate error, got: {err}"
);
}
#[test]
fn test_unicode_escape_invalid_low_surrogate() {
let result = parse_rrjson(r#"{"s": "\uD83D\u0041"}"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("surrogate"),
"expected surrogate error, got: {err}"
);
}
#[test]
fn test_line_comments() {
let input = "{\n// comment\n\"key\": \"value\"\n}";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_block_comments() {
let input = "{/* comment */\"key\": \"value\"}";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_unterminated_block_comment() {
let input = "{/* this comment never closes\n\"key\": \"value\"}";
let err = parse_rrjson(input).unwrap_err();
assert!(
err.message.contains("unterminated block comment"),
"expected unterminated block comment error, got: {err}"
);
assert_eq!(err.line, 1);
assert_eq!(err.column, 2);
}
#[test]
fn test_unterminated_block_comment_at_eof() {
let err = parse_rrjson("/*").unwrap_err();
assert!(err.message.contains("unterminated block comment"));
}
#[test]
fn test_hash_comments() {
let input = "{\n# comment\n\"key\": \"value\"\n}";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_trailing_comma_object() {
let v = parse_rrjson(r#"{"a": 1, "b": 2,}"#).unwrap();
assert_eq!(v["a"], Value::Number(1.0));
assert_eq!(v["b"], Value::Number(2.0));
}
#[test]
fn test_trailing_comma_array() {
let v = parse_rrjson("[1, 2, 3,]").unwrap();
assert_eq!(
v,
Value::Array(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0)
])
);
}
#[test]
fn test_unquoted_keys() {
let v = parse_rrjson("{key: \"value\"}").unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_single_quoted_strings() {
let v = parse_rrjson("{'key': 'value'}").unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_equals_separator() {
let v = parse_rrjson("{key = \"value\"}").unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_bare_key_value_pairs() {
let input = "key = \"value\"\nnum = 42";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
assert_eq!(v["num"], Value::Number(42.0));
}
#[test]
fn test_dot_separated_keys() {
let input = r#"{"pdf.chorus.indent": 20}"#;
let v = parse_rrjson(input).unwrap();
assert_eq!(v["pdf"]["chorus"]["indent"], Value::Number(20.0));
}
#[test]
fn test_dot_keys_bare_format() {
let input = "pdf.chorus.indent = 20\npdf.chorus.bar = 10";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["pdf"]["chorus"]["indent"], Value::Number(20.0));
assert_eq!(v["pdf"]["chorus"]["bar"], Value::Number(10.0));
}
#[test]
fn test_dotted_key_overwrites_scalar() {
let input = "a = 1\na.b = 2";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["a"]["b"], Value::Number(2.0));
}
#[test]
fn test_scalar_overwrites_dotted_key() {
let input = "a.b = 1\na = 2";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["a"], Value::Number(2.0));
}
#[test]
fn test_dotted_key_merges_siblings() {
let input = "a.b = 1\na.c = 2";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["a"]["b"], Value::Number(1.0));
assert_eq!(v["a"]["c"], Value::Number(2.0));
}
#[test]
fn test_dotted_key_deep_overwrite() {
let input = "a.b.c = 1\na.b = 2";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["a"]["b"], Value::Number(2.0));
}
#[test]
fn test_include_directive_skipped() {
let input = "include \"base.json\"\nkey = \"value\"";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_include_directive_single_quoted() {
let input = "include 'base.json'\nkey = \"value\"";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["key"], Value::String("value".to_string()));
}
#[test]
fn test_include_at_eof() {
let input = "include \"base.json\"";
let v = parse_rrjson(input).unwrap();
assert_eq!(v, Value::Object(vec![]));
}
#[test]
fn test_includes_key_not_treated_as_include() {
let input = "includes = \"base.json\"";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["includes"], Value::String("base.json".to_string()));
}
#[test]
fn test_include_path_key_not_treated_as_include() {
let input = "include_path = \"/etc/chordpro\"";
let v = parse_rrjson(input).unwrap();
assert_eq!(
v["include_path"],
Value::String("/etc/chordpro".to_string())
);
}
#[test]
fn test_included_key_not_treated_as_include() {
let input = "included = true";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["included"], Value::Bool(true));
}
#[test]
fn test_include_key_with_colon_separator() {
let input = "include : \"somefile.json\"";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["include"], Value::String("somefile.json".to_string()));
}
#[test]
fn test_include_key_with_equals_separator() {
let input = "include = \"somefile.json\"";
let v = parse_rrjson(input).unwrap();
assert_eq!(v["include"], Value::String("somefile.json".to_string()));
}
#[test]
fn test_include_directive_produces_warning() {
let input = "include \"base.json\"\nkey = \"value\"";
let result = parse_rrjson_with_warnings(input).unwrap();
assert_eq!(result.value["key"], Value::String("value".to_string()));
assert_eq!(result.warnings.len(), 1);
assert!(result.warnings[0].contains("include directives are not supported"));
}
#[test]
fn test_no_warnings_for_normal_input() {
let input = r#"{"key": "value"}"#;
let result = parse_rrjson_with_warnings(input).unwrap();
assert!(result.warnings.is_empty());
}
#[test]
fn test_mixed_relaxed_features() {
let input = r#"{
// Configuration file
title: 'My Song',
pdf.font.size: 12,
chords: [
"Am",
"G",
],
}"#;
let v = parse_rrjson(input).unwrap();
assert_eq!(v["title"], Value::String("My Song".to_string()));
assert_eq!(v["pdf"]["font"]["size"], Value::Number(12.0));
}
#[test]
fn test_error_line_column() {
let result = parse_rrjson("{\n \"key\": }");
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.line, 2);
assert!(err.column > 0);
}
#[test]
fn test_unterminated_string() {
let result = parse_rrjson(r#"{"key": "unterminated}"#);
assert!(result.is_err());
}
#[test]
fn test_value_as_str() {
let v = Value::String("hello".to_string());
assert_eq!(v.as_str(), Some("hello"));
assert_eq!(Value::Null.as_str(), None);
}
#[test]
fn test_value_as_f64() {
let v = Value::Number(2.75);
assert_eq!(v.as_f64(), Some(2.75));
assert_eq!(Value::Null.as_f64(), None);
}
#[test]
fn test_value_as_bool() {
assert_eq!(Value::Bool(true).as_bool(), Some(true));
assert_eq!(Value::Null.as_bool(), None);
}
#[test]
fn test_value_as_array() {
let v = Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]);
let arr = v.as_array();
assert!(arr.is_some());
assert_eq!(arr.unwrap().len(), 2);
assert_eq!(arr.unwrap()[0], Value::Number(1.0));
assert!(Value::Null.as_array().is_none());
assert!(Value::String("x".to_string()).as_array().is_none());
}
#[test]
fn test_value_as_object() {
let v = Value::Object(vec![("key".to_string(), Value::Bool(true))]);
let obj = v.as_object();
assert!(obj.is_some());
assert_eq!(obj.unwrap().len(), 1);
assert_eq!(obj.unwrap()[0].0, "key");
assert!(Value::Null.as_object().is_none());
assert!(Value::Array(vec![]).as_object().is_none());
}
#[test]
fn test_value_index_missing_key() {
let v = Value::Object(vec![]);
assert!(v["missing"].is_null());
}
#[test]
fn test_value_index_usize() {
let v = Value::Array(vec![
Value::Number(10.0),
Value::String("hello".to_string()),
Value::Bool(true),
]);
assert_eq!(v[0], Value::Number(10.0));
assert_eq!(v[1], Value::String("hello".to_string()));
assert_eq!(v[2], Value::Bool(true));
}
#[test]
fn test_value_index_usize_out_of_bounds() {
let v = Value::Array(vec![Value::Number(1.0)]);
assert!(v[5].is_null());
}
#[test]
fn test_value_index_usize_non_array() {
let v = Value::String("hello".to_string());
assert!(v[0].is_null());
}
#[test]
fn test_value_is_null() {
assert!(Value::Null.is_null());
assert!(!Value::Bool(false).is_null());
}
#[test]
fn test_display_null() {
assert_eq!(Value::Null.to_string(), "null");
}
#[test]
fn test_display_bool() {
assert_eq!(Value::Bool(true).to_string(), "true");
assert_eq!(Value::Bool(false).to_string(), "false");
}
#[test]
fn test_display_number_integer() {
assert_eq!(Value::Number(42.0).to_string(), "42");
}
#[test]
fn test_display_number_float() {
assert_eq!(Value::Number(2.75).to_string(), "2.75");
}
#[test]
fn test_display_string() {
assert_eq!(Value::String("hello".to_string()).to_string(), r#""hello""#);
}
#[test]
fn test_display_string_escapes() {
let v = Value::String("a\"b\\c\n\r\t".to_string());
assert_eq!(v.to_string(), r#""a\"b\\c\n\r\t""#);
}
#[test]
fn test_display_string_control_chars() {
let v = Value::String("\u{08}\u{0C}\u{01}".to_string());
assert_eq!(v.to_string(), r#""\b\f\u0001""#);
}
#[test]
fn test_display_array() {
let v = Value::Array(vec![
Value::Number(1.0),
Value::String("two".to_string()),
Value::Null,
]);
assert_eq!(v.to_string(), r#"[1,"two",null]"#);
}
#[test]
fn test_display_empty_array() {
assert_eq!(Value::Array(vec![]).to_string(), "[]");
}
#[test]
fn test_display_object() {
let v = Value::Object(vec![
("a".to_string(), Value::Number(1.0)),
("b".to_string(), Value::Bool(true)),
]);
assert_eq!(v.to_string(), r#"{"a":1,"b":true}"#);
}
#[test]
fn test_display_empty_object() {
assert_eq!(Value::Object(vec![]).to_string(), "{}");
}
#[test]
fn test_display_number_large_f64() {
assert_eq!(Value::Number(1e20).to_string(), "100000000000000000000");
assert_eq!(Value::Number(-1e20).to_string(), "-100000000000000000000");
}
#[test]
fn test_display_object_key_escapes() {
let v = Value::Object(vec![
("a\"b".to_string(), Value::Number(1.0)),
("c\\d".to_string(), Value::Number(2.0)),
("e\nf".to_string(), Value::Number(3.0)),
]);
assert_eq!(v.to_string(), r#"{"a\"b":1,"c\\d":2,"e\nf":3}"#);
}
#[test]
fn test_display_object_key_control_chars() {
let v = Value::Object(vec![("\u{08}\u{0C}\u{01}".to_string(), Value::Null)]);
assert_eq!(v.to_string(), r#"{"\b\f\u0001":null}"#);
}
#[test]
fn test_display_roundtrip() {
let input = r#"{"name":"test","values":[1,2,3],"nested":{"flag":true}}"#;
let v = parse_rrjson(input).unwrap();
let output = v.to_string();
let reparsed = parse_rrjson(&output).unwrap();
assert_eq!(v, reparsed);
}
#[test]
fn test_scientific_notation() {
let v = parse_rrjson(r#"{"n": 1.5e2}"#).unwrap();
assert_eq!(v["n"], Value::Number(150.0));
}
#[test]
fn test_deeply_nested_objects_rejected() {
let open: String = "{\"a\":".repeat(MAX_NESTING_DEPTH + 1);
let close: String = "}".repeat(MAX_NESTING_DEPTH + 1);
let input = format!("{open}1{close}");
let result = parse_rrjson(&input);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("nesting depth"),
"error should mention nesting depth: {}",
err.message
);
}
#[test]
fn test_deeply_nested_arrays_rejected() {
let open: String = "[".repeat(MAX_NESTING_DEPTH + 1);
let close: String = "]".repeat(MAX_NESTING_DEPTH + 1);
let input = format!("{open}1{close}");
let result = parse_rrjson(&input);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("nesting depth"),
"error should mention nesting depth: {}",
err.message
);
}
#[test]
fn test_nesting_at_max_depth_accepted() {
let open: String = "{\"a\":".repeat(MAX_NESTING_DEPTH);
let close: String = "}".repeat(MAX_NESTING_DEPTH);
let input = format!("{open}1{close}");
let result = parse_rrjson(&input);
assert!(result.is_ok(), "nesting at exactly max depth should work");
}
#[test]
fn test_mixed_nesting_depth_rejected() {
let mut input = String::new();
for i in 0..=MAX_NESTING_DEPTH {
if i % 2 == 0 {
input.push_str("{\"a\":");
} else {
input.push('[');
}
}
input.push('1');
for i in (0..=MAX_NESTING_DEPTH).rev() {
if i % 2 == 0 {
input.push('}');
} else {
input.push(']');
}
}
let result = parse_rrjson(&input);
assert!(result.is_err());
}
#[test]
fn test_dotted_key_exceeding_depth_is_rejected() {
let segments: Vec<String> = (0..=MAX_NESTING_DEPTH).map(|i| format!("k{i}")).collect();
let deep_key = segments.join(".");
let input = format!("{deep_key} = 1");
let result = parse_rrjson(&input);
assert!(
result.is_err(),
"dotted key exceeding depth limit should fail"
);
}
#[test]
fn test_dotted_key_at_limit_is_accepted() {
let segments: Vec<String> = (0..MAX_NESTING_DEPTH).map(|i| format!("k{i}")).collect();
let deep_key = segments.join(".");
let input = format!("{deep_key} = 1");
let result = parse_rrjson(&input);
assert!(
result.is_ok(),
"dotted key at depth limit should be accepted"
);
}
#[test]
fn test_parse_rejects_infinity() {
let result = parse_rrjson(r#"{"x": 1e309}"#);
assert!(result.is_err(), "1e309 should be rejected as infinite");
assert!(result.unwrap_err().message.contains("out of range"));
}
#[test]
fn test_parse_rejects_negative_infinity() {
let result = parse_rrjson(r#"{"x": -1e309}"#);
assert!(result.is_err(), "-1e309 should be rejected as infinite");
}
#[test]
fn test_parse_accepts_large_but_finite_number() {
let result = parse_rrjson(r#"{"x": 1e308}"#);
assert!(result.is_ok(), "1e308 is large but finite");
}
#[test]
fn test_array_entry_limit_exceeded() {
assert_eq!(MAX_ENTRIES, 10_000);
let mut input = String::with_capacity(MAX_ENTRIES * 2 + 2);
input.push('[');
for i in 0..=MAX_ENTRIES {
if i > 0 {
input.push(',');
}
input.push('1');
}
input.push(']');
let result = parse_rrjson(&input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.message
.contains("maximum array entry count")
);
}
#[test]
fn test_object_entry_limit_exceeded() {
assert_eq!(MAX_ENTRIES, 10_000);
let mut input = String::with_capacity(MAX_ENTRIES * 10 + 2);
input.push('{');
for i in 0..=MAX_ENTRIES {
if i > 0 {
input.push(',');
}
input.push_str(&format!("k{i}:1"));
}
input.push('}');
let result = parse_rrjson(&input);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.message
.contains("maximum object entry count")
);
}
#[test]
fn test_dotted_key_empty_segment_double_dot() {
let result = parse_rrjson("a..b = 1");
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("empty segment"));
}
#[test]
fn test_dotted_key_empty_segment_leading_dot() {
let result = parse_rrjson(r#"{".a": 1}"#);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("empty segment"));
}
#[test]
fn test_dotted_key_empty_segment_trailing_dot() {
let result = parse_rrjson(r#"{"a.": 1}"#);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("empty segment"));
}
#[test]
fn test_dotted_key_single_dot() {
let result = parse_rrjson(r#"{".": 1}"#);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("empty segment"));
}
#[test]
fn test_dotted_key_valid_segments() {
let result = parse_rrjson("a.b.c = 1");
assert!(result.is_ok());
}
#[test]
fn test_bare_minus_rejected() {
let result = parse_rrjson(r#"{"x": -}"#);
assert!(result.is_err());
let msg = result.unwrap_err().message;
assert!(
msg.contains("expected digit"),
"bare minus should produce digit error, got: {msg}"
);
}
#[test]
fn test_minus_dot_rejected() {
let result = parse_rrjson(r#"{"x": -.}"#);
assert!(result.is_err());
let msg = result.unwrap_err().message;
assert!(
msg.contains("expected digit"),
"minus-dot should produce digit error, got: {msg}"
);
}
#[test]
fn test_minus_dot_five_accepted() {
let result = parse_rrjson(r#"{"x": -.5}"#);
assert!(result.is_ok());
let obj = result.unwrap();
if let Value::Object(entries) = obj {
let val = entries.iter().find(|(k, _)| k == "x").map(|(_, v)| v);
assert_eq!(val, Some(&Value::Number(-0.5)));
} else {
panic!("expected object");
}
}
#[test]
fn test_negative_number_accepted() {
let result = parse_rrjson(r#"{"x": -42}"#);
assert!(result.is_ok());
let obj = result.unwrap();
if let Value::Object(entries) = obj {
let val = entries.iter().find(|(k, _)| k == "x").map(|(_, v)| v);
assert_eq!(val, Some(&Value::Number(-42.0)));
} else {
panic!("expected object");
}
}
#[test]
fn test_display_i64_max_boundary_no_overflow() {
let val = Value::Number(9_223_372_036_854_775_808.0);
let s = val.to_string();
assert!(
!s.starts_with('-'),
"value near i64::MAX must not overflow to negative: {s}"
);
}
#[test]
fn test_display_i64_min_boundary() {
let val = Value::Number(i64::MIN as f64);
assert_eq!(val.to_string(), "-9223372036854775808");
}
#[test]
fn test_display_normal_integer() {
let val = Value::Number(42.0);
assert_eq!(val.to_string(), "42");
}
#[test]
fn test_display_fractional() {
let val = Value::Number(1.5);
assert_eq!(val.to_string(), "1.5");
}
#[test]
fn test_display_nan_emits_null() {
let val = Value::Number(f64::NAN);
assert_eq!(val.to_string(), "null");
}
#[test]
fn test_display_positive_infinity_emits_null() {
let val = Value::Number(f64::INFINITY);
assert_eq!(val.to_string(), "null");
}
#[test]
fn test_display_negative_infinity_emits_null() {
let val = Value::Number(f64::NEG_INFINITY);
assert_eq!(val.to_string(), "null");
}
#[test]
fn test_display_non_finite_inside_array_and_object_is_valid_json() {
let arr = Value::Array(vec![
Value::Number(1.0),
Value::Number(f64::NAN),
Value::Number(2.0),
]);
assert_eq!(arr.to_string(), "[1,null,2]");
let obj = Value::Object(vec![
("a".to_string(), Value::Number(f64::INFINITY)),
("b".to_string(), Value::Number(0.0)),
]);
assert_eq!(obj.to_string(), "{\"a\":null,\"b\":0}");
}
#[test]
fn test_empty_input() {
let result = parse_rrjson("");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Value::Object(vec![]));
}
#[test]
fn test_whitespace_only_input() {
let result = parse_rrjson(" \n\t ");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Value::Object(vec![]));
}
#[test]
fn test_comment_only_input() {
let result = parse_rrjson("// just a comment\n# another comment");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Value::Object(vec![]));
}
#[test]
fn test_duplicate_keys_last_wins() {
let result = parse_rrjson(r#"{"a": 1, "a": 2}"#).unwrap();
if let Value::Object(entries) = result {
let vals: Vec<_> = entries
.iter()
.filter(|(k, _)| k == "a")
.map(|(_, v)| v)
.collect();
assert_eq!(vals.len(), 1);
assert_eq!(vals[0], &Value::Number(2.0));
} else {
panic!("expected object");
}
}
#[test]
fn test_semicolon_separator_in_bare_object() {
let result = parse_rrjson("a = 1; b = 2");
assert!(result.is_ok());
let obj = result.unwrap();
if let Value::Object(entries) = obj {
assert!(
entries
.iter()
.any(|(k, v)| k == "a" && *v == Value::Number(1.0))
);
assert!(
entries
.iter()
.any(|(k, v)| k == "b" && *v == Value::Number(2.0))
);
} else {
panic!("expected object");
}
}
#[test]
fn test_uppercase_exponent() {
let result = parse_rrjson(r#"{"x": 1E10}"#).unwrap();
if let Value::Object(entries) = result {
let val = entries.iter().find(|(k, _)| k == "x").map(|(_, v)| v);
assert_eq!(val, Some(&Value::Number(1e10)));
} else {
panic!("expected object");
}
}
#[test]
fn test_display_roundtrip_unicode() {
let original = r#"{"emoji":"🎵","cjk":"日本語"}"#;
let parsed = parse_rrjson(original).unwrap();
let output = parsed.to_string();
let reparsed = parse_rrjson(&output).unwrap();
assert_eq!(parsed, reparsed);
}
#[test]
fn test_display_roundtrip_escape_sequences() {
let original = r#"{"tab":"\t","newline":"\n","backslash":"\\"}"#;
let parsed = parse_rrjson(original).unwrap();
let output = parsed.to_string();
let reparsed = parse_rrjson(&output).unwrap();
assert_eq!(parsed, reparsed);
}
}