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> {
self.render(chunk, false)?;
Ok(&self.current)
}
pub fn push_delta(&mut self, chunk: &str) -> Result<Delta<'_>, Error> {
let keep = self.render(chunk, true)?;
Ok(Delta {
keep,
text: &self.current[keep..],
})
}
fn render(&mut self, chunk: &str, delta: bool) -> Result<usize, 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());
if delta {
self.previous.clear();
self.previous.push_str(&self.current[keep..]);
}
self.current.truncate(keep);
let mut parser = parser::Parser::new_stream_resumed(
&self.input,
self.opts,
&mut self.current,
&mut self.cp,
);
let (resumed, shift) = (parser.parse_resume(), parser.shifted_at());
if resumed.is_ok() {
return Ok(if delta {
resumed_prefix_len(&self.current, keep, &self.previous, shift)
} else {
0
});
}
if delta {
if let Some(at) = shift {
self.current.remove(at);
}
self.current.truncate(keep);
self.current.push_str(&self.previous);
}
}
self.cp.valid = false;
let cp = if self.cp_enabled() {
Some(&mut self.cp)
} else {
None
};
let target = if delta {
&mut self.previous
} else {
&mut self.current
};
target.clear();
match crate::repair_document_into(&self.input, self.opts, target, cp) {
Ok(()) if delta => {
core::mem::swap(&mut self.previous, &mut self.current);
Ok(common_prefix_len(&self.previous, &self.current))
}
Ok(()) => Ok(0),
Err(error) => {
self.cp.valid = false;
self.escape_scan.rollback();
self.input.truncate(before);
if !delta {
self.current.clear();
let _ = crate::repair_document_into(
&self.input,
self.opts,
&mut self.current,
None,
);
}
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 {
char_floor(b, shared_bytes(a.as_bytes(), b.as_bytes()))
}
fn resumed_prefix_len(new: &str, keep: usize, old_tail: &str, shift: Option<usize>) -> usize {
let bytes = new.as_bytes();
let mut len = shift.unwrap_or(keep);
while len < keep && bytes[len + 1] == bytes[len] {
len += 1;
}
if len == keep {
len += shared_bytes(old_tail.as_bytes(), &bytes[keep..]);
}
char_floor(new, len)
}
fn shared_bytes(a: &[u8], b: &[u8]) -> usize {
let n = a.len().min(b.len());
let mut i = 0;
while i + 8 <= n && crate::swar::load_word(a, i) == crate::swar::load_word(b, i) {
i += 8;
}
while i < n && a[i] == b[i] {
i += 1;
}
i
}
fn char_floor(text: &str, mut index: usize) -> usize {
while !text.is_char_boundary(index) {
index -= 1;
}
index
}
#[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());
}
const CRASH_2546: &[u8] = &[
34, 123, 97, 58, 255, 92, 117, 48, 48, 101, 57, 26, 32, 32, 92, 255, 255, 255,
];
fn fuzz_chunks(data: &[u8]) -> alloc::vec::Vec<String> {
let (&steer, rest) = data.split_first().expect("nonempty");
let step = 1 + usize::from(steer % 32);
let text = String::from_utf8_lossy(rest);
let mut chunks = alloc::vec::Vec::new();
let mut start = 0;
while start < text.len() {
let mut end = (start + step).min(text.len());
while !text.is_char_boundary(end) {
end += 1;
}
chunks.push(String::from(&text[start..end]));
start = end;
}
chunks
}
#[test]
fn crash2546_exact_bytes() {
let mut s = StreamRepairer::new();
let mut acc = String::new();
for chunk in fuzz_chunks(CRASH_2546) {
acc.push_str(&chunk);
let got = s.push(&chunk).map(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:?})");
}
}
}
#[test]
fn eof_dependent_renders_never_checkpoint() {
let af2a: &[u8] = &[99, 123, 34, 97, 34, 0, 34, 0, 58, 32, 50, 169, 41];
let f245: &[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,
];
for (data, must_repair) in [(CRASH_2546, true), (af2a, true), (f245, false)] {
let mut s = StreamRepairer::new();
for (n, chunk) in fuzz_chunks(data).iter().enumerate() {
let pushed = s.push(chunk).map(String::from);
assert!(
!must_repair || pushed.is_ok(),
"chunk {n} {chunk:?}: {pushed:?}"
);
assert!(!s.cp.valid, "after chunk {n} {chunk:?} cp={:?}", s.cp);
}
}
}
#[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());
}
}