#![no_std]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
mod chars;
mod error;
mod extract;
mod lexer;
mod options;
mod parser;
mod stream;
mod swar;
mod value;
#[cfg(all(doctest, feature = "serde_json", feature = "std"))]
#[doc = include_str!("../README.md")]
#[allow(dead_code)]
struct ReadmeDoctests;
#[cfg(feature = "serde_json")]
mod serde_json_support;
#[cfg(feature = "serde")]
mod serde_support;
use alloc::string::String;
pub use error::{Error, ErrorKind};
pub use extract::{extract, extract_all, extract_partial};
pub use options::{Allow, Options, Repairs};
pub use parser::MAX_NESTING_DEPTH;
pub use stream::{Delta, StreamRepairer};
pub use value::{Number, Value};
#[cfg(feature = "serde_json")]
pub use serde_json_support::{DeserializeError, deserialize, deserialize_with, loads, loads_with};
#[cfg(feature = "serde")]
pub use serde_support::from_value;
pub fn parse(input: &str) -> Result<Value, Error> {
parse_with(input, Options::all())
}
pub fn parse_with(input: &str, opts: Options) -> Result<Value, Error> {
unescaped(input, opts, |text, _| {
parser::Parser::new(text, opts).parse_document()
})
}
fn unescaped<T>(
input: &str,
opts: Options,
parse: impl FnOnce(&str, bool) -> Result<T, Error>,
) -> Result<T, Error> {
if !(opts.repairs(Repairs::UNQUOTED) && looks_double_escaped(input)) {
return parse(input, false);
}
let text = unescape_once(input);
parse(&text, true).map_err(|error| {
Error::new(
error.kind().clone(),
unescaped_position(input, error.position()),
)
})
}
pub fn parse_partial(input: &str, opts: Options) -> Result<Value, Error> {
parse_with(input, opts)
}
pub fn validate(input: &str) -> Result<Value, Error> {
parse_with(input, Options::strict())
}
pub fn repair(input: &str) -> Result<String, Error> {
repair_with(input, Options::all())
}
pub fn repair_with(input: &str, opts: Options) -> Result<String, Error> {
let mut out = String::with_capacity(input.len());
repair_into(input, &mut out, opts)?;
Ok(out)
}
pub fn repair_into(input: &str, out: &mut String, opts: Options) -> Result<(), Error> {
repair_document_into(input, opts, out, None)
}
pub fn repair_bytes(input: &[u8]) -> Result<alloc::vec::Vec<u8>, Error> {
repair_bytes_with(input, Options::all())
}
pub fn repair_bytes_with(input: &[u8], opts: Options) -> Result<alloc::vec::Vec<u8>, Error> {
let mut out = alloc::vec::Vec::with_capacity(input.len());
repair_bytes_into(input, &mut out, opts)?;
Ok(out)
}
pub fn repair_bytes_into(
input: &[u8],
out: &mut alloc::vec::Vec<u8>,
opts: Options,
) -> Result<(), Error> {
let input = str_from_utf8(input)?;
let start = out.len();
match String::from_utf8(core::mem::take(out)) {
Ok(mut buf) => {
buf.reserve(input.len());
let result = repair_into(input, &mut buf, opts);
*out = buf.into_bytes();
result.inspect_err(|_| out.truncate(start))
}
Err(error) => {
*out = error.into_bytes();
let text = repair_with(input, opts).inspect_err(|_| {
out.truncate(start);
})?;
out.extend_from_slice(text.as_bytes());
Ok(())
}
}
}
fn str_from_utf8(input: &[u8]) -> Result<&str, Error> {
core::str::from_utf8(input)
.map_err(|error| Error::new(ErrorKind::InvalidUtf8, error.valid_up_to()))
}
pub(crate) fn repair_document_into(
input: &str,
opts: Options,
out: &mut String,
cp: Option<&mut parser::ResumeCp>,
) -> Result<(), Error> {
let start = out.len();
let result = if opts.allows(Allow::ALL) {
unescaped(input, opts, |text, rewritten| {
let cp = if rewritten { None } else { cp };
let mut parser = parser::Parser::new_stream(text, opts, out, cp);
parser.parse_document_stream()?;
Ok(Some(parser.max_depth()))
})
} else {
parse_with(input, opts).map(|value| {
value.write_to(out);
None
})
};
let max_depth = result.inspect_err(|_| out.truncate(start))?;
let scan_needed = max_depth.is_none_or(|depth| depth + 1 > parser::MAX_NESTING_DEPTH);
if scan_needed && parser::structural_depth(&out[start..]) > parser::MAX_NESTING_DEPTH {
out.truncate(start);
return Err(Error::new(ErrorKind::DepthLimitExceeded, input.len()));
}
Ok(())
}
#[cfg(feature = "std")]
#[cfg(test)]
mod writer_tests {
use super::*;
use std::vec::Vec;
#[test]
fn repair_to_writer_appends_canonical_json() {
let mut out: Vec<u8> = b"[prefix] ".to_vec();
repair_to_writer("{a: 1}", &mut out, Options::all()).expect("repairs");
assert_eq!(out, b"[prefix] {\"a\": 1}");
}
#[test]
fn repair_to_writer_leaves_the_writer_untouched_on_repair_error() {
let mut out: Vec<u8> = b"prefix".to_vec();
let error = repair_to_writer("prose {\"a\": 1}", &mut out, Options::all())
.expect_err("prose must not repair");
assert!(matches!(error, WriteError::Repair(_)));
assert_eq!(out, b"prefix");
}
}
#[cfg(test)]
mod bytes_tests {
use super::*;
use alloc::string::String;
use alloc::vec::Vec;
#[test]
fn repair_bytes_into_appends_canonical_json() {
let mut out: Vec<u8> = b"[prefix] ".to_vec();
repair_bytes_into(b"{a: 1, /* c */ b: 'two',}", &mut out, Options::all()).expect("repairs");
assert_eq!(out, b"[prefix] {\"a\": 1, \"b\": \"two\"}");
}
#[test]
fn repair_bytes_returns_the_repaired_document() {
let out = repair_bytes(b"{a: 1}").expect("repairs");
assert_eq!(out, b"{\"a\": 1}");
}
#[test]
fn repair_bytes_into_appends_after_a_non_utf8_prefix() {
let mut out: Vec<u8> = alloc::vec![0xFF, 0xFE, b' '];
repair_bytes_into(b"{a: 1,}", &mut out, Options::all()).expect("repairs");
let mut expected = alloc::vec![0xFF_u8, 0xFE, b' '];
expected.extend_from_slice(b"{\"a\": 1}");
assert_eq!(out, expected);
}
#[test]
fn repair_bytes_rejects_invalid_utf8_at_the_exact_offset() {
let error = repair_bytes(b"{\"a\": \xff}").expect_err("must reject invalid UTF-8");
assert_eq!(error.kind(), &ErrorKind::InvalidUtf8);
assert_eq!(error.position(), 6);
}
#[test]
fn repair_bytes_into_leaves_the_sink_untouched_on_error() {
let mut out: Vec<u8> = b"prefix".to_vec();
repair_bytes_into(b"prose {\"a\": 1}", &mut out, Options::all())
.expect_err("must not repair");
assert_eq!(out, b"prefix");
repair_bytes_into(b"\xff", &mut out, Options::all())
.expect_err("must reject invalid UTF-8");
assert_eq!(out, b"prefix");
}
#[test]
fn repair_bytes_matches_the_text_api() {
for input in ["{a: 1", "'x', 'y'", "[1, 2, 3,]", "{\"k\": \"b\\\"}"] {
let text = repair_with(input, Options::all()).expect("repairs");
let bytes = repair_bytes_with(input.as_bytes(), Options::all()).expect("repairs");
assert_eq!(bytes, text.as_bytes(), "input {input:?}");
}
}
#[test]
fn repair_bytes_supports_restricted_options() {
let mut out = String::new();
let mut bytes = Vec::new();
let mut opts = Options::all();
opts.allow = crate::Allow::NONE; repair_into("{a: 1,}", &mut out, opts).expect("repairs");
repair_bytes_into(b"{a: 1,}", &mut bytes, opts).expect("repairs");
assert_eq!(bytes, out.as_bytes());
}
}
#[cfg(feature = "std")]
#[derive(Debug)]
#[non_exhaustive]
pub enum WriteError {
Repair(Error),
Write(std::io::Error),
}
#[cfg(feature = "std")]
impl core::fmt::Display for WriteError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
WriteError::Repair(error) => write!(f, "repair failed: {error}"),
WriteError::Write(error) => write!(f, "write failed: {error}"),
}
}
}
#[cfg(feature = "std")]
impl core::error::Error for WriteError {}
#[cfg(feature = "std")]
impl From<Error> for WriteError {
fn from(error: Error) -> Self {
WriteError::Repair(error)
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for WriteError {
fn from(error: std::io::Error) -> Self {
WriteError::Write(error)
}
}
#[cfg(feature = "std")]
pub fn repair_to_writer(
input: &str,
sink: &mut impl std::io::Write,
opts: Options,
) -> Result<(), WriteError> {
let mut buf = alloc::string::String::with_capacity(input.len());
repair_into(input, &mut buf, opts)?;
sink.write_all(buf.as_bytes()).map_err(WriteError::Write)
}
pub fn repair_extract(input: &str) -> Result<String, Error> {
match extract(input) {
Some(span) => repair(span).or_else(|_| repair(input)),
None => repair(input),
}
}
fn looks_double_escaped(input: &str) -> bool {
let bytes = input.as_bytes();
let mut escaped_quotes = 0usize;
for (index, byte) in bytes.iter().enumerate() {
if *byte != b'"' {
continue;
}
let backslashes = bytes[..index]
.iter()
.rev()
.take_while(|b| **b == b'\\')
.count();
if backslashes % 2 == 1 {
escaped_quotes += 1;
} else {
return false;
}
}
escaped_quotes > 0
}
#[derive(Debug, Clone, Default)]
pub(crate) struct DoubleEscapeScanner {
escaped_quotes: usize,
raw_quotes: usize,
backslashes: usize,
snapshot: (usize, usize, usize),
}
impl DoubleEscapeScanner {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push(&mut self, chunk: &str) {
self.snapshot = (self.escaped_quotes, self.raw_quotes, self.backslashes);
for &byte in chunk.as_bytes() {
match byte {
b'\\' => self.backslashes += 1,
b'"' => {
if self.backslashes % 2 == 1 {
self.escaped_quotes += 1;
} else {
self.raw_quotes += 1;
}
self.backslashes = 0;
}
_ => self.backslashes = 0,
}
}
}
pub(crate) fn rollback(&mut self) {
(self.escaped_quotes, self.raw_quotes, self.backslashes) = self.snapshot;
}
pub(crate) fn verdict(&self) -> bool {
self.escaped_quotes > 0 && self.raw_quotes == 0
}
}
#[cfg(all(test, feature = "serde_json"))]
mod loads_tests {
use super::*;
use alloc::string::String;
use serde_json::Value as JsonValue;
#[test]
fn loads_repairs_straight_into_a_serde_json_value() {
let value = serde_json_support::loads("{name: 'Ada', scores: [1, 2,], ok: True}")
.expect("repairs and parses");
assert_eq!(value["name"], "Ada");
assert_eq!(value["scores"][1], 2);
assert_eq!(value["ok"], true);
}
#[test]
fn loads_matches_deserialize() {
let input = r#"{"a": 1, "b": [true, null, 1234567890123456789]}"#;
let via_loads = serde_json_support::loads(input).expect("loads");
let via_deserialize: JsonValue = crate::deserialize(input).expect("deserialize");
assert_eq!(via_loads, via_deserialize);
}
#[test]
fn loads_keeps_out_of_range_numbers_as_strings() {
let value = serde_json_support::loads("1e400").expect("repairs");
assert_eq!(value, JsonValue::String(String::from("1e400")));
}
}
fn unescape_once(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some(next) => match chars::simple_escape(next) {
Some(decoded) => out.push(decoded),
None => {
out.push('\\');
out.push(next);
}
},
None => out.push('\\'),
}
}
out
}
fn unescaped_position(input: &str, position: usize) -> usize {
let mut out = 0usize;
let mut chars = input.char_indices().peekable();
while let Some((start, c)) = chars.next() {
let (orig_len, out_len) = if c != '\\' {
(c.len_utf8(), c.len_utf8())
} else {
match chars.peek() {
Some(&(_, next)) => {
chars.next();
if chars::simple_escape(next).is_some() {
(2, 1)
} else {
let len = 1 + next.len_utf8();
(len, len)
}
}
None => (1, 1),
}
};
if out + out_len > position {
return start
+ if orig_len == out_len {
position - out
} else {
0
};
}
out += out_len;
}
input.len()
}
#[cfg(test)]
mod double_escape_scanner_tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn incremental_scan_matches_the_whole_input_scan() {
let inputs = [
r#"\"a\": 1"#,
r#""a\" b""#,
"\\\\",
"\\\\\\\"",
"\"",
"\\\"",
"\\\\\"",
"a\\\\\\",
r#"\"a\": \"b\""#,
r#"\"a\": \"b\\\""#,
"no quotes at all",
];
for input in inputs {
let want = looks_double_escaped(input);
let one_char_chunks: Vec<&str> = {
let mut spans = Vec::new();
let mut chars = input.char_indices().peekable();
while let Some((start, _)) = chars.next() {
let end = chars.peek().map(|(next, _)| *next).unwrap_or(input.len());
spans.push(&input[start..end]);
}
spans
};
for split in 0..=input.len() {
if !input.is_char_boundary(split) {
continue;
}
for chunks in [
vec![input],
vec![&input[..split], &input[split..]],
one_char_chunks.clone(),
] {
let mut scanner = DoubleEscapeScanner::new();
for chunk in &chunks {
scanner.push(chunk);
}
assert_eq!(
scanner.verdict(),
want,
"input {input:?} split {split:?} chunks {chunks:?}"
);
}
}
}
}
#[test]
fn rollback_restores_the_previous_verdict() {
let mut scanner = DoubleEscapeScanner::new();
scanner.push(r#"\"a\" b \"c"#);
let before = scanner.verdict();
scanner.push("\\\""); scanner.rollback();
assert_eq!(scanner.verdict(), before);
}
}