use alloc::string::String;
use crate::error::Error;
use crate::options::{Allow, Options, Repairs};
use crate::parser::{self, ResumeCp};
use crate::value::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Delta<'a> {
pub keep: usize,
pub text: &'a str,
}
#[derive(Debug, Clone)]
pub struct StreamRepairer {
input: String,
previous: String,
current: String,
opts: Options,
cp: ResumeCp,
saw_backtick: bool,
escape_scan: crate::DoubleEscapeScanner,
}
impl StreamRepairer {
#[must_use]
pub fn new() -> Self {
Self::with_options(Options::all())
}
#[must_use]
pub fn with_options(opts: Options) -> Self {
Self {
input: String::new(),
previous: String::new(),
current: String::new(),
opts,
cp: ResumeCp::new(),
saw_backtick: false,
escape_scan: crate::DoubleEscapeScanner::new(),
}
}
fn cp_enabled(&self) -> bool {
self.opts.allows(Allow::ALL)
&& !(self.opts.repairs(Repairs::UNQUOTED) && self.escape_scan.verdict())
&& !self.saw_backtick
}
fn can_resume(&self) -> bool {
self.cp.valid && self.cp_enabled()
}
fn note_chunk(&mut self, chunk: &str) {
self.escape_scan.push(chunk);
if !self.saw_backtick && chunk.contains('`') {
self.saw_backtick = true;
self.cp.valid = false;
}
}
pub fn push(&mut self, chunk: &str) -> Result<&str, Error> {
let before = self.input.len();
self.note_chunk(chunk);
self.input.push_str(chunk);
if self.can_resume() {
let keep = self.cp.output_len.min(self.current.len());
self.current.truncate(keep);
let resumed = {
let mut parser = parser::Parser::new_stream_resumed(
&self.input,
self.opts,
&mut self.current,
&mut self.cp,
);
parser.parse_resume()
};
if resumed.is_ok() {
return Ok(&self.current);
}
}
self.current.clear();
let result = if self.cp_enabled() {
crate::repair_document_into(
&self.input,
self.opts,
&mut self.current,
Some(&mut self.cp),
)
} else {
crate::repair_document_into(&self.input, self.opts, &mut self.current, None)
};
match result {
Ok(()) => Ok(&self.current),
Err(error) => {
self.cp.valid = false;
self.escape_scan.rollback();
self.input.truncate(before);
self.current.clear();
let _ =
crate::repair_document_into(&self.input, self.opts, &mut self.current, None);
Err(error)
}
}
}
pub fn push_delta(&mut self, chunk: &str) -> Result<Delta<'_>, Error> {
let before = self.input.len();
self.note_chunk(chunk);
self.input.push_str(chunk);
if self.can_resume() {
core::mem::swap(&mut self.previous, &mut self.current);
self.current.clear();
let prefix_len = self.cp.output_len.min(self.previous.len());
self.current.push_str(&self.previous[..prefix_len]);
let resumed = {
let mut parser = parser::Parser::new_stream_resumed(
&self.input,
self.opts,
&mut self.current,
&mut self.cp,
);
parser.parse_resume()
};
match resumed {
Ok(()) => {
let keep = common_prefix_len(&self.previous, &self.current);
return Ok(Delta {
keep,
text: &self.current[keep..],
});
}
Err(error) => {
self.cp.valid = false;
self.escape_scan.rollback();
self.current.clear();
core::mem::swap(&mut self.previous, &mut self.current);
self.input.truncate(before);
return Err(error);
}
}
}
self.previous.clear();
let result = if self.cp_enabled() {
crate::repair_document_into(
&self.input,
self.opts,
&mut self.previous,
Some(&mut self.cp),
)
} else {
crate::repair_document_into(&self.input, self.opts, &mut self.previous, None)
};
match result {
Ok(()) => {
core::mem::swap(&mut self.previous, &mut self.current);
let keep = common_prefix_len(&self.previous, &self.current);
Ok(Delta {
keep,
text: &self.current[keep..],
})
}
Err(error) => {
self.cp.valid = false;
self.escape_scan.rollback();
self.input.truncate(before);
self.previous.clear();
Err(error)
}
}
}
pub fn value(&self) -> Result<Value, Error> {
crate::parse_with(&self.input, self.opts)
}
#[must_use]
pub fn input(&self) -> &str {
&self.input
}
#[must_use]
pub fn output(&self) -> &str {
&self.current
}
#[must_use]
pub fn len(&self) -> usize {
self.input.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.input.is_empty()
}
pub fn reset(&mut self) {
self.input.clear();
self.previous.clear();
self.current.clear();
self.cp = ResumeCp::new();
self.saw_backtick = false;
self.escape_scan = crate::DoubleEscapeScanner::new();
}
}
impl Default for StreamRepairer {
fn default() -> Self {
Self::new()
}
}
fn common_prefix_len(a: &str, b: &str) -> usize {
let (left, right) = (a.as_bytes(), b.as_bytes());
let mut len = 0usize;
while len < left.len() && len < right.len() && left[len] == right[len] {
len += 1;
}
while len > 0 && !b.is_char_boundary(len) {
len -= 1;
}
len
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checkpoint_after_a_comma_boundary() {
let mut stream = StreamRepairer::new();
stream.push("{\"a\": 1,").unwrap();
assert!(stream.cp.valid, "consumed comma must checkpoint");
assert_eq!(stream.cp.phase, parser::CpPhase::InContainer);
}
#[test]
fn scalar_at_eof_does_not_checkpoint() {
let mut stream = StreamRepairer::new();
stream.push("12").unwrap();
assert!(
!stream.cp.valid,
"growable token at EOF must not checkpoint"
);
}
#[test]
fn closed_container_checkpoints_even_at_eof() {
let mut stream = StreamRepairer::new();
stream.push("{\"a\": 1}").unwrap();
assert!(stream.cp.valid, "a real closing brace must checkpoint");
}
#[test]
fn truncated_string_at_eof_does_not_checkpoint() {
let mut stream = StreamRepairer::new();
let out = String::from(stream.push("\"a#\n").expect("chunk0"));
assert_eq!(out, "\"a#\"");
assert!(
!stream.cp.valid,
"truncated string at EOF must not leave a resume point (cp={:?})",
stream.cp
);
let full = String::from(stream.push("\0\0\0\x32").expect("chunk1"));
assert_eq!(full, crate::repair("\"a#\n\0\0\0\x32").unwrap());
}
}
#[cfg(test)]
mod fuzz_regressions2 {
use super::*;
#[test]
fn unquoted_value_spaces_then_more_does_not_checkpoint() {
let mut s = StreamRepairer::new();
let p0 = "{\"a:\u{FFFD}\\u00e9\u{1a} ";
let o0 = String::from(s.push(p0).unwrap());
assert_eq!(o0, crate::repair(p0).unwrap());
assert!(
!s.cp.valid,
"truncated unquoted/str value at EOF must not checkpoint: {:?}",
s.cp
);
let p1 = "\u{FFFD}\u{FFFD}";
let mut acc = String::from(p0);
acc.push_str(p1);
let o1 = String::from(s.push(p1).unwrap());
assert_eq!(o1, crate::repair(&acc).unwrap());
}
#[test]
fn crash2546_exact_bytes() {
let data: &[u8] = &[
34, 123, 97, 58, 255, 92, 117, 48, 48, 101, 57, 26, 32, 32, 92, 255, 255, 255,
];
let (&steer, rest) = data.split_first().expect("nonempty");
let step = 1 + usize::from(steer % 32);
let text = alloc::string::String::from_utf8_lossy(rest);
let text = text.as_ref();
let mut s = StreamRepairer::new();
let mut acc = alloc::string::String::new();
let mut start = 0;
while start < text.len() {
let mut end = (start + step).min(text.len());
if end < text.len() {
while end < text.len() && !text.is_char_boundary(end) {
end += 1;
}
}
let chunk = &text[start..end];
acc.push_str(chunk);
let got = s.push(chunk).map(alloc::string::String::from);
let want = crate::repair(&acc);
if got.is_err() && want.is_err() {
acc.truncate(acc.len() - chunk.len());
} else {
assert_eq!(got, want, "at prefix {acc:?} (chunk {chunk:?})");
}
start = end;
}
}
#[test]
fn probe_cp_after_each_chunk() {
let data: &[u8] = &[
34, 123, 97, 58, 255, 92, 117, 48, 48, 101, 57, 26, 32, 32, 92, 255, 255, 255,
];
let (&steer, rest) = data.split_first().expect("nonempty");
let step = 1 + usize::from(steer % 32);
let text = String::from_utf8_lossy(rest);
let text = text.as_ref();
let mut s = StreamRepairer::new();
let mut start = 0;
let mut n = 0;
while start < text.len() {
let mut end = (start + step).min(text.len());
if end < text.len() {
while end < text.len() && !text.is_char_boundary(end) {
end += 1;
}
}
let chunk = &text[start..end];
let out = String::from(s.push(chunk).expect("push"));
if s.cp.valid {
panic!(
"after chunk {n} [{start}..{end}] {chunk:?} out={out:?} cp={:?}",
s.cp
);
}
start = end;
n += 1;
}
}
#[test]
fn truncated_keyword_promotion_does_not_checkpoint() {
let mut s = StreamRepairer::new();
let o0 = String::from(s.push("nu").expect("chunk0"));
assert_eq!(o0, crate::repair("nu").unwrap());
assert!(
!s.cp.valid,
"promoted truncated keyword must not checkpoint: {:?}",
s.cp
);
let o1 = String::from(s.push("ll").expect("chunk1"));
assert_eq!(o1, crate::repair("null").unwrap());
}
#[test]
fn probe_af2a_cp() {
let data: &[u8] = &[99, 123, 34, 97, 34, 0, 34, 0, 58, 32, 50, 169, 41];
let (&steer, rest) = data.split_first().expect("x");
let step = 1 + usize::from(steer % 32u8);
let text = String::from_utf8_lossy(rest);
let text = text.as_ref();
let mut s = StreamRepairer::new();
let mut start = 0;
let mut n = 0;
while start < text.len() {
let mut end = (start + step).min(text.len());
if end < text.len() {
while end < text.len() && !text.is_char_boundary(end) {
end += 1;
}
}
let chunk = &text[start..end];
let out = String::from(s.push(chunk).expect("p"));
if s.cp.valid {
panic!(
"chunk {n} [{start}..{end}] {chunk:?} out={out:?} cp={:?}",
s.cp
);
}
start = end;
n += 1;
}
}
#[test]
fn probe_24f5_cp() {
let data: &[u8] = &[
99, 123, 34, 97, 34, 58, 32, 49, 38, 50, 169, 41, 123, 97, 10, 10, 10, 10, 10, 10, 10,
86, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 236, 255, 255, 255, 10, 0, 0, 0, 0,
0, 0, 0, 0, 0,
];
let (&steer, rest) = data.split_first().expect("x");
let step = 1 + usize::from(steer as u16 % 32);
let text = String::from_utf8_lossy(rest);
let text = text.as_ref();
let mut s = StreamRepairer::new();
let mut start = 0;
let mut n = 0;
while start < text.len() {
let mut end = (start + step).min(text.len());
if end < text.len() {
while end < text.len() && !text.is_char_boundary(end) {
end += 1;
}
}
let chunk = &text[start..end];
if s.push(chunk).is_err() {
}
if s.cp.valid {
panic!("after chunk {n} [{start}..{end}] cp={:?}", s.cp);
}
start = end;
n += 1;
}
}
}