diff --git a/crates/memofs/src/lib.rs b/crates/memofs/src/lib.rs index 6c0f17e2..411b6f41 100644 --- a/crates/memofs/src/lib.rs +++ b/crates/memofs/src/lib.rs @@ -259,6 +259,28 @@ impl Vfs { Self::new(StdBackend::new()) } + /// Creates a new `Vfs` suitable for one-shot operations like syncback. + /// + /// Unlike `new_default()`, this creates a backend that: + /// - Has file watching disabled by default + /// - Uses a non-fatal error handler for watcher issues (logs instead of exiting) + /// + /// This is ideal for CLI commands that don't need real-time file watching + /// and shouldn't be terminated if the watcher thread encounters issues. + pub fn new_oneshot() -> Self { + let backend = StdBackend::new_with_error_handler(Box::new(|err| { + // Log the error but don't exit - one-shot operations don't need file watching + log::debug!( + "File watcher issue (non-fatal for one-shot operation): {}", + err + ); + true // Stop the watcher thread, but don't exit the process + })); + let vfs = Self::new(backend); + vfs.set_watch_enabled(false); + vfs + } + /// Creates a new `Vfs` with the given backend. pub fn new(backend: B) -> Self { let lock = VfsInner { diff --git a/src/cli/fmt_project.rs b/src/cli/fmt_project.rs index d536b261..44a8a1a4 100644 --- a/src/cli/fmt_project.rs +++ b/src/cli/fmt_project.rs @@ -18,8 +18,8 @@ pub struct FmtProjectCommand { impl FmtProjectCommand { pub fn run(self) -> anyhow::Result<()> { - let vfs = Vfs::new_default(); - vfs.set_watch_enabled(false); + // Use oneshot Vfs - file watching isn't needed for formatting + let vfs = Vfs::new_oneshot(); let base_path = resolve_path(&self.project); let project = Project::load_fuzzy(&vfs, &base_path)? diff --git a/src/cli/syncback.rs b/src/cli/syncback.rs index 42d68934..7a2cae18 100644 --- a/src/cli/syncback.rs +++ b/src/cli/syncback.rs @@ -69,8 +69,9 @@ impl SyncbackCommand { dom_start_timer.elapsed().as_secs_f32() ); - let vfs = Vfs::new_default(); - vfs.set_watch_enabled(false); + // Use oneshot Vfs for syncback - file watching isn't needed and + // watcher errors shouldn't terminate the process + let vfs = Vfs::new_oneshot(); let project_start_timer = Instant::now(); let session_old = ServeSession::new(vfs, path_old.clone())?; diff --git a/src/json.rs b/src/json.rs index 441a2775..058c6278 100644 --- a/src/json.rs +++ b/src/json.rs @@ -127,17 +127,122 @@ pub fn from_slice_with_context( } /// A JSON5 value that uses BTreeMap for sorted keys and supports NaN/Infinity. -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] -#[serde(untagged)] +/// Uses String for numbers to preserve exact representation (including scientific notation). +#[derive(Debug, Clone, PartialEq)] enum Json5Value { Null, Bool(bool), - Number(f64), + Number(String), // Store as string to preserve exact representation String(String), Array(Vec), Object(std::collections::BTreeMap), } +impl Json5Value { + /// Write this value to a string with proper JSON5 formatting and indentation. + fn write_to(&self, output: &mut String, indent: usize) { + let indent_str = " ".repeat(indent); + let inner_indent = " ".repeat(indent + 1); + + match self { + Json5Value::Null => output.push_str("null"), + Json5Value::Bool(b) => output.push_str(if *b { "true" } else { "false" }), + Json5Value::Number(s) => output.push_str(s), + Json5Value::String(s) => { + write_escaped_string(output, s); + } + Json5Value::Array(arr) => { + if arr.is_empty() { + output.push_str("[]"); + } else { + output.push_str("[\n"); + for (i, item) in arr.iter().enumerate() { + output.push_str(&inner_indent); + item.write_to(output, indent + 1); + if i < arr.len() - 1 { + output.push(','); + } + output.push('\n'); + } + output.push_str(&indent_str); + output.push(']'); + } + } + Json5Value::Object(map) => { + if map.is_empty() { + output.push_str("{}"); + } else { + output.push_str("{\n"); + let entries: Vec<_> = map.iter().collect(); + for (i, (key, value)) in entries.iter().enumerate() { + output.push_str(&inner_indent); + // Use unquoted keys if valid identifier, otherwise quote with escaping + if is_valid_identifier(key) { + output.push_str(key); + } else { + write_escaped_string(output, key); + } + output.push_str(": "); + value.write_to(output, indent + 1); + if i < entries.len() - 1 { + output.push(','); + } + output.push('\n'); + } + output.push_str(&indent_str); + output.push('}'); + } + } + } + } +} + +/// Write a string to output with proper JSON5 escaping (double-quoted). +/// +/// Per JSON5 spec Section 5.1 (https://spec.json5.org/#escapes): +/// - Standard escapes: \" \\ \b \f \n \r \t \v +/// - Control characters as \uXXXX +/// +/// Per Section 5.2, generators SHOULD escape U+2028 and U+2029. +fn write_escaped_string(output: &mut String, s: &str) { + output.push('"'); + for c in s.chars() { + match c { + // Standard JSON5 escape sequences (Table 1 in spec) + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\u{0008}' => output.push_str("\\b"), // Backspace + '\u{000C}' => output.push_str("\\f"), // Form feed + '\n' => output.push_str("\\n"), // Line feed (U+000A) + '\r' => output.push_str("\\r"), // Carriage return (U+000D) + '\t' => output.push_str("\\t"), // Horizontal tab (U+0009) + '\u{000B}' => output.push_str("\\v"), // Vertical tab + // Per Section 5.2: generators SHOULD escape U+2028 and U+2029 + '\u{2028}' => output.push_str("\\u2028"), // Line separator + '\u{2029}' => output.push_str("\\u2029"), // Paragraph separator + // Other control characters as \uXXXX + c if c.is_control() => { + output.push_str(&format!("\\u{:04x}", c as u32)); + } + c => output.push(c), + } + } + output.push('"'); +} + +/// Check if a string is a valid unquoted JSON5 identifier +fn is_valid_identifier(s: &str) -> bool { + if s.is_empty() { + return false; + } + let mut chars = s.chars(); + let first = chars.next().unwrap(); + if !first.is_alphabetic() && first != '_' && first != '$' { + return false; + } + chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$') +} + /// Error type for compact JSON5 serialization. #[derive(Debug)] struct CompactJson5Error(String); @@ -249,7 +354,13 @@ impl<'a> serde::Serializer for &mut CompactJson5Serializer<'a> { self.output.push_str("-Infinity"); } } else { - self.output.push_str(&v.to_string()); + // Use scientific notation for very large or very small numbers + let abs = v.abs(); + if abs != 0.0 && !(1e-6..1e15).contains(&abs) { + self.output.push_str(&format!("{:e}", v)); + } else { + self.output.push_str(&v.to_string()); + } } Ok(()) } @@ -508,23 +619,392 @@ impl<'a> serde::ser::SerializeStructVariant for &mut CompactJson5Serializer<'a> /// Serialize a value to a JSON5 byte vector with sorted keys. /// /// Supports NaN/Infinity and sorts object keys alphabetically. +/// Uses a custom serializer that builds a tree directly (no parsing needed). /// /// # Errors /// -/// Returns an error if the value cannot be serialized to JSON5. +/// Returns an error if the value cannot be serialized. pub fn to_vec_pretty_sorted(value: &T) -> anyhow::Result> { - // Serialize to json5 string (preserves NaN/Infinity) - let json5_str = json5::to_string(value).context("Failed to serialize to JSON5")?; + // Serialize directly to Json5Value tree (with BTreeMap for sorted keys) + let tree = value + .serialize(Json5ValueSerializer) + .map_err(|e| anyhow::anyhow!("Failed to serialize: {}", e))?; - // Parse into Json5Value which uses BTreeMap (sorted keys) - let sorted: Json5Value = - json5::from_str(&json5_str).context("Failed to parse JSON5 for sorting")?; - - // Serialize back to json5 (now sorted) - let output = json5::to_string(&sorted).context("Failed to serialize sorted JSON5")?; + // Write the tree to string with pretty formatting + let mut output = String::new(); + tree.write_to(&mut output, 0); + output.push('\n'); Ok(output.into_bytes()) } +/// A serde Serializer that builds a Json5Value tree directly. +/// This avoids the need to parse - we serialize directly to the intermediate representation. +struct Json5ValueSerializer; + +impl serde::Serializer for Json5ValueSerializer { + type Ok = Json5Value; + type Error = Json5SerError; + type SerializeSeq = Json5SeqSerializer; + type SerializeTuple = Json5SeqSerializer; + type SerializeTupleStruct = Json5SeqSerializer; + type SerializeTupleVariant = Json5TupleVariantSerializer; + type SerializeMap = Json5MapSerializer; + type SerializeStruct = Json5MapSerializer; + type SerializeStructVariant = Json5StructVariantSerializer; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Json5Value::Bool(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(Json5Value::Number(v.to_string())) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(Json5Value::Number(v.to_string())) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(Json5Value::Number(v.to_string())) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Json5Value::Number(v.to_string())) + } + + fn serialize_u8(self, v: u8) -> Result { + Ok(Json5Value::Number(v.to_string())) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(Json5Value::Number(v.to_string())) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(Json5Value::Number(v.to_string())) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Json5Value::Number(v.to_string())) + } + + fn serialize_f32(self, v: f32) -> Result { + self.serialize_f64(v as f64) + } + + fn serialize_f64(self, v: f64) -> Result { + let s = if v.is_nan() { + "NaN".to_string() + } else if v.is_infinite() { + if v.is_sign_positive() { + "Infinity".to_string() + } else { + "-Infinity".to_string() + } + } else { + // Use scientific notation for very large or very small numbers + // to ensure they can be parsed back by json5 + let abs = v.abs(); + if abs != 0.0 && !(1e-6..1e15).contains(&abs) { + format!("{:e}", v) + } else { + v.to_string() + } + }; + Ok(Json5Value::Number(s)) + } + + fn serialize_char(self, v: char) -> Result { + Ok(Json5Value::String(v.to_string())) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Json5Value::String(v.to_string())) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + let arr: Vec = v + .iter() + .map(|b| Json5Value::Number(b.to_string())) + .collect(); + Ok(Json5Value::Array(arr)) + } + + fn serialize_none(self) -> Result { + Ok(Json5Value::Null) + } + + fn serialize_some(self, value: &T) -> Result { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(Json5Value::Null) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Ok(Json5Value::Null) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + Ok(Json5Value::String(variant.to_string())) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result { + let inner = value.serialize(Json5ValueSerializer)?; + let mut map = std::collections::BTreeMap::new(); + map.insert(variant.to_string(), inner); + Ok(Json5Value::Object(map)) + } + + fn serialize_seq(self, len: Option) -> Result { + Ok(Json5SeqSerializer { + items: Vec::with_capacity(len.unwrap_or(0)), + }) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(Json5TupleVariantSerializer { + variant: variant.to_string(), + items: Vec::with_capacity(len), + }) + } + + fn serialize_map(self, len: Option) -> Result { + Ok(Json5MapSerializer { + map: std::collections::BTreeMap::new(), + next_key: None, + _capacity: len.unwrap_or(0), + }) + } + + fn serialize_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_map(Some(len)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(Json5StructVariantSerializer { + variant: variant.to_string(), + map: std::collections::BTreeMap::new(), + _capacity: len, + }) + } +} + +#[derive(Debug)] +struct Json5SerError(String); + +impl std::fmt::Display for Json5SerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for Json5SerError {} + +impl serde::ser::Error for Json5SerError { + fn custom(msg: T) -> Self { + Json5SerError(msg.to_string()) + } +} + +struct Json5SeqSerializer { + items: Vec, +} + +impl serde::ser::SerializeSeq for Json5SeqSerializer { + type Ok = Json5Value; + type Error = Json5SerError; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> { + self.items.push(value.serialize(Json5ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Json5Value::Array(self.items)) + } +} + +impl serde::ser::SerializeTuple for Json5SeqSerializer { + type Ok = Json5Value; + type Error = Json5SerError; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl serde::ser::SerializeTupleStruct for Json5SeqSerializer { + type Ok = Json5Value; + type Error = Json5SerError; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +struct Json5TupleVariantSerializer { + variant: String, + items: Vec, +} + +impl serde::ser::SerializeTupleVariant for Json5TupleVariantSerializer { + type Ok = Json5Value; + type Error = Json5SerError; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> { + self.items.push(value.serialize(Json5ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + let mut map = std::collections::BTreeMap::new(); + map.insert(self.variant, Json5Value::Array(self.items)); + Ok(Json5Value::Object(map)) + } +} + +struct Json5MapSerializer { + map: std::collections::BTreeMap, + next_key: Option, + _capacity: usize, +} + +impl serde::ser::SerializeMap for Json5MapSerializer { + type Ok = Json5Value; + type Error = Json5SerError; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> { + // Serialize key to Json5Value, then extract string + let key_value = key.serialize(Json5ValueSerializer)?; + let key_str = match key_value { + Json5Value::String(s) => s, + Json5Value::Number(n) => n, + _ => { + return Err(Json5SerError( + "Map key must be string or number".to_string(), + )) + } + }; + self.next_key = Some(key_str); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> { + let key = self.next_key.take().ok_or_else(|| { + Json5SerError("serialize_value called without serialize_key".to_string()) + })?; + self.map.insert(key, value.serialize(Json5ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Json5Value::Object(self.map)) + } +} + +impl serde::ser::SerializeStruct for Json5MapSerializer { + type Ok = Json5Value; + type Error = Json5SerError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + self.map + .insert(key.to_string(), value.serialize(Json5ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Json5Value::Object(self.map)) + } +} + +struct Json5StructVariantSerializer { + variant: String, + map: std::collections::BTreeMap, + _capacity: usize, +} + +impl serde::ser::SerializeStructVariant for Json5StructVariantSerializer { + type Ok = Json5Value; + type Error = Json5SerError; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + self.map + .insert(key.to_string(), value.serialize(Json5ValueSerializer)?); + Ok(()) + } + + fn end(self) -> Result { + let mut outer = std::collections::BTreeMap::new(); + outer.insert(self.variant, Json5Value::Object(self.map)); + Ok(Json5Value::Object(outer)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -1123,4 +1603,3198 @@ mod tests { assert!(deserialized.properties.get("Health").unwrap().is_infinite()); assert_eq!(*deserialized.properties.get("WalkSpeed").unwrap(), 16.0); } + + // ========================================================================= + // Sorted JSON5 serializer tests - comprehensive coverage + // ========================================================================= + + mod serializer_tests { + use super::*; + + // ===================================================================== + // Large/Small Number Handling (the main bug we fixed) + // ===================================================================== + + #[test] + fn test_roundtrip_large_number() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: f64, + } + + let original = Data { value: 1e47 }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Should use scientific notation for large numbers + assert!( + json_str.contains("1e47") || json_str.contains("1e+47"), + "Large number should be in scientific notation: {}", + json_str + ); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + #[test] + fn test_roundtrip_very_small_number() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: f64, + } + + let original = Data { value: 1e-100 }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Should use scientific notation for very small numbers + assert!( + json_str.contains("e-"), + "Very small number should be in scientific notation: {}", + json_str + ); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + #[test] + fn test_roundtrip_negative_large_number() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: f64, + } + + let original = Data { value: -1e47 }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + #[test] + fn test_roundtrip_negative_small_number() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: f64, + } + + let original = Data { value: -1e-50 }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + // Test the exact boundaries where we switch to scientific notation + #[test] + fn test_boundary_at_1e15() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + below: f64, + at: f64, + above: f64, + } + + let original = Data { + below: 9.99e14, + at: 1e15, + above: 1.01e15, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // At and above 1e15 should use scientific notation + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.below, original.below); + assert_eq!(deserialized.at, original.at); + assert_eq!(deserialized.above, original.above); + } + + #[test] + fn test_boundary_at_1e_minus_6() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + above: f64, + at: f64, + below: f64, + } + + let original = Data { + above: 1.01e-6, + at: 1e-6, + below: 9.9e-7, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Below 1e-6 should use scientific notation + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.above, original.above); + assert_eq!(deserialized.at, original.at); + assert_eq!(deserialized.below, original.below); + } + + #[test] + fn test_zero_not_scientific() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + num: f64, + } + + let original = Data { num: 0.0 }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Zero should not use scientific notation (check the value part only) + // The output should contain "num: 0" not "num: 0e0" or similar + assert!( + json_str.contains(": 0") && !json_str.contains("0e"), + "Zero should not be in scientific notation: {}", + json_str + ); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.num, 0.0); + } + + #[test] + fn test_normal_numbers_not_scientific() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + a: f64, + b: f64, + c: f64, + } + + let original = Data { + a: 42.5, + b: 1000.0, + c: 0.001, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Normal numbers should not use scientific notation + assert!( + !json_str.contains("e"), + "Normal numbers should not use scientific notation: {}", + json_str + ); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.a, original.a); + assert_eq!(deserialized.b, original.b); + assert_eq!(deserialized.c, original.c); + } + + // ===================================================================== + // Special Float Values (NaN, Infinity) + // ===================================================================== + + #[test] + fn test_nan_roundtrip() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + value: f64, + } + + let original = Data { value: f64::NAN }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!(json_str.contains("NaN"), "Should contain NaN: {}", json_str); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert!(deserialized.value.is_nan()); + } + + #[test] + fn test_infinity_roundtrip() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + value: f64, + } + + let original = Data { + value: f64::INFINITY, + }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!( + json_str.contains("Infinity"), + "Should contain Infinity: {}", + json_str + ); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert!(deserialized.value.is_infinite() && deserialized.value.is_sign_positive()); + } + + #[test] + fn test_negative_infinity_roundtrip() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + value: f64, + } + + let original = Data { + value: f64::NEG_INFINITY, + }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!( + json_str.contains("-Infinity"), + "Should contain -Infinity: {}", + json_str + ); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert!(deserialized.value.is_infinite() && deserialized.value.is_sign_negative()); + } + + #[test] + fn test_mixed_special_floats() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + nan: f64, + pos_inf: f64, + neg_inf: f64, + normal: f64, + large: f64, + } + + let original = Data { + nan: f64::NAN, + pos_inf: f64::INFINITY, + neg_inf: f64::NEG_INFINITY, + normal: 42.5, + large: 1e47, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert!(deserialized.nan.is_nan()); + assert!(deserialized.pos_inf.is_infinite() && deserialized.pos_inf.is_sign_positive()); + assert!(deserialized.neg_inf.is_infinite() && deserialized.neg_inf.is_sign_negative()); + assert_eq!(deserialized.normal, 42.5); + assert_eq!(deserialized.large, 1e47); + } + + // ===================================================================== + // Integer Types + // ===================================================================== + + #[test] + fn test_integer_types() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + i8_val: i8, + i16_val: i16, + i32_val: i32, + i64_val: i64, + u8_val: u8, + u16_val: u16, + u32_val: u32, + u64_val: u64, + } + + let original = Data { + i8_val: -128, + i16_val: -32768, + i32_val: -2147483648, + i64_val: -9223372036854775808, + u8_val: 255, + u16_val: 65535, + u32_val: 4294967295, + u64_val: 18446744073709551615, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + // ===================================================================== + // Sorted Keys + // ===================================================================== + + #[test] + fn test_output_sorted_keys() { + #[derive(Debug, Serialize)] + struct Data { + zebra: i32, + apple: i32, + mango: i32, + } + + let data = Data { + zebra: 3, + apple: 1, + mango: 2, + }; + + let serialized = to_vec_pretty_sorted(&data).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Keys should be sorted alphabetically + let apple_pos = json_str.find("apple").unwrap(); + let mango_pos = json_str.find("mango").unwrap(); + let zebra_pos = json_str.find("zebra").unwrap(); + + assert!(apple_pos < mango_pos); + assert!(mango_pos < zebra_pos); + } + + #[test] + fn test_nested_objects_sorted() { + #[derive(Debug, Serialize)] + struct Inner { + z: i32, + a: i32, + } + + #[derive(Debug, Serialize)] + struct Outer { + z_inner: Inner, + a_inner: Inner, + } + + let data = Outer { + z_inner: Inner { z: 1, a: 2 }, + a_inner: Inner { z: 3, a: 4 }, + }; + + let serialized = to_vec_pretty_sorted(&data).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Outer keys should be sorted + let a_inner_pos = json_str.find("a_inner").unwrap(); + let z_inner_pos = json_str.find("z_inner").unwrap(); + assert!(a_inner_pos < z_inner_pos); + + // Inner keys should also be sorted (check first occurrence of 'a:' vs 'z:') + // This is a bit tricky since 'a' appears in key names too + assert!(json_str.contains("a_inner")); + assert!(json_str.contains("z_inner")); + } + + #[test] + fn test_map_keys_sorted() { + let mut map = std::collections::HashMap::new(); + map.insert("zebra", 3); + map.insert("apple", 1); + map.insert("mango", 2); + + let serialized = to_vec_pretty_sorted(&map).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let apple_pos = json_str.find("apple").unwrap(); + let mango_pos = json_str.find("mango").unwrap(); + let zebra_pos = json_str.find("zebra").unwrap(); + + assert!(apple_pos < mango_pos); + assert!(mango_pos < zebra_pos); + } + + // ===================================================================== + // Pretty Formatting + // ===================================================================== + + #[test] + fn test_output_pretty_format() { + #[derive(Debug, Serialize)] + struct Data { + key: String, + } + + let data = Data { + key: "value".to_string(), + }; + + let serialized = to_vec_pretty_sorted(&data).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Should have newlines and indentation + assert!(json_str.contains('\n')); + assert!(json_str.contains(" ")); // indentation + } + + #[test] + fn test_nested_indentation() { + #[derive(Debug, Serialize)] + struct Inner { + value: i32, + } + + #[derive(Debug, Serialize)] + struct Outer { + inner: Inner, + } + + let data = Outer { + inner: Inner { value: 42 }, + }; + + let serialized = to_vec_pretty_sorted(&data).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Should have multiple levels of indentation + assert!(json_str.contains(" ")); // 4 spaces for nested + } + + // ===================================================================== + // String Escaping + // ===================================================================== + + #[test] + fn test_write_to_escapes_special_chars() { + let value = Json5Value::String("hello\nworld\t\"test\"".to_string()); + let mut output = String::new(); + value.write_to(&mut output, 0); + assert_eq!(output, r#""hello\nworld\t\"test\"""#); + } + + #[test] + fn test_string_escaping_backslash() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + path: String, + } + + let original = Data { + path: r"C:\Users\test".to_string(), + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!( + json_str.contains(r"\\"), + "Backslash should be escaped: {}", + json_str + ); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.path, original.path); + } + + #[test] + fn test_string_escaping_control_chars() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + text: String, + } + + let original = Data { + text: "line1\nline2\rline3\ttab".to_string(), + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!(json_str.contains(r"\n")); + assert!(json_str.contains(r"\r")); + assert!(json_str.contains(r"\t")); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.text, original.text); + } + + #[test] + fn test_unicode_strings() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + emoji: String, + chinese: String, + arabic: String, + } + + let original = Data { + emoji: "Hello 👋 World 🌍".to_string(), + chinese: "你好世界".to_string(), + arabic: "مرحبا بالعالم".to_string(), + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + // ===================================================================== + // Identifier Quoting + // ===================================================================== + + #[test] + fn test_valid_identifier_unquoted() { + assert!(is_valid_identifier("foo")); + assert!(is_valid_identifier("_bar")); + assert!(is_valid_identifier("$baz")); + assert!(is_valid_identifier("foo123")); + assert!(is_valid_identifier("_")); + assert!(is_valid_identifier("$")); + assert!(is_valid_identifier("camelCase")); + assert!(is_valid_identifier("snake_case")); + } + + #[test] + fn test_invalid_identifier_quoted() { + assert!(!is_valid_identifier("")); // empty + assert!(!is_valid_identifier("123")); // starts with digit + assert!(!is_valid_identifier("foo-bar")); // contains dash + assert!(!is_valid_identifier("foo bar")); // contains space + assert!(!is_valid_identifier("foo.bar")); // contains dot + assert!(!is_valid_identifier("1foo")); // starts with digit + assert!(!is_valid_identifier("foo@bar")); // contains @ + } + + #[test] + fn test_keys_with_special_chars_quoted() { + let mut map = std::collections::HashMap::new(); + map.insert("normal_key", 1); + map.insert("key-with-dash", 2); + map.insert("key with space", 3); + + let serialized = to_vec_pretty_sorted(&map).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Normal key should be unquoted + assert!( + json_str.contains("normal_key:"), + "Normal key should be unquoted: {}", + json_str + ); + + // Special keys should be quoted + assert!( + json_str.contains("\"key-with-dash\""), + "Dashed key should be quoted: {}", + json_str + ); + assert!( + json_str.contains("\"key with space\""), + "Spaced key should be quoted: {}", + json_str + ); + } + + #[test] + fn test_keys_with_escaped_chars_roundtrip() { + // Test keys that contain characters requiring escaping + let mut map = std::collections::HashMap::new(); + map.insert("key\"with\"quotes".to_string(), 1); + map.insert("key\\with\\backslash".to_string(), 2); + map.insert("key\nwith\nnewline".to_string(), 3); + map.insert("key\twith\ttab".to_string(), 4); + + let serialized = to_vec_pretty_sorted(&map).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Verify escaping is present + assert!( + json_str.contains(r#"\""#), + "Quotes in key should be escaped: {}", + json_str + ); + assert!( + json_str.contains(r"\\"), + "Backslash in key should be escaped: {}", + json_str + ); + assert!( + json_str.contains(r"\n"), + "Newline in key should be escaped: {}", + json_str + ); + assert!( + json_str.contains(r"\t"), + "Tab in key should be escaped: {}", + json_str + ); + + // Verify roundtrip works + let deserialized: std::collections::HashMap = from_str(&json_str).unwrap(); + assert_eq!(deserialized.get("key\"with\"quotes"), Some(&1)); + assert_eq!(deserialized.get("key\\with\\backslash"), Some(&2)); + assert_eq!(deserialized.get("key\nwith\nnewline"), Some(&3)); + assert_eq!(deserialized.get("key\twith\ttab"), Some(&4)); + } + + #[test] + fn test_keys_with_control_chars_roundtrip() { + // Test keys with control characters (should be escaped as \uXXXX) + let mut map = std::collections::HashMap::new(); + map.insert("key\x00null".to_string(), 1); // NUL character + map.insert("key\x07bell".to_string(), 2); // BEL character + + let serialized = to_vec_pretty_sorted(&map).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Control characters should be escaped as \uXXXX + assert!( + json_str.contains(r"\u0000") || json_str.contains(r"\u0007"), + "Control characters in key should be escaped: {}", + json_str + ); + + // Verify roundtrip works + let deserialized: std::collections::HashMap = from_str(&json_str).unwrap(); + assert_eq!(deserialized.get("key\x00null"), Some(&1)); + assert_eq!(deserialized.get("key\x07bell"), Some(&2)); + } + + #[test] + fn test_write_escaped_string_function() { + // Direct test of the escaping function + let mut output = String::new(); + write_escaped_string(&mut output, r#"hello"world"#); + assert_eq!(output, r#""hello\"world""#); + + let mut output = String::new(); + write_escaped_string(&mut output, "line1\nline2"); + assert_eq!(output, r#""line1\nline2""#); + + let mut output = String::new(); + write_escaped_string(&mut output, r"path\to\file"); + assert_eq!(output, r#""path\\to\\file""#); + } + + // ===================================================================== + // Arrays and Empty Collections + // ===================================================================== + + #[test] + fn test_array_roundtrip() { + let original: Vec = vec![1, 2, 3, 4, 5]; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Vec = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + #[test] + fn test_empty_array() { + let original: Vec = vec![]; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!(json_str.contains("[]")); + + let deserialized: Vec = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + #[test] + fn test_empty_object() { + let original: std::collections::HashMap = std::collections::HashMap::new(); + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!(json_str.contains("{}")); + + let deserialized: std::collections::HashMap = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + #[test] + fn test_nested_arrays() { + let original: Vec> = vec![vec![1, 2], vec![3, 4], vec![5, 6]]; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Vec> = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + // ===================================================================== + // Optional Values + // ===================================================================== + + #[test] + fn test_option_some() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: Option, + } + + let original = Data { value: Some(42) }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + #[test] + fn test_option_none() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: Option, + } + + let original = Data { value: None }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!(json_str.contains("null")); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + // ===================================================================== + // Bool and Null + // ===================================================================== + + #[test] + fn test_bool_values() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + t: bool, + f: bool, + } + + let original = Data { t: true, f: false }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert!(json_str.contains("true")); + assert!(json_str.contains("false")); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + #[test] + fn test_null_value() { + let original: Option = None; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + assert_eq!(json_str.trim(), "null"); + } + + // ===================================================================== + // Complex/Real-World Structures + // ===================================================================== + + #[test] + fn test_roundtrip_complex_model() { + #[derive(Debug, Serialize, Deserialize)] + struct Model { + #[serde(rename = "className")] + class_name: String, + properties: std::collections::BTreeMap, + } + + let mut properties = std::collections::BTreeMap::new(); + properties.insert("Value".to_string(), 1e47); + properties.insert("Normal".to_string(), 42.5); + properties.insert("Tiny".to_string(), 1e-50); + + let original = Model { + class_name: "NumberValue".to_string(), + properties, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Model = from_str(&json_str).unwrap(); + assert_eq!(deserialized.class_name, "NumberValue"); + assert_eq!(*deserialized.properties.get("Value").unwrap(), 1e47); + assert_eq!(*deserialized.properties.get("Normal").unwrap(), 42.5); + assert_eq!(*deserialized.properties.get("Tiny").unwrap(), 1e-50); + } + + #[test] + fn test_roblox_model_like_structure() { + // Mimics the actual structure that caused the original bug + #[derive(Debug, Serialize, Deserialize)] + struct JsonModel { + #[serde(rename = "className")] + class_name: String, + #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")] + properties: std::collections::BTreeMap, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + children: Vec, + } + + let mut properties = std::collections::BTreeMap::new(); + properties.insert("Value".to_string(), serde_json::json!(1e47)); + + let model = JsonModel { + class_name: "NumberValue".to_string(), + properties, + children: vec![], + }; + + let serialized = to_vec_pretty_sorted(&model).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Should be parseable + let _: JsonModel = from_str(&json_str).unwrap(); + } + + #[test] + fn test_deeply_nested_structure() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Level3 { + value: f64, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Level2 { + level3: Level3, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Level1 { + level2: Level2, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Root { + level1: Level1, + } + + let original = Root { + level1: Level1 { + level2: Level2 { + level3: Level3 { value: 1e47 }, + }, + }, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Root = from_str(&json_str).unwrap(); + assert_eq!(deserialized.level1.level2.level3.value, 1e47); + } + + // ===================================================================== + // Enum Variants + // ===================================================================== + + #[test] + fn test_enum_unit_variant() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + enum Status { + Active, + Inactive, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + status: Status, + } + + let original = Data { + status: Status::Active, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + #[test] + fn test_enum_newtype_variant() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + enum Value { + Number(f64), + Text(String), + } + + let original = Value::Number(1e47); + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Value = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + // ===================================================================== + // Char and Bytes + // ===================================================================== + + #[test] + fn test_char_serialization() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + ch: char, + } + + let original = Data { ch: 'X' }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized, original); + } + + // ===================================================================== + // Regression Tests + // ===================================================================== + + #[test] + fn test_regression_numbervalue_1e47() { + // This is the exact case that caused the original bug + #[derive(Debug, Serialize, Deserialize)] + struct NumberValue { + #[serde(rename = "className")] + class_name: String, + properties: std::collections::BTreeMap, + } + + let mut properties = std::collections::BTreeMap::new(); + properties.insert("Value".to_string(), 1e47); + + let original = NumberValue { + class_name: "NumberValue".to_string(), + properties, + }; + + // This should not panic or error + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // And should be parseable + let deserialized: NumberValue = from_str(&json_str).unwrap(); + assert_eq!(deserialized.properties["Value"], 1e47); + } + + #[test] + fn test_f64_max_min() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + max: f64, + min: f64, + } + + let original = Data { + max: f64::MAX, + min: f64::MIN, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.max, f64::MAX); + assert_eq!(deserialized.min, f64::MIN); + } + + #[test] + fn test_f64_epsilon_and_min_positive() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + epsilon: f64, + min_positive: f64, + } + + let original = Data { + epsilon: f64::EPSILON, + min_positive: f64::MIN_POSITIVE, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.epsilon, f64::EPSILON); + assert_eq!(deserialized.min_positive, f64::MIN_POSITIVE); + } + } + + // ========================================================================= + // JSON5-specific parsing edge cases (using json5 crate) + // These test that we can correctly parse JSON5 input formats + // ========================================================================= + + mod json5_parsing_edge_cases { + use super::*; + + // ===================================================================== + // Number Parsing Edge Cases + // ===================================================================== + + #[test] + fn test_parse_hexadecimal() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i64, + } + + let result: Data = from_str(r#"{ value: 0xFF }"#).unwrap(); + assert_eq!(result.value, 255); + + let result: Data = from_str(r#"{ value: 0xDEADBEEF }"#).unwrap(); + assert_eq!(result.value, 0xDEADBEEF); + } + + #[test] + fn test_parse_negative_hexadecimal() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i64, + } + + let result: Data = from_str(r#"{ value: -0xFF }"#).unwrap(); + assert_eq!(result.value, -255); + } + + #[test] + fn test_parse_leading_decimal_point() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: .5 }"#).unwrap(); + assert_eq!(result.value, 0.5); + + let result: Data = from_str(r#"{ value: .123 }"#).unwrap(); + assert_eq!(result.value, 0.123); + } + + #[test] + fn test_parse_trailing_decimal_point() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: 5. }"#).unwrap(); + assert_eq!(result.value, 5.0); + + let result: Data = from_str(r#"{ value: 123. }"#).unwrap(); + assert_eq!(result.value, 123.0); + } + + #[test] + fn test_parse_positive_sign() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: +5 }"#).unwrap(); + assert_eq!(result.value, 5.0); + + let result: Data = from_str(r#"{ value: +Infinity }"#).unwrap(); + assert!(result.value.is_infinite() && result.value.is_sign_positive()); + } + + #[test] + fn test_parse_negative_zero() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: -0 }"#).unwrap(); + assert_eq!(result.value, 0.0); + // Note: -0.0 == 0.0 in Rust, but we can check the sign bit + assert!(result.value.is_sign_negative() || result.value == 0.0); + } + + #[test] + fn test_parse_exponent_variations() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: f64, + } + + // Uppercase E + let result: Data = from_str(r#"{ value: 1E10 }"#).unwrap(); + assert_eq!(result.value, 1e10); + + // Positive exponent with + + let result: Data = from_str(r#"{ value: 1e+10 }"#).unwrap(); + assert_eq!(result.value, 1e10); + + // Negative exponent + let result: Data = from_str(r#"{ value: 1e-10 }"#).unwrap(); + assert_eq!(result.value, 1e-10); + + // Zero exponent + let result: Data = from_str(r#"{ value: 5e0 }"#).unwrap(); + assert_eq!(result.value, 5.0); + } + + // ===================================================================== + // String Parsing Edge Cases + // ===================================================================== + + #[test] + fn test_parse_single_quoted_strings() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: 'hello world' }"#).unwrap(); + assert_eq!(result.value, "hello world"); + } + + #[test] + fn test_parse_single_quoted_with_double_quote() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: 'say "hello"' }"#).unwrap(); + assert_eq!(result.value, r#"say "hello""#); + } + + #[test] + fn test_parse_escaped_single_quote() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: 'it\'s working' }"#).unwrap(); + assert_eq!(result.value, "it's working"); + } + + #[test] + fn test_parse_multi_line_string() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // JSON5 allows escaped newlines in strings + let result: Data = from_str("{ value: 'line1\\\nline2' }").unwrap(); + assert_eq!(result.value, "line1line2"); + } + + #[test] + fn test_parse_hex_escape_sequences() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: "\x48\x65\x6C\x6C\x6F" }"#).unwrap(); + assert_eq!(result.value, "Hello"); + } + + #[test] + fn test_parse_unicode_escape_sequences() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: "\u0048\u0065\u006C\u006C\u006F" }"#).unwrap(); + assert_eq!(result.value, "Hello"); + + // Emoji + let result: Data = from_str(r#"{ value: "\uD83D\uDE00" }"#).unwrap(); + assert_eq!(result.value, "😀"); + } + + // ===================================================================== + // Object Parsing Edge Cases + // ===================================================================== + + #[test] + fn test_parse_unquoted_keys() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + hello: String, + world: i32, + } + + let result: Data = from_str(r#"{ hello: "test", world: 42 }"#).unwrap(); + assert_eq!(result.hello, "test"); + assert_eq!(result.world, 42); + } + + #[test] + fn test_parse_single_quoted_keys() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + #[serde(rename = "my-key")] + my_key: String, + } + + let result: Data = from_str(r#"{ 'my-key': "value" }"#).unwrap(); + assert_eq!(result.my_key, "value"); + } + + #[test] + fn test_parse_reserved_words_as_keys() { + // JSON5 allows reserved words as unquoted keys + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + r#while: i32, + r#for: i32, + r#if: i32, + } + + let result: Data = from_str(r#"{ while: 1, for: 2, if: 3 }"#).unwrap(); + assert_eq!(result.r#while, 1); + assert_eq!(result.r#for, 2); + assert_eq!(result.r#if, 3); + } + + #[test] + fn test_parse_trailing_comma_object() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + a: i32, + b: i32, + } + + let result: Data = from_str(r#"{ a: 1, b: 2, }"#).unwrap(); + assert_eq!(result.a, 1); + assert_eq!(result.b, 2); + } + + #[test] + fn test_parse_nested_trailing_commas() { + #[derive(Debug, Deserialize, PartialEq)] + struct Inner { + x: i32, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct Outer { + inner: Inner, + value: i32, + } + + let result: Outer = from_str(r#"{ inner: { x: 1, }, value: 2, }"#).unwrap(); + assert_eq!(result.inner.x, 1); + assert_eq!(result.value, 2); + } + + // ===================================================================== + // Array Parsing Edge Cases + // ===================================================================== + + #[test] + fn test_parse_trailing_comma_array() { + let result: Vec = from_str(r#"[1, 2, 3,]"#).unwrap(); + assert_eq!(result, vec![1, 2, 3]); + } + + #[test] + fn test_parse_nested_arrays_with_trailing_commas() { + let result: Vec> = from_str(r#"[[1, 2,], [3, 4,],]"#).unwrap(); + assert_eq!(result, vec![vec![1, 2], vec![3, 4]]); + } + + // ===================================================================== + // Comment Parsing Edge Cases + // ===================================================================== + + #[test] + fn test_parse_single_line_comment() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + let result: Data = from_str( + r#"{ + // This is a comment + value: 42 + }"#, + ) + .unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_parse_multi_line_comment() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + let result: Data = from_str( + r#"{ + /* This is a + multi-line comment */ + value: 42 + }"#, + ) + .unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_parse_comment_after_value() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + a: i32, + b: i32, + } + + let result: Data = from_str( + r#"{ + a: 1, // comment after value + b: 2 /* another comment */ + }"#, + ) + .unwrap(); + assert_eq!(result.a, 1); + assert_eq!(result.b, 2); + } + + #[test] + fn test_parse_comment_in_array() { + let result: Vec = from_str( + r#"[ + 1, // first + 2, /* second */ + 3 + ]"#, + ) + .unwrap(); + assert_eq!(result, vec![1, 2, 3]); + } + + // ===================================================================== + // Whitespace Edge Cases + // ===================================================================== + + #[test] + fn test_parse_various_whitespace() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + // Tab, newline, carriage return + let result: Data = from_str("{\t\n\rvalue\t:\n42\r}").unwrap(); + assert_eq!(result.value, 42); + } + + // ===================================================================== + // Mixed JSON5 Features + // ===================================================================== + + #[test] + fn test_parse_complex_json5() { + #[derive(Debug, Deserialize)] + struct Config { + name: String, + #[serde(rename = "maxValue")] + max_value: f64, + enabled: bool, + items: Vec, + } + + let result: Config = from_str( + r#"{ + // Configuration file + name: 'test-config', // single quotes + maxValue: Infinity, // special float + enabled: true, + items: [ + .5, // leading decimal + 1., // trailing decimal + 0xFF, // hex + 1e10, // scientific + NaN, // NaN + ], // trailing comma + }"#, + ) + .unwrap(); + + assert_eq!(result.name, "test-config"); + assert!(result.max_value.is_infinite()); + assert!(result.enabled); + assert_eq!(result.items[0], 0.5); + assert_eq!(result.items[1], 1.0); + assert_eq!(result.items[2], 255.0); + assert_eq!(result.items[3], 1e10); + assert!(result.items[4].is_nan()); + } + + // ===================================================================== + // Negative Zero Serialization Roundtrip + // ===================================================================== + + #[test] + fn test_negative_zero_roundtrip() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + value: f64, + } + + let original = Data { value: -0.0 }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + // -0.0 == 0.0 in IEEE 754, but the sign bit should be preserved + assert_eq!(deserialized.value, 0.0); + } + + // ===================================================================== + // Subnormal Numbers + // ===================================================================== + + #[test] + fn test_subnormal_numbers() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + tiny: f64, + } + + // Subnormal (denormalized) number + let original = Data { tiny: 5e-324 }; + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.tiny, original.tiny); + } + } + + // ========================================================================= + // JSON5 Spec Conformance Tests (https://spec.json5.org/) + // ========================================================================= + // + // Comprehensive tests based on the official JSON5 specification to ensure + // our serializer produces valid JSON5 and can parse all valid JSON5 input. + + mod json5_spec_conformance { + use super::*; + + // ===================================================================== + // Section 5.1: Escape Sequences (Table 1) + // https://spec.json5.org/#escapes + // ===================================================================== + + #[test] + fn test_escape_quotation_mark() { + // \" - Quotation mark (U+0022) + let mut output = String::new(); + write_escaped_string(&mut output, "say \"hello\""); + assert_eq!(output, r#""say \"hello\"""#); + } + + #[test] + fn test_escape_reverse_solidus() { + // \\ - Reverse solidus (U+005C) + let mut output = String::new(); + write_escaped_string(&mut output, r"path\to\file"); + assert_eq!(output, r#""path\\to\\file""#); + } + + #[test] + fn test_escape_backspace() { + // \b - Backspace (U+0008) + let mut output = String::new(); + write_escaped_string(&mut output, "hello\u{0008}world"); + assert_eq!(output, r#""hello\bworld""#); + } + + #[test] + fn test_escape_form_feed() { + // \f - Form feed (U+000C) + let mut output = String::new(); + write_escaped_string(&mut output, "page\u{000C}break"); + assert_eq!(output, r#""page\fbreak""#); + } + + #[test] + fn test_escape_line_feed() { + // \n - Line feed (U+000A) + let mut output = String::new(); + write_escaped_string(&mut output, "line1\nline2"); + assert_eq!(output, r#""line1\nline2""#); + } + + #[test] + fn test_escape_carriage_return() { + // \r - Carriage return (U+000D) + let mut output = String::new(); + write_escaped_string(&mut output, "line1\rline2"); + assert_eq!(output, r#""line1\rline2""#); + } + + #[test] + fn test_escape_horizontal_tab() { + // \t - Horizontal tab (U+0009) + let mut output = String::new(); + write_escaped_string(&mut output, "col1\tcol2"); + assert_eq!(output, r#""col1\tcol2""#); + } + + #[test] + fn test_escape_vertical_tab() { + // \v - Vertical tab (U+000B) + let mut output = String::new(); + write_escaped_string(&mut output, "hello\u{000B}world"); + assert_eq!(output, r#""hello\vworld""#); + } + + #[test] + fn test_escape_null_character() { + // \0 - Null (U+0000) - we use \u0000 for safety + let mut output = String::new(); + write_escaped_string(&mut output, "null\u{0000}char"); + assert!( + output.contains("\\u0000") || output.contains("\\0"), + "Null should be escaped: {}", + output + ); + } + + #[test] + fn test_all_standard_escapes_combined() { + // Test all standard escapes in one string + let input = "\"\\\u{0008}\u{000C}\n\r\t\u{000B}"; + let mut output = String::new(); + write_escaped_string(&mut output, input); + + assert!(output.contains("\\\""), "Quote not escaped"); + assert!(output.contains("\\\\"), "Backslash not escaped"); + assert!(output.contains("\\b"), "Backspace not escaped"); + assert!(output.contains("\\f"), "Form feed not escaped"); + assert!(output.contains("\\n"), "Line feed not escaped"); + assert!(output.contains("\\r"), "Carriage return not escaped"); + assert!(output.contains("\\t"), "Tab not escaped"); + assert!(output.contains("\\v"), "Vertical tab not escaped"); + } + + // ===================================================================== + // Section 5.2: Paragraph and Line Separators + // "JSON5 generators should escape these code points in strings." + // ===================================================================== + + #[test] + fn test_escape_line_separator_u2028() { + // U+2028 - Line separator (generators SHOULD escape) + let mut output = String::new(); + write_escaped_string(&mut output, "line\u{2028}separator"); + assert!( + output.contains("\\u2028"), + "Line separator U+2028 should be escaped: {}", + output + ); + } + + #[test] + fn test_escape_paragraph_separator_u2029() { + // U+2029 - Paragraph separator (generators SHOULD escape) + let mut output = String::new(); + write_escaped_string(&mut output, "paragraph\u{2029}separator"); + assert!( + output.contains("\\u2029"), + "Paragraph separator U+2029 should be escaped: {}", + output + ); + } + + #[test] + fn test_parse_unescaped_line_separator() { + // JSON5 parsers should accept unescaped U+2028 + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // Construct JSON5 with literal U+2028 + let json5 = "{ value: \"line\u{2028}separator\" }"; + let result: Data = from_str(json5).unwrap(); + assert_eq!(result.value, "line\u{2028}separator"); + } + + #[test] + fn test_parse_unescaped_paragraph_separator() { + // JSON5 parsers should accept unescaped U+2029 + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let json5 = "{ value: \"paragraph\u{2029}separator\" }"; + let result: Data = from_str(json5).unwrap(); + assert_eq!(result.value, "paragraph\u{2029}separator"); + } + + // ===================================================================== + // Section 5.1: Hex Escapes (\xHH) + // ===================================================================== + + #[test] + fn test_parse_hex_escape_lowercase() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: "\x48\x65\x6c\x6c\x6f" }"#).unwrap(); + assert_eq!(result.value, "Hello"); + } + + #[test] + fn test_parse_hex_escape_uppercase() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: "\x48\x45\x4C\x4C\x4F" }"#).unwrap(); + assert_eq!(result.value, "HELLO"); + } + + // ===================================================================== + // Section 5.1: Unicode Escapes (\uHHHH) + // ===================================================================== + + #[test] + fn test_parse_unicode_escape_bmp() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // Basic Multilingual Plane characters + let result: Data = from_str(r#"{ value: "\u0048\u0065\u006C\u006C\u006F" }"#).unwrap(); + assert_eq!(result.value, "Hello"); + } + + #[test] + fn test_parse_unicode_escape_surrogate_pair() { + // Characters outside BMP use surrogate pairs + // 🎼 (U+1F3BC) = \uD83C\uDFBC + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: "\uD83C\uDFBC" }"#).unwrap(); + assert_eq!(result.value, "🎼"); + } + + #[test] + fn test_parse_unicode_escape_emoji() { + // 😀 (U+1F600) = \uD83D\uDE00 + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: "\uD83D\uDE00" }"#).unwrap(); + assert_eq!(result.value, "😀"); + } + + // ===================================================================== + // Section 6: Numbers + // https://spec.json5.org/#numbers + // ===================================================================== + + #[test] + fn test_number_hexadecimal_lowercase() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i64, + } + + let result: Data = from_str(r#"{ value: 0xdecaf }"#).unwrap(); + assert_eq!(result.value, 0xdecaf); + } + + #[test] + fn test_number_hexadecimal_uppercase() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i64, + } + + let result: Data = from_str(r#"{ value: 0XDECAF }"#).unwrap(); + assert_eq!(result.value, 0xDECAF); + } + + #[test] + fn test_number_hexadecimal_mixed_case() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i64, + } + + let result: Data = from_str(r#"{ value: 0xDeAdBeEf }"#).unwrap(); + assert_eq!(result.value, 0xDEADBEEF); + } + + #[test] + fn test_number_negative_hexadecimal() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i64, + } + + let result: Data = from_str(r#"{ value: -0xC0FFEE }"#).unwrap(); + assert_eq!(result.value, -0xC0FFEE); + } + + #[test] + fn test_number_leading_decimal_point() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: .8675309 }"#).unwrap(); + assert!((result.value - 0.8675309).abs() < 1e-10); + } + + #[test] + fn test_number_trailing_decimal_point() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: 8675309. }"#).unwrap(); + assert_eq!(result.value, 8675309.0); + } + + #[test] + fn test_number_positive_infinity() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: Infinity }"#).unwrap(); + assert!(result.value.is_infinite() && result.value.is_sign_positive()); + } + + #[test] + fn test_number_positive_infinity_with_plus() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: +Infinity }"#).unwrap(); + assert!(result.value.is_infinite() && result.value.is_sign_positive()); + } + + #[test] + fn test_number_negative_infinity() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: -Infinity }"#).unwrap(); + assert!(result.value.is_infinite() && result.value.is_sign_negative()); + } + + #[test] + fn test_number_nan() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: NaN }"#).unwrap(); + assert!(result.value.is_nan()); + } + + #[test] + fn test_number_positive_sign() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + let result: Data = from_str(r#"{ value: +42 }"#).unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_number_exponent_uppercase_e() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: 1E10 }"#).unwrap(); + assert_eq!(result.value, 1e10); + } + + #[test] + fn test_number_exponent_with_positive_sign() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + let result: Data = from_str(r#"{ value: 1e+10 }"#).unwrap(); + assert_eq!(result.value, 1e10); + } + + // ===================================================================== + // Section 3: Objects + // https://spec.json5.org/#objects + // ===================================================================== + + #[test] + fn test_object_unquoted_identifier_keys() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + width: i32, + height: i32, + } + + let result: Data = from_str(r#"{ width: 1920, height: 1080 }"#).unwrap(); + assert_eq!(result.width, 1920); + assert_eq!(result.height, 1080); + } + + #[test] + fn test_object_single_trailing_comma() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + width: i32, + height: i32, + } + + let result: Data = from_str(r#"{ width: 1920, height: 1080, }"#).unwrap(); + assert_eq!(result.width, 1920); + assert_eq!(result.height, 1080); + } + + #[test] + fn test_object_single_quoted_key() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + #[serde(rename = "aspect-ratio")] + aspect_ratio: String, + } + + let result: Data = from_str(r#"{ 'aspect-ratio': '16:9' }"#).unwrap(); + assert_eq!(result.aspect_ratio, "16:9"); + } + + #[test] + fn test_object_double_quoted_key() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + #[serde(rename = "aspect-ratio")] + aspect_ratio: String, + } + + let result: Data = from_str(r#"{ "aspect-ratio": "16:9" }"#).unwrap(); + assert_eq!(result.aspect_ratio, "16:9"); + } + + #[test] + fn test_object_nested() { + #[derive(Debug, Deserialize, PartialEq)] + struct Inner { + width: i32, + height: i32, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct Outer { + image: Inner, + } + + let result: Outer = from_str( + r#"{ + image: { + width: 1920, + height: 1080, + } + }"#, + ) + .unwrap(); + assert_eq!(result.image.width, 1920); + assert_eq!(result.image.height, 1080); + } + + // ===================================================================== + // Section 4: Arrays + // https://spec.json5.org/#arrays + // ===================================================================== + + #[test] + fn test_array_trailing_comma() { + let result: Vec = from_str(r#"[1, 2, 3,]"#).unwrap(); + assert_eq!(result, vec![1, 2, 3]); + } + + #[test] + fn test_array_mixed_types() { + // "There is no requirement that the values in an array be of the same type." + // Use serde_json::Value which can hold any type + let result: Vec = from_str(r#"[1, true, 'three',]"#).unwrap(); + assert_eq!(result.len(), 3); + assert_eq!(result[0], 1); + assert_eq!(result[1], true); + assert_eq!(result[2], "three"); + } + + #[test] + fn test_array_nested_with_trailing_commas() { + let result: Vec> = from_str(r#"[[1, 2,], [3, 4,],]"#).unwrap(); + assert_eq!(result, vec![vec![1, 2], vec![3, 4]]); + } + + // ===================================================================== + // Section 5: Strings + // https://spec.json5.org/#strings + // ===================================================================== + + #[test] + fn test_string_single_quoted() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: 'single quoted' }"#).unwrap(); + assert_eq!(result.value, "single quoted"); + } + + #[test] + fn test_string_single_quoted_with_double_quotes() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: 'I can use "double quotes" here' }"#).unwrap(); + assert_eq!(result.value, r#"I can use "double quotes" here"#); + } + + #[test] + fn test_string_multiline_escaped() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // Line continuation - backslash followed by newline is removed + let result: Data = from_str("{ value: 'Look, Mom! \\\nNo newlines!' }").unwrap(); + assert_eq!(result.value, "Look, Mom! No newlines!"); + } + + // ===================================================================== + // Section 7: Comments + // https://spec.json5.org/#comments + // ===================================================================== + + #[test] + fn test_comment_single_line() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + let result: Data = from_str( + r#"{ + // This is a single line comment. + value: 42 + }"#, + ) + .unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_comment_multi_line() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + let result: Data = from_str( + r#"{ + /* This is a multi- + line comment. */ + value: 42 + }"#, + ) + .unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_comment_inline() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + a: i32, + b: i32, + } + + let result: Data = from_str( + r#"{ + a: 1, // inline comment + b: 2 /* another */ + }"#, + ) + .unwrap(); + assert_eq!(result.a, 1); + assert_eq!(result.b, 2); + } + + // ===================================================================== + // Section 8: White Space + // https://spec.json5.org/#white-space + // ===================================================================== + + #[test] + fn test_whitespace_horizontal_tab() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + let result: Data = from_str("{\tvalue:\t42\t}").unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_whitespace_vertical_tab() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + let result: Data = from_str("{\u{000B}value:\u{000B}42\u{000B}}").unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_whitespace_form_feed() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + let result: Data = from_str("{\u{000C}value:\u{000C}42\u{000C}}").unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_whitespace_non_breaking_space() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + // U+00A0 Non-breaking space + let result: Data = from_str("{\u{00A0}value:\u{00A0}42\u{00A0}}").unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_whitespace_bom() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + // U+FEFF Byte order mark + let result: Data = from_str("\u{FEFF}{ value: 42 }").unwrap(); + assert_eq!(result.value, 42); + } + + // ===================================================================== + // Section 1.2: Short Example from Spec + // Complete example that uses many JSON5 features + // ===================================================================== + + #[test] + fn test_spec_short_example() { + // This is the example from the JSON5 spec Section 1.2 + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct Example { + unquoted: String, + single_quotes: String, + line_breaks: String, + hexadecimal: i64, + leading_decimal_point: f64, + and_trailing: f64, + positive_sign: i32, + trailing_comma: String, + and_in: Vec, + #[serde(rename = "backwardsCompatible")] + backwards_compatible: String, + } + + let result: Example = from_str( + r#"{ + // comments + unquoted: 'and you can quote me on that', + singleQuotes: 'I can use "double quotes" here', + lineBreaks: "Look, Mom! \ +No \\n's!", + hexadecimal: 0xdecaf, + leadingDecimalPoint: .8675309, andTrailing: 8675309., + positiveSign: +1, + trailingComma: 'in objects', andIn: ['arrays',], + "backwardsCompatible": "with JSON", + }"#, + ) + .unwrap(); + + assert_eq!(result.unquoted, "and you can quote me on that"); + assert_eq!(result.single_quotes, r#"I can use "double quotes" here"#); + assert_eq!(result.line_breaks, "Look, Mom! No \\n's!"); + assert_eq!(result.hexadecimal, 0xdecaf); + assert!((result.leading_decimal_point - 0.8675309).abs() < 1e-10); + assert_eq!(result.and_trailing, 8675309.0); + assert_eq!(result.positive_sign, 1); + assert_eq!(result.trailing_comma, "in objects"); + assert_eq!(result.and_in, vec!["arrays"]); + assert_eq!(result.backwards_compatible, "with JSON"); + } + + // ===================================================================== + // Roundtrip Tests - Serialization then Parsing + // ===================================================================== + + #[test] + fn test_roundtrip_all_escapes() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // String with all escapable characters + let original = Data { + value: "\"\\\u{0008}\u{000C}\n\r\t\u{000B}\u{2028}\u{2029}".to_string(), + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + #[test] + fn test_roundtrip_special_numbers() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + infinity: f64, + neg_infinity: f64, + nan: f64, + large: f64, + small: f64, + } + + let original = Data { + infinity: f64::INFINITY, + neg_infinity: f64::NEG_INFINITY, + nan: f64::NAN, + large: 1e47, + small: 1e-47, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert!( + deserialized.infinity.is_infinite() && deserialized.infinity.is_sign_positive() + ); + assert!( + deserialized.neg_infinity.is_infinite() + && deserialized.neg_infinity.is_sign_negative() + ); + assert!(deserialized.nan.is_nan()); + assert_eq!(deserialized.large, 1e47); + assert_eq!(deserialized.small, 1e-47); + } + + #[test] + fn test_roundtrip_keys_with_all_escapes() { + // Test that keys are properly escaped and can be parsed back + let mut map = std::collections::HashMap::new(); + map.insert("key\"quote".to_string(), 1); + map.insert("key\\backslash".to_string(), 2); + map.insert("key\nline".to_string(), 3); + map.insert("key\ttab".to_string(), 4); + map.insert("key\u{0008}backspace".to_string(), 5); + map.insert("key\u{000C}formfeed".to_string(), 6); + map.insert("key\u{000B}vtab".to_string(), 7); + map.insert("key\u{2028}lsep".to_string(), 8); + map.insert("key\u{2029}psep".to_string(), 9); + + let serialized = to_vec_pretty_sorted(&map).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: std::collections::HashMap = from_str(&json_str).unwrap(); + + assert_eq!(deserialized.get("key\"quote"), Some(&1)); + assert_eq!(deserialized.get("key\\backslash"), Some(&2)); + assert_eq!(deserialized.get("key\nline"), Some(&3)); + assert_eq!(deserialized.get("key\ttab"), Some(&4)); + assert_eq!(deserialized.get("key\u{0008}backspace"), Some(&5)); + assert_eq!(deserialized.get("key\u{000C}formfeed"), Some(&6)); + assert_eq!(deserialized.get("key\u{000B}vtab"), Some(&7)); + assert_eq!(deserialized.get("key\u{2028}lsep"), Some(&8)); + assert_eq!(deserialized.get("key\u{2029}psep"), Some(&9)); + } + + #[test] + fn test_generator_produces_valid_json5() { + // Section 11: "A JSON5 generator produces JSON5 text. The resulting + // text must strictly conform to the JSON5 grammar." + #[derive(Debug, Serialize)] + struct Complex { + string: String, + number: f64, + boolean: bool, + null_val: Option, + array: Vec, + nested: Nested, + } + + #[derive(Debug, Serialize)] + struct Nested { + value: f64, + } + + let data = Complex { + string: "hello\nworld\t\"test\"\u{2028}\u{2029}".to_string(), + number: 1e47, + boolean: true, + null_val: None, + array: vec![1, 2, 3], + nested: Nested { + value: f64::INFINITY, + }, + }; + + let serialized = to_vec_pretty_sorted(&data).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // If it parses, it's valid JSON5 + let _: serde_json::Value = from_str(&json_str).unwrap(); + } + } + + // ========================================================================= + // DISGUSTING EDGE CASE TESTS + // ========================================================================= + // + // These tests push the JSON5 spec to its absolute limits. They are designed + // to break parsers and serializers that don't strictly follow the spec. + // Based on https://spec.json5.org/ + + mod json5_torture_tests { + use super::*; + + // ===================================================================== + // String Escape Torture + // ===================================================================== + + #[test] + fn test_every_escape_sequence_in_one_string() { + // Every escape from Table 1 plus hex and unicode escapes + let json5 = r#"{ + value: "\"\\\b\f\n\r\t\v\0\x41\u0042" + }"#; + + #[derive(Debug, Deserialize)] + struct Data { + value: String, + } + + let result: Data = from_str(json5).unwrap(); + // " \ backspace formfeed newline carriage-return tab vertical-tab null A B + assert_eq!(result.value, "\"\\\u{0008}\u{000C}\n\r\t\u{000B}\u{0000}AB"); + } + + #[test] + fn test_string_with_all_control_characters() { + // Roundtrip all ASCII control characters (U+0000 to U+001F) + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: String, + } + + let mut control_chars = String::new(); + for i in 0u8..32 { + control_chars.push(i as char); + } + // Add DEL (U+007F) too + control_chars.push('\u{007F}'); + + let original = Data { + value: control_chars, + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + #[test] + fn test_surrogate_pair_boundaries() { + // Test the exact boundaries of surrogate pairs + // High surrogates: U+D800 to U+DBFF + // Low surrogates: U+DC00 to U+DFFF + // First valid surrogate pair encodes U+10000 + #[derive(Debug, Deserialize)] + struct Data { + value: String, + } + + // 𐀀 (U+10000) = \uD800\uDC00 + let result: Data = from_str(r#"{ value: "\uD800\uDC00" }"#).unwrap(); + assert_eq!(result.value, "\u{10000}"); + + // 🀀 (U+1F000) = \uD83C\uDC00 + let result: Data = from_str(r#"{ value: "\uD83C\uDC00" }"#).unwrap(); + assert_eq!(result.value, "🀀"); + + // Last valid BMP character before surrogates: U+D7FF + let result: Data = from_str(r#"{ value: "\uD7FF" }"#).unwrap(); + assert_eq!(result.value, "\u{D7FF}"); + + // First character after surrogates: U+E000 + let result: Data = from_str(r#"{ value: "\uE000" }"#).unwrap(); + assert_eq!(result.value, "\u{E000}"); + } + + #[test] + fn test_maximum_unicode_code_point() { + // U+10FFFF is the maximum valid Unicode code point + // Encoded as surrogate pair: \uDBFF\uDFFF + #[derive(Debug, Deserialize)] + struct Data { + value: String, + } + + let result: Data = from_str(r#"{ value: "\uDBFF\uDFFF" }"#).unwrap(); + assert_eq!(result.value, "\u{10FFFF}"); + } + + #[test] + fn test_string_line_continuation_all_terminators() { + // Per Table 2: line continuation can use LF, CR, CRLF, U+2028, U+2029 + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // LF continuation + let result: Data = from_str("{ value: 'hello\\\nworld' }").unwrap(); + assert_eq!(result.value, "helloworld"); + + // CR continuation + let result: Data = from_str("{ value: 'hello\\\rworld' }").unwrap(); + assert_eq!(result.value, "helloworld"); + + // CRLF continuation + let result: Data = from_str("{ value: 'hello\\\r\nworld' }").unwrap(); + assert_eq!(result.value, "helloworld"); + + // U+2028 line separator continuation + let result: Data = from_str("{ value: 'hello\\\u{2028}world' }").unwrap(); + assert_eq!(result.value, "helloworld"); + + // U+2029 paragraph separator continuation + let result: Data = from_str("{ value: 'hello\\\u{2029}world' }").unwrap(); + assert_eq!(result.value, "helloworld"); + } + + #[test] + fn test_identity_escapes() { + // Per spec Section 5.1: "If any other character follows a reverse solidus, + // except for the decimal digits 1 through 9, that character will be + // included in the string, but the reverse solidus will not." + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // \A\C\/\D\C should become AC/DC + let result: Data = from_str(r#"{ value: '\A\C\/\D\C' }"#).unwrap(); + assert_eq!(result.value, "AC/DC"); + + // Random escaped letters + let result: Data = from_str(r#"{ value: '\z\y\x41' }"#).unwrap(); + assert_eq!(result.value, "zyA"); // \x41 is hex escape for 'A' + } + + // ===================================================================== + // Number Torture + // ===================================================================== + + #[test] + fn test_all_special_number_forms() { + #[derive(Debug, Deserialize)] + struct Data { + a: f64, + b: f64, + c: f64, + d: f64, + e: f64, + f: f64, + g: f64, + h: f64, + i: f64, + j: f64, + } + + let result: Data = from_str( + r#"{ + a: Infinity, + b: -Infinity, + c: +Infinity, + d: NaN, + e: +NaN, + f: -NaN, + g: .5, + h: 5., + i: +42, + j: 0xDEADBEEF + }"#, + ) + .unwrap(); + + assert!(result.a.is_infinite() && result.a.is_sign_positive()); + assert!(result.b.is_infinite() && result.b.is_sign_negative()); + assert!(result.c.is_infinite() && result.c.is_sign_positive()); + assert!(result.d.is_nan()); + assert!(result.e.is_nan()); + assert!(result.f.is_nan()); + assert_eq!(result.g, 0.5); + assert_eq!(result.h, 5.0); + assert_eq!(result.i, 42.0); + assert_eq!(result.j, 0xDEADBEEF_u64 as f64); + } + + #[test] + fn test_hexadecimal_edge_cases() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i64, + } + + // Lowercase x + let result: Data = from_str(r#"{ value: 0x0 }"#).unwrap(); + assert_eq!(result.value, 0); + + // Uppercase X + let result: Data = from_str(r#"{ value: 0X0 }"#).unwrap(); + assert_eq!(result.value, 0); + + // All hex digits lowercase + let result: Data = from_str(r#"{ value: 0xabcdef }"#).unwrap(); + assert_eq!(result.value, 0xABCDEF); + + // All hex digits uppercase + let result: Data = from_str(r#"{ value: 0xABCDEF }"#).unwrap(); + assert_eq!(result.value, 0xABCDEF); + + // Positive hex + let result: Data = from_str(r#"{ value: +0xFF }"#).unwrap(); + assert_eq!(result.value, 255); + + // Negative hex + let result: Data = from_str(r#"{ value: -0xFF }"#).unwrap(); + assert_eq!(result.value, -255); + } + + #[test] + fn test_exponent_edge_cases() { + #[derive(Debug, Deserialize)] + struct Data { + value: f64, + } + + // Uppercase E + let result: Data = from_str(r#"{ value: 1E10 }"#).unwrap(); + assert_eq!(result.value, 1e10); + + // Lowercase e + let result: Data = from_str(r#"{ value: 1e10 }"#).unwrap(); + assert_eq!(result.value, 1e10); + + // Explicit positive exponent + let result: Data = from_str(r#"{ value: 1e+10 }"#).unwrap(); + assert_eq!(result.value, 1e10); + + // Negative exponent + let result: Data = from_str(r#"{ value: 1e-10 }"#).unwrap(); + assert_eq!(result.value, 1e-10); + + // Zero exponent + let result: Data = from_str(r#"{ value: 1e0 }"#).unwrap(); + assert_eq!(result.value, 1.0); + + // Leading decimal with exponent + let result: Data = from_str(r#"{ value: .5e10 }"#).unwrap(); + assert_eq!(result.value, 0.5e10); + + // Trailing decimal with exponent + let result: Data = from_str(r#"{ value: 5.e10 }"#).unwrap(); + assert_eq!(result.value, 5e10); + } + + // ===================================================================== + // Identifier/Key Torture + // ===================================================================== + + #[test] + fn test_identifier_edge_cases() { + // ECMAScript 5.1 IdentifierName rules + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + #[serde(rename = "$")] + dollar: i32, + #[serde(rename = "_")] + underscore: i32, + #[serde(rename = "$_")] + dollar_underscore: i32, + #[serde(rename = "_$")] + underscore_dollar: i32, + #[serde(rename = "$$$$")] + many_dollars: i32, + #[serde(rename = "____")] + many_underscores: i32, + #[serde(rename = "a1234567890")] + letter_digits: i32, + } + + let result: Data = from_str( + r#"{ + $: 1, + _: 2, + $_: 3, + _$: 4, + $$$$: 5, + ____: 6, + a1234567890: 7 + }"#, + ) + .unwrap(); + + assert_eq!(result.dollar, 1); + assert_eq!(result.underscore, 2); + assert_eq!(result.dollar_underscore, 3); + assert_eq!(result.underscore_dollar, 4); + assert_eq!(result.many_dollars, 5); + assert_eq!(result.many_underscores, 6); + assert_eq!(result.letter_digits, 7); + } + + #[test] + fn test_reserved_words_as_keys() { + // All ES5 reserved words should work as unquoted keys + #[derive(Debug, Deserialize)] + #[allow(dead_code)] + struct Reserved { + r#break: i32, + r#case: i32, + r#catch: i32, + r#continue: i32, + r#debugger: i32, + r#default: i32, + r#delete: i32, + r#do: i32, + r#else: i32, + r#finally: i32, + r#for: i32, + r#function: i32, + r#if: i32, + r#in: i32, + r#instanceof: i32, + r#new: i32, + r#return: i32, + r#switch: i32, + this: i32, + r#throw: i32, + r#try: i32, + r#typeof: i32, + var: i32, + void: i32, + r#while: i32, + with: i32, + class: i32, + r#const: i32, + r#enum: i32, + export: i32, + extends: i32, + import: i32, + #[serde(rename = "super")] + super_: i32, + null: i32, + r#true: i32, + r#false: i32, + } + + let json5 = r#"{ + break: 1, case: 2, catch: 3, continue: 4, debugger: 5, + default: 6, delete: 7, do: 8, else: 9, finally: 10, + for: 11, function: 12, if: 13, in: 14, instanceof: 15, + new: 16, return: 17, switch: 18, this: 19, throw: 20, + try: 21, typeof: 22, var: 23, void: 24, while: 25, + with: 26, class: 27, const: 28, enum: 29, export: 30, + extends: 31, import: 32, super: 33, null: 34, true: 35, false: 36 + }"#; + + let _: Reserved = from_str(json5).unwrap(); + } + + #[test] + fn test_keys_that_look_like_numbers() { + // Keys that start with digits must be quoted + let mut map = std::collections::HashMap::new(); + map.insert("123".to_string(), 1); + map.insert("0xFF".to_string(), 2); + map.insert("3.14".to_string(), 3); + map.insert("1e10".to_string(), 4); + + let serialized = to_vec_pretty_sorted(&map).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // All should be quoted since they start with digits + assert!(json_str.contains("\"123\"")); + assert!(json_str.contains("\"0xFF\"")); + assert!(json_str.contains("\"3.14\"")); + assert!(json_str.contains("\"1e10\"")); + + // Roundtrip + let deserialized: std::collections::HashMap = from_str(&json_str).unwrap(); + assert_eq!(deserialized.get("123"), Some(&1)); + } + + // ===================================================================== + // Whitespace Torture + // ===================================================================== + + #[test] + fn test_all_whitespace_characters() { + // Per Table 3 in spec - all valid whitespace + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + // Build a string with all whitespace types + let whitespace = + "\u{0009}\u{000A}\u{000B}\u{000C}\u{000D}\u{0020}\u{00A0}\u{2028}\u{2029}\u{FEFF}"; + let json5 = format!( + "{}{{{}value{}:{}42{}}}", + whitespace, whitespace, whitespace, whitespace, whitespace + ); + + let result: Data = from_str(&json5).unwrap(); + assert_eq!(result.value, 42); + } + + #[test] + fn test_whitespace_between_every_token() { + #[derive(Debug, Deserialize)] + struct Data { + arr: Vec, + } + + // Whitespace can appear before and after any token + let json5 = " { arr : [ 1 , 2 , 3 , ] , } "; + let result: Data = from_str(json5).unwrap(); + assert_eq!(result.arr, vec![1, 2, 3]); + } + + // ===================================================================== + // Comment Torture + // ===================================================================== + + #[test] + fn test_comments_everywhere() { + #[derive(Debug, Deserialize)] + struct Data { + a: i32, + b: i32, + } + + let json5 = r#" + // comment before object + { // comment after opening brace + // comment before key + a // comment after key + : // comment after colon + 1 // comment after value + , // comment after comma + /* multi + line + comment */ + b: /* inline */ 2 /* another */ + , // trailing comma comment + } // comment after closing brace + // comment at end + "#; + + let result: Data = from_str(json5).unwrap(); + assert_eq!(result.a, 1); + assert_eq!(result.b, 2); + } + + #[test] + fn test_comment_with_special_content() { + #[derive(Debug, Deserialize, PartialEq)] + struct Data { + value: i32, + } + + // Comments can contain anything except */ for multi-line + let json5 = r#"{ + // Comment with "quotes" and 'apostrophes' and \escapes + /* Multi-line with + { "fake": "json" } + and // nested single-line looking things + */ + value: 42 + }"#; + + let result: Data = from_str(json5).unwrap(); + assert_eq!(result.value, 42); + } + + // ===================================================================== + // Nesting Torture + // ===================================================================== + + #[test] + fn test_deeply_nested_objects() { + // 20 levels of nesting + let mut json5 = String::new(); + for _ in 0..20 { + json5.push_str("{ a: "); + } + json5.push_str("42"); + for _ in 0..20 { + json5.push_str(" }"); + } + + let result: serde_json::Value = from_str(&json5).unwrap(); + + // Navigate to the innermost value + let mut current = &result; + for _ in 0..20 { + current = ¤t["a"]; + } + assert_eq!(current.as_i64().unwrap(), 42); + } + + #[test] + fn test_deeply_nested_arrays() { + // 20 levels of array nesting + let mut json5 = String::new(); + for _ in 0..20 { + json5.push('['); + } + json5.push_str("42"); + for _ in 0..20 { + json5.push(']'); + } + + let result: serde_json::Value = from_str(&json5).unwrap(); + + // Navigate to the innermost value + let mut current = &result; + for _ in 0..20 { + current = ¤t[0]; + } + assert_eq!(current.as_i64().unwrap(), 42); + } + + // ===================================================================== + // Trailing Comma Torture + // ===================================================================== + + #[test] + fn test_trailing_commas_everywhere() { + #[derive(Debug, Deserialize)] + struct Inner { + x: i32, + } + + #[derive(Debug, Deserialize)] + struct Data { + obj: Inner, + arr: Vec, + } + + let json5 = r#"{ + obj: { + x: 1, + }, + arr: [ + 1, + 2, + 3, + ], + }"#; + + let result: Data = from_str(json5).unwrap(); + assert_eq!(result.obj.x, 1); + assert_eq!(result.arr, vec![1, 2, 3]); + } + + // ===================================================================== + // Mixed Quote Style Torture + // ===================================================================== + + #[test] + fn test_mixed_quote_styles() { + #[derive(Debug, Deserialize)] + struct Data { + #[serde(rename = "double-key")] + double_key: String, + #[serde(rename = "single-key")] + single_key: String, + } + + let json5 = r#"{ + "double-key": 'single value', + 'single-key': "double value" + }"#; + + let result: Data = from_str(json5).unwrap(); + assert_eq!(result.double_key, "single value"); + assert_eq!(result.single_key, "double value"); + } + + // ===================================================================== + // Empty/Minimal Structure Torture + // ===================================================================== + + #[test] + fn test_minimal_valid_json5() { + // Smallest valid JSON5 values + let _: serde_json::Value = from_str("{}").unwrap(); + let _: serde_json::Value = from_str("[]").unwrap(); + let _: serde_json::Value = from_str("null").unwrap(); + let _: serde_json::Value = from_str("true").unwrap(); + let _: serde_json::Value = from_str("false").unwrap(); + let _: serde_json::Value = from_str("0").unwrap(); + let _: serde_json::Value = from_str("\"\"").unwrap(); + let _: serde_json::Value = from_str("''").unwrap(); + } + + #[test] + fn test_object_with_empty_string_key() { + let mut map = std::collections::HashMap::new(); + map.insert("".to_string(), 42); + + let serialized = to_vec_pretty_sorted(&map).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + // Empty key must be quoted + assert!(json_str.contains("\"\"")); + + let deserialized: std::collections::HashMap = from_str(&json_str).unwrap(); + assert_eq!(deserialized.get(""), Some(&42)); + } + + // ===================================================================== + // Unicode Torture + // ===================================================================== + + #[test] + fn test_unicode_identifiers() { + // ECMAScript 5.1 allows Unicode letters in identifiers + // Note: this depends on the json5 crate's support + #[derive(Debug, Deserialize)] + struct Data { + #[serde(rename = "café")] + cafe: i32, + #[serde(rename = "naïve")] + naive: i32, + } + + // These may or may not work depending on json5 crate implementation + // Using quoted keys to be safe + let json5 = r#"{ "café": 1, "naïve": 2 }"#; + let result: Data = from_str(json5).unwrap(); + assert_eq!(result.cafe, 1); + assert_eq!(result.naive, 2); + } + + #[test] + fn test_zero_width_characters_in_strings() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // Zero-width joiner, zero-width non-joiner, zero-width space + let original = Data { + value: "a\u{200D}b\u{200C}c\u{200B}d".to_string(), + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + #[test] + fn test_combining_characters() { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // é as e + combining acute accent + let original = Data { + value: "e\u{0301}".to_string(), + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + #[test] + fn test_bidi_characters() { + // Bidirectional text control characters + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Data { + value: String, + } + + // LRM, RLM, LRE, RLE, PDF, LRO, RLO + let original = Data { + value: "\u{200E}\u{200F}\u{202A}\u{202B}\u{202C}\u{202D}\u{202E}".to_string(), + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + assert_eq!(deserialized.value, original.value); + } + + // ===================================================================== + // Stress Test - Everything Combined + // ===================================================================== + + #[test] + fn test_ultimate_torture() { + // Combine as many edge cases as possible in one test + // We test parsing succeeds and verify key parts + let json5 = r#" + // Ultimate torture test + { + /* Multi-line + comment */ + $weird_key$: 'single \' quote', + "key with spaces": "double \" quote", + 'key-with-dashes': [ + 1, + .5, + 5., + +42, + -0, + 0xFF, + 1e+10, + Infinity, + -Infinity, + NaN, + null, + true, + false, + "", + '', + "\n\r\t\b\f\v\0", + "\u0000\u001F\u007F", + "\uD83D\uDE00", // 😀 + ], + nested: { + deeply: { + nested: { + value: 42, + }, + }, + }, + empty_obj: {}, + empty_arr: [], + trailing: 'comma', + } + "#; + + // Test that it parses at all - this validates the json5 crate handles all features + let result: serde_json::Value = from_str(json5).unwrap(); + + // Verify key structural parts (avoiding NaN/Infinity which serde_json::Value can't hold) + assert_eq!(result["$weird_key$"], "single ' quote"); + assert_eq!(result["key with spaces"], "double \" quote"); + + // Check array length + assert_eq!(result["key-with-dashes"].as_array().unwrap().len(), 18); + + // Check some numeric values that serde_json can hold + assert_eq!(result["key-with-dashes"][0], 1.0); + assert_eq!(result["key-with-dashes"][1], 0.5); + assert_eq!(result["key-with-dashes"][5], 255.0); // 0xFF + + // Check nested + assert_eq!(result["nested"]["deeply"]["nested"]["value"], 42); + + // Check empty collections + assert!(result["empty_obj"].as_object().unwrap().is_empty()); + assert!(result["empty_arr"].as_array().unwrap().is_empty()); + + assert_eq!(result["trailing"], "comma"); + + // Now test specific special floats with typed deserialization + #[derive(Debug, Deserialize)] + struct SpecialFloats { + infinity: f64, + neg_infinity: f64, + nan: f64, + } + + let special: SpecialFloats = from_str( + r#"{ + infinity: Infinity, + neg_infinity: -Infinity, + nan: NaN + }"#, + ) + .unwrap(); + + assert!(special.infinity.is_infinite() && special.infinity.is_sign_positive()); + assert!(special.neg_infinity.is_infinite() && special.neg_infinity.is_sign_negative()); + assert!(special.nan.is_nan()); + } + + // ===================================================================== + // Roundtrip Stress Tests + // ===================================================================== + + #[test] + fn test_roundtrip_pathological_keys() { + let mut map = std::collections::HashMap::new(); + + // Keys that push every edge case + map.insert("".to_string(), 1); // empty + map.insert(" ".to_string(), 2); // space + map.insert(" ".to_string(), 3); // multiple spaces + map.insert("\t".to_string(), 4); // tab + map.insert("\n".to_string(), 5); // newline + map.insert("\"".to_string(), 6); // quote + map.insert("\\".to_string(), 7); // backslash + map.insert("'".to_string(), 8); // apostrophe + map.insert("\u{0000}".to_string(), 9); // null + map.insert("\u{001F}".to_string(), 10); // unit separator + map.insert("\u{007F}".to_string(), 11); // delete + map.insert("\u{2028}".to_string(), 12); // line separator + map.insert("\u{2029}".to_string(), 13); // paragraph separator + map.insert("key\"with\"quotes".to_string(), 14); + map.insert("key\\with\\backslashes".to_string(), 15); + map.insert("key\nwith\nnewlines".to_string(), 16); + map.insert("αβγδ".to_string(), 17); // Greek + map.insert("日本語".to_string(), 18); // Japanese + map.insert("🎉🎊🎁".to_string(), 19); // Emoji + + let serialized = to_vec_pretty_sorted(&map).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: std::collections::HashMap = from_str(&json_str).unwrap(); + + for (key, expected_value) in &map { + assert_eq!( + deserialized.get(key), + Some(expected_value), + "Failed for key: {:?}", + key + ); + } + } + + #[test] + fn test_roundtrip_pathological_values() { + #[derive(Debug, Serialize, Deserialize)] + struct Data { + a: f64, + b: f64, + c: f64, + d: f64, + e: f64, + f: f64, + g: f64, + h: f64, + i: String, + } + + let original = Data { + a: f64::INFINITY, + b: f64::NEG_INFINITY, + c: f64::NAN, + d: f64::MAX, + e: f64::MIN, + f: f64::MIN_POSITIVE, + g: f64::EPSILON, + h: 0.0, + i: (0u8..=127).map(|b| b as char).collect::(), // All ASCII + }; + + let serialized = to_vec_pretty_sorted(&original).unwrap(); + let json_str = String::from_utf8(serialized).unwrap(); + + let deserialized: Data = from_str(&json_str).unwrap(); + + assert!(deserialized.a.is_infinite() && deserialized.a.is_sign_positive()); + assert!(deserialized.b.is_infinite() && deserialized.b.is_sign_negative()); + assert!(deserialized.c.is_nan()); + assert_eq!(deserialized.d, f64::MAX); + assert_eq!(deserialized.e, f64::MIN); + assert_eq!(deserialized.f, f64::MIN_POSITIVE); + assert_eq!(deserialized.g, f64::EPSILON); + assert_eq!(deserialized.h, 0.0); + assert_eq!(deserialized.i, original.i); + } + } } diff --git a/src/snapshot_middleware/json_model.rs b/src/snapshot_middleware/json_model.rs index 4a037157..cc312258 100644 --- a/src/snapshot_middleware/json_model.rs +++ b/src/snapshot_middleware/json_model.rs @@ -83,12 +83,32 @@ pub fn syncback_json_model<'sync>( model.schema = old_inst.metadata().schema.clone(); } + let serialized = match crate::json::to_vec_pretty_sorted(&model) { + Ok(bytes) => bytes, + Err(e) => { + // Provide more detail about what's in the model + let prop_details: Vec<_> = model + .properties + .iter() + .map(|(k, v)| format!("{}={:?}", k.as_str(), v)) + .collect(); + let attr_names: Vec<_> = model.attributes.keys().map(|k| k.as_str()).collect(); + log::error!( + "Failed to serialize JSON model for class '{}': properties=[{}], attributes={:?}, error: {}", + model.class_name, + prop_details.join(", "), + attr_names, + e + ); + return Err(e.context(format!( + "failed to serialize new JSON Model for class '{}'", + model.class_name + ))); + } + }; + Ok(SyncbackReturn { - fs_snapshot: FsSnapshot::new().with_added_file( - &snapshot.path, - crate::json::to_vec_pretty_sorted(&model) - .context("failed to serialize new JSON Model")?, - ), + fs_snapshot: FsSnapshot::new().with_added_file(&snapshot.path, serialized), children: Vec::new(), removed_children: Vec::new(), }) diff --git a/src/web/api.rs b/src/web/api.rs index d337ba5e..db8b8a56 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -1823,14 +1823,14 @@ mod tests { #[test] fn test_float32() { - let variant = Variant::Float32(3.14); + let variant = Variant::Float32(3.125); // Use a non-constant value let result = variant_to_json(&variant); assert!(result.is_some()); } #[test] fn test_float64() { - let variant = Variant::Float64(2.71828); + let variant = Variant::Float64(2.5); // Use a non-constant value let result = variant_to_json(&variant); assert!(result.is_some()); } @@ -1959,7 +1959,7 @@ mod tests { // Configuration creates a directory instead let json5_types = vec!["Part", "Frame", "TextLabel", "Model"]; for class_name in json5_types { - let file_name = format!("Test.model.json5"); + let file_name = "Test.model.json5"; assert!( file_name.ends_with(".model.json5"), "{} should create .model.json5 file", @@ -2062,10 +2062,8 @@ mod tests { .map(|e| &e.items); if let Some(enums) = run_context_enums { - if let Some(&client_value) = enums.get("Client") { - // Verify the enum value exists - assert!(client_value > 0 || client_value == 0); - } + // Verify the enum value exists - just getting it is the test + assert!(enums.get("Client").is_some()); } } @@ -2078,9 +2076,7 @@ mod tests { .map(|e| &e.items); if let Some(enums) = run_context_enums { - if let Some(&server_value) = enums.get("Server") { - assert!(server_value > 0 || server_value == 0); - } + assert!(enums.get("Server").is_some()); } } @@ -2093,9 +2089,7 @@ mod tests { .map(|e| &e.items); if let Some(enums) = run_context_enums { - if let Some(&legacy_value) = enums.get("Legacy") { - assert!(legacy_value > 0 || legacy_value == 0); - } + assert!(enums.get("Legacy").is_some()); } } @@ -2108,9 +2102,7 @@ mod tests { .map(|e| &e.items); if let Some(enums) = run_context_enums { - if let Some(&plugin_value) = enums.get("Plugin") { - assert!(plugin_value > 0 || plugin_value == 0); - } + assert!(enums.get("Plugin").is_some()); } } @@ -3675,7 +3667,7 @@ mod tests { // When syncing back children, duplicates should be skipped // This test verifies the filter_duplicate_children logic - let children_names = vec!["ChildA", "ChildB", "ChildA", "ChildC"]; + let children_names = ["ChildA", "ChildB", "ChildA", "ChildC"]; let unique: Vec<_> = children_names .iter() .filter(|&&name| children_names.iter().filter(|&&n| n == name).count() == 1) @@ -3697,8 +3689,8 @@ mod tests { // └─ Child2/ // └─ GrandChild (unique within its parent) - let child1_grandchildren = vec!["GrandChild", "GrandChild"]; - let child2_grandchildren = vec!["GrandChild"]; + let child1_grandchildren = ["GrandChild", "GrandChild"]; + let child2_grandchildren = ["GrandChild"]; // Child1's grandchildren have duplicates let child1_filtered: Vec<_> = child1_grandchildren @@ -3741,7 +3733,7 @@ mod tests { // This simulates the is_tree_path_unique check // Scenario: Trying to add a script under a folder that has duplicate siblings - let parent_path_levels = vec![ + let parent_path_levels = [ ("Root", vec!["FolderA", "FolderA"]), // Duplicate at this level! ]; @@ -3758,7 +3750,7 @@ mod tests { #[test] fn test_valid_parent_path() { // Valid parent path with no duplicates at any level - let parent_path_levels = vec![ + let parent_path_levels = [ ( "ReplicatedStorage", vec!["Workspace", "ReplicatedStorage", "ServerStorage"], diff --git a/tests/rojo_test/internable.rs b/tests/rojo_test/internable.rs index 5dc1be61..31f97fb9 100644 --- a/tests/rojo_test/internable.rs +++ b/tests/rojo_test/internable.rs @@ -58,7 +58,7 @@ impl<'a> Internable<&'a HashMap>> for Instance<'a> { impl Internable<()> for SocketPacket<'_> { fn intern(&self, redactions: &mut RedactionMap, extra: ()) { - redactions.intern(&self.session_id); + redactions.intern(self.session_id); match &self.body { SocketPacketBody::Messages(packet) => packet.intern(redactions, extra), }