From 37878498401a27a64b7b1d498349a589f894112a Mon Sep 17 00:00:00 2001 From: yuxi-liu-wired <33951560+yuxi-liu-wired@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:23:42 +0000 Subject: [PATCH] fix: StreamDeserializer ends without an error after the last value `into_stream()` yielded an extra `Err(EOF while parsing)` once the input was exhausted, so every well-formed stream ended in an error and a loop like `for v in stream { v? }` always failed: Deserializer::from_str("1 2").into_stream::() Ok(1), Ok(2), Err(EOF while parsing at line 1 column 3) Deserializer::from_str("").into_stream::() Err(EOF while parsing at line 1 column 1) serde_json's `StreamDeserializer` returns `None` there, which issue #202 asks sonic-rs to align with. `next()` now checks for remaining non-space input first; a value that is cut short or garbage after a value is still an error and still ends the stream. (serde_json additionally requires whitespace between two values that do not delimit themselves, e.g. `1true`; sonic-rs splits them. Not changed here.) Test: `test_stream_deserializer_ends_without_error` (fails before this change). Found by a differential fuzzer (serde_json, simd-json, sonic-rs, jiter) extended to multi-document streams. --- src/serde/de.rs | 6 ++++++ src/serde/mod.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/serde/de.rs b/src/serde/de.rs index 17b2115..db71213 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -196,6 +196,12 @@ where if self.is_ending { return None; } + // Only whitespace left: the stream is done (as serde_json's `StreamDeserializer`), not an + // EOF error. A value that starts but is cut short is still an error. + if self.de.parser.skip_space_peek().is_none() { + self.is_ending = true; + return None; + } let val: Result = self.de.deserialize(); if val.is_err() { self.is_ending = true; diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 61f6d28..1a90144 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -741,6 +741,32 @@ mod test { assert_eq!(expect, got); } + #[test] + fn test_stream_deserializer_ends_without_error() { + // A stream with only whitespace left is finished: `next()` returns None, as for + // serde_json's StreamDeserializer. It used to yield an extra `Err(EOF while parsing)`. + fn collect(json: &str) -> Vec> { + Deserializer::from_str(json) + .into_stream::() + .map(|r| r.map_err(|_| ())) + .collect() + } + assert!(collect("").is_empty()); + assert!(collect(" \n\t").is_empty()); + assert_eq!( + collect("1 2"), + vec![Ok(crate::json!(1)), Ok(crate::json!(2))] + ); + assert_eq!( + collect("[1] {} \n"), + vec![Ok(crate::json!([1])), Ok(crate::json!({}))] + ); + assert_eq!(collect("true"), vec![Ok(crate::json!(true))]); + // garbage or a truncated value is still an error, and ends the stream + assert_eq!(collect("1 x"), vec![Ok(crate::json!(1)), Err(())]); + assert_eq!(collect("[1] [2"), vec![Ok(crate::json!([1])), Err(())]); + } + #[test] fn test_utf8_lossy() { let data = [&[b'\"', 0xff, b'\"'][..], br#""\uD800""#, br#""\udc00""#];