diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4c2cbdf..7b0983f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -24,3 +24,5 @@ jobs: run: cargo test --verbose --no-default-features - name: Run tests default run: cargo test --verbose + - name: Run compile-fail tests + run: python3 compile_fail_tests/verify_soundness_failures.py diff --git a/benches/bench.rs b/benches/bench.rs index ed07577..8866d59 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -51,7 +51,7 @@ fn parse_bench() { runner.set_name(name); let access = get_access_for_input_name(name); - runner.register("serde_json", move |_data| { + runner.register("serde_json parse only", move |_data| { let mut val = None; for line in input_gen() { let json: serde_json::Value = serde_json::from_str(&line).unwrap(); @@ -60,7 +60,7 @@ fn parse_bench() { black_box(val); }); - runner.register("serde_json + access by key", move |_data| { + runner.register("serde_json access by key", move |_data| { let mut total_size = 0; for line in input_gen() { let json: serde_json::Value = serde_json::from_str(&line).unwrap(); @@ -68,7 +68,7 @@ fn parse_bench() { } black_box(total_size); }); - runner.register("serde_json_borrow::OwnedValue", move |_data| { + runner.register("serde_json_borrow::OwnedValue parse only", move |_data| { let mut val = None; for line in input_gen() { let json: OwnedValue = OwnedValue::parse_from(line).unwrap(); @@ -78,7 +78,7 @@ fn parse_bench() { }); runner.register( - "serde_json_borrow::OwnedValue + access by key", + "serde_json_borrow::OwnedValue access by key", move |_data| { let mut total_size = 0; for line in input_gen() { @@ -89,13 +89,22 @@ fn parse_bench() { }, ); - runner.register("SIMD_json_borrow", move |_data| { + runner.register("serde_json_borrow::ReusableMap parse only", move |_data| { + let mut map = serde_json_borrow::ReusableMap::new(); + for line in input_gen() { + let parsed = map.deserialize(&line).unwrap(); + black_box(parsed); + } + }); + + runner.register("SIMD_json_borrow parse only", move |_data| { for line in input_gen() { let mut data: Vec = line.into(); let v: simd_json::BorrowedValue = simd_json::to_borrowed_value(&mut data).unwrap(); black_box(v); } }); + runner.run(); } } diff --git a/compile_fail_tests/.gitignore b/compile_fail_tests/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/compile_fail_tests/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/compile_fail_tests/Cargo.toml b/compile_fail_tests/Cargo.toml new file mode 100644 index 0000000..8e2c15d --- /dev/null +++ b/compile_fail_tests/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "compile_fail_tests" +version = "0.0.0" +edition = "2024" + +[workspace] + +[dependencies] +serde_json_borrow = { version = "0.8.0", path = ".." } + +[[bin]] +name = "deserialize_borrow_conflict" +path = "deserialize_borrow_conflict.rs" + +[[bin]] +name = "guard_thread_safety" +path = "guard_thread_safety.rs" + +[[bin]] +name = "map_lifetime_soundness" +path = "map_lifetime_soundness.rs" + +[[bin]] +name = "guard_outlives_string_scope" +path = "guard_outlives_string_scope.rs" + +[[bin]] +name = "guard_return_from_function" +path = "guard_return_from_function.rs" + +[[bin]] +name = "leak_references_from_guard" +path = "leak_references_from_guard.rs" diff --git a/compile_fail_tests/README.md b/compile_fail_tests/README.md new file mode 100644 index 0000000..06c6b28 --- /dev/null +++ b/compile_fail_tests/README.md @@ -0,0 +1,65 @@ +# Compile-Fail Tests + +This directory contains tests that are designed to fail at compile time. These tests verify that our lifetime safety guarantees in the `deser` module are properly enforced by the Rust compiler. + +## Purpose + +These tests ensure that: + +1. A guard cannot outlive the JSON string it references (`guard_outlives_string_scope`) +2. A guard cannot be returned from a function where the JSON string is local (`guard_return_from_function`) +3. References cannot be leaked from a guard to outlive their source (`leak_references_from_guard`) +4. A deserializer cannot be used while a guard exists (`safety_tests/deserializer_borrow_conflict`) +5. A guard's internal map cannot be borrowed mutably multiple times (`safety_tests/guard_multiple_borrows`) +6. Guards cannot be sent between threads if they contain references to thread-local data (`safety_tests/guard_thread_safety`) +7. The lifetime of the map is properly tied to the JSON string's lifetime (`safety_tests/map_lifetime_soundness`) + +## How to Run + +These tests are meant to fail at compile time, so you can verify them by trying to build each test individually: + +```bash +# Test that a guard cannot outlive its string reference +cd guard_outlives_string_scope +cargo build +# Should fail with: error[E0597]: `json_string` does not live long enough + +# Test that a guard cannot be returned from a function with local string +cd ../guard_return_from_function +cargo build +# Should fail with lifetime errors + +# Test that references cannot be leaked from a guard +cd ../leak_references_from_guard +cargo build +# Should fail with lifetime errors + +# Additional safety tests +cd ../safety_tests +cargo check --bin deserializer_borrow_conflict # Should fail with "cannot borrow as mutable" +cargo check --bin guard_multiple_borrows # Should fail with "cannot borrow as mutable more than once" +cargo check --bin guard_thread_safety # Should fail with "cannot be sent between threads safely" +cargo check --bin map_lifetime_soundness # Should fail with "does not live long enough" +``` + +## Expected Errors + +Each test demonstrates a different aspect of lifetime safety: + +- `guard_outlives_string_scope`: Should fail with `error[E0597]: 'json_string' does not live long enough` +- `guard_return_from_function`: Should fail with errors about lifetimes not matching +- `leak_references_from_guard`: Should fail with `error[E0597]: 'json_string' does not live long enough` +- `deserializer_borrow_conflict`: Should fail with "cannot borrow as mutable" error +- `guard_multiple_borrows`: Should fail with "cannot borrow as mutable more than once" error +- `guard_thread_safety`: Should fail with "cannot be sent between threads safely" error +- `map_lifetime_soundness`: Should fail with "does not live long enough" error + +## Why This Matters + +These tests are crucial for verifying that our guard pattern correctly prevents use-after-free bugs by enforcing compile-time lifetime guarantees. If any of these tests were to compile successfully, it would indicate a flaw in our safety guarantees. + +The safety of our deserialization approach depends on these lifetime constraints being properly enforced by the compiler. These tests give us confidence that the guard pattern is working as intended to prevent memory safety issues. + +## Additional Runtime Tests + +In addition to these compile-fail tests, there is a runtime test `test_map_cleared_on_drop` in the main test suite that verifies that the map is properly cleared when the guard is dropped, ensuring no dangling references remain. \ No newline at end of file diff --git a/compile_fail_tests/deserialize_borrow_conflict.rs b/compile_fail_tests/deserialize_borrow_conflict.rs new file mode 100644 index 0000000..a958cae --- /dev/null +++ b/compile_fail_tests/deserialize_borrow_conflict.rs @@ -0,0 +1,24 @@ +//! This test demonstrates that the deserializer cannot be used while a guard exists. +//! This file should fail to compile because the deserializer is already borrowed by the first guard. +//! EXPECT: error[E0499]: cannot borrow `deserializer` as mutable more than once at a time + +use serde_json_borrow::ReusableMap; + +fn main() { + let mut deserializer = ReusableMap::new(); + + // First JSON string + let json_str1 = r#"{"first":"value"}"#; + + // Get a guard from the deserializer + let guard1 = deserializer.deserialize(json_str1).unwrap(); + + // Try to use the deserializer again while guard1 exists + // This should fail to compile because the deserializer is already mutably borrowed + let json_str2 = r#"{"second":"value"}"#; + let guard2 = deserializer.deserialize(json_str2).unwrap(); // Should fail with borrow error + + // Use both guards to ensure the compiler doesn't optimize away + println!("First guard length: {}", guard1.len()); + println!("Second guard length: {}", guard2.len()); +} diff --git a/compile_fail_tests/guard_outlives_string_scope.rs b/compile_fail_tests/guard_outlives_string_scope.rs new file mode 100644 index 0000000..10f130c --- /dev/null +++ b/compile_fail_tests/guard_outlives_string_scope.rs @@ -0,0 +1,24 @@ +//! This test demonstrates that a guard cannot outlive the JSON string it references. +//! This file should fail to compile with an error like: +//! EXPECT: error[E0597]: `json_string` does not live long enough + +use serde_json_borrow::ReusableMap; + +fn main() { + let mut deserializer = ReusableMap::new(); + let guard; + + { + // Create a JSON string with a limited scope + let json_string = r#"{"temporary":"value"}"#.to_string(); + + // This should fail to compile because the guard would outlive json_string + guard = deserializer.deserialize(&json_string).unwrap(); + + // The guard borrows from json_string, but json_string will be dropped + // at the end of this block, while guard would live longer + } + + // This would be a use-after-free if the compiler allowed it + assert_eq!(guard.len(), 1); +} diff --git a/compile_fail_tests/guard_return_from_function.rs b/compile_fail_tests/guard_return_from_function.rs new file mode 100644 index 0000000..d1650e8 --- /dev/null +++ b/compile_fail_tests/guard_return_from_function.rs @@ -0,0 +1,26 @@ +//! This test demonstrates that a guard cannot be returned from a function +//! where the JSON string is local to that function. +//! This file should fail to compile with an error about lifetimes not matching. +//! EXPECT: error[E0515]: cannot return value referencing local variable `json_string` + +use serde_json_borrow::{BorrowedMap, ReusableMap}; + +// This function tries to return a guard that references a local JSON string +fn create_guard<'d>(deserializer: &'d mut ReusableMap) -> BorrowedMap<'static, 'd> { + // Local JSON string that will be dropped when the function returns + let json_string = r#"{"escape":"attempt"}"#.to_string(); + + // This should fail to compile - cannot convert BorrowedMap<'_, 'd> to BorrowedMap<'static, 'd> + // because that would allow the guard to outlive the json_string + deserializer.deserialize(&json_string).unwrap() +} + +fn main() { + let mut deserializer = ReusableMap::new(); + + // Try to get a guard with an invalid 'static lifetime for the JSON string + let guard = create_guard(&mut deserializer); + + // This would be a use-after-free if the compiler allowed it + assert_eq!(guard.len(), 1); +} diff --git a/compile_fail_tests/guard_thread_safety.rs b/compile_fail_tests/guard_thread_safety.rs new file mode 100644 index 0000000..6f3a31f --- /dev/null +++ b/compile_fail_tests/guard_thread_safety.rs @@ -0,0 +1,31 @@ +//! This test demonstrates that a BorrowedMap cannot be sent across thread boundaries +//! when it contains references to stack data. +//! +//! This file should fail to compile because we're trying to move a guard with +//! references to stack variables into a new thread. +//! EXPECT: error[E0597]: `json_string` does not live long enough +//! EXPECT: argument requires that `json_string` is borrowed for `'static` + +use std::thread; + +use serde_json_borrow::ReusableMap; + +fn main() { + // Create a JSON string on the stack + let json_string = r#"{"key":"value"}"#.to_string(); + + // Create a deserializer and guard + let mut deserializer = ReusableMap::new(); + let guard = deserializer.deserialize(&json_string).unwrap(); + + // Attempt to move the guard to a new thread + // This should fail to compile because the guard contains references + // to stack data (json_string and deserializer) that won't be valid + // in the new thread. + let handle = thread::spawn(move || { + // Try to use the guard in the new thread + println!("Guard in thread: {} elements", guard.len()); + }); + + handle.join().unwrap(); +} diff --git a/compile_fail_tests/leak_references_from_guard.rs b/compile_fail_tests/leak_references_from_guard.rs new file mode 100644 index 0000000..7a19d8c --- /dev/null +++ b/compile_fail_tests/leak_references_from_guard.rs @@ -0,0 +1,29 @@ +//! This test demonstrates that references cannot be leaked from a guard. +//! This file should fail to compile with an error about lifetimes not matching. +//! EXPECT: error[E0597]: `json_string` does not live long enough +//! EXPECT: error[E0597]: `guard` does not live long enough + +use serde_json_borrow::ReusableMap; +use serde_json_borrow::Value; + +fn main() { + let mut deserializer = ReusableMap::new(); + let leaked_ref: &str; + + { + let json_string = r#"{"name":"test"}"#.to_string(); + let guard = deserializer.deserialize(&json_string).unwrap(); + + // Try to extract and leak a reference from the guard + if let Some(Value::Str(name)) = guard.get("name") { + // This should fail to compile - cannot assign a reference with lifetime + // tied to json_string to a variable that outlives json_string + leaked_ref = name; + } else { + unreachable!(); + } + } + + // This would be a use-after-free if the compiler allowed it + println!("Leaked reference: {}", leaked_ref); +} diff --git a/compile_fail_tests/map_lifetime_soundness.rs b/compile_fail_tests/map_lifetime_soundness.rs new file mode 100644 index 0000000..2fede34 --- /dev/null +++ b/compile_fail_tests/map_lifetime_soundness.rs @@ -0,0 +1,31 @@ +//! This test demonstrates that the lifetime of the map is properly tied to the JSON string. +//! This file should fail to compile because we're trying to store a reference from the map +//! that would outlive the JSON string it references. +//! EXPECT: error[E0597]: `json_string` does not live long enough +//! EXPECT: error[E0597]: `guard` does not live long enough + +use serde_json_borrow::{ReusableMap, Value}; + +fn main() { + let mut deserializer = ReusableMap::new(); + let stored_ref: &str; + + { + // Create a JSON string with a limited scope + let json_string = r#"{"key":"value"}"#.to_string(); + let guard = deserializer.deserialize(&json_string).unwrap(); + + // Try to store a reference from the map that would outlive the JSON string + if let Some(Value::Str(val)) = guard.get("key") { + // This should fail to compile - cannot assign a reference with lifetime + // tied to json_string to a variable that outlives json_string + stored_ref = val; + } else { + unreachable!(); + } + } + + // Try to use the stored reference after the JSON string is dropped + // This would be a use-after-free if the compiler allowed it + println!("Stored ref: {}", stored_ref); +} diff --git a/compile_fail_tests/verify_soundness_failures.py b/compile_fail_tests/verify_soundness_failures.py new file mode 100755 index 0000000..108e9ca --- /dev/null +++ b/compile_fail_tests/verify_soundness_failures.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +Verify that all Rust files in the compile_fail_tests directory +fail to compile with the expected error messages. +""" + +import argparse +import os +import re +import subprocess + + +import sys + + +def main(): + """ + Find all rs, run cargo check on them, and verify that they fail with the expected error messages + """ + args = parse_args() + os.chdir(args.dir) + + rs_files = find_rs_files() + unexpected_success = [] + expectation_failures = {} + + if not rs_files: + print("No Rust files found in compile_fail_tests directory") + return 1 + + for file in rs_files: + bin_name = file[:-3] # strip .rs extension + print(f"Testing {bin_name}...") + + expectations = extract_expectations(file) + if not expectations: + print(f"ERROR: No EXPECT: lines found in {file}") + return 1 + + success, output = run_cargo_check(bin_name) + + show_output = args.show_output + if success: + print(f"FAIL: Compilation succeeded unexpectedly: {bin_name}") + unexpected_success.append(bin_name) + show_output = True + else: + # Check if all expected errors are in the output + unmet = verify_expectations(file, expectations, output) + + if unmet: + print(f"FAIL: Expected errors not found for {bin_name}:") + for exp in unmet: + print(f" - {exp}") + expectation_failures[bin_name] = unmet + show_output = True + + if show_output: + print(f"=== output for {bin_name}") + print(output) + print(f"=== end output for {bin_name}") + + # Final report + if not unexpected_success and not expectation_failures: + print("All tests failed to compile with expected errors!") + return 0 + else: + if unexpected_success: + print("\nFAIL: Successfully compiled binaries that should have failed:") + for target in unexpected_success: + print(f" {target}") + + if expectation_failures: + print("\nFAIL: Missing expected error messages:") + for bin_name, unmet in expectation_failures.items(): + print(f" {bin_name}:") + for exp in unmet: + print(f" - {exp}") + + return 1 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Verify that all Rust files in the compile_fail_tests directory fail to compile with the expected error messages." + ) + parser.add_argument( + "dir", + nargs="?", + help="Directory containing Rust binaries", + default=os.path.abspath(os.path.dirname(__file__)), + ) + parser.add_argument("--show-output", action="store_true", help="Show output of all compilations, whether or not they succeed", default=False) + return parser.parse_args() + + +def find_rs_files() -> list[str]: + """Find all Rust files in the given directory.""" + return [file for file in os.listdir() if file.endswith(".rs")] + + +def extract_expectations(file_path: str) -> list[str]: + """Extract the EXPECT: lines from a Rust file.""" + expectations = [] + with open(file_path, "r") as f: + for line in f: + match = re.search(r"EXPECT:\s*(.*)", line) + if match: + expectations.append(match.group(1).strip()) + return expectations + + +def run_cargo_check(bin_name: str) -> tuple[bool, str]: + """ + Run cargo check for a specific binary target. + Returns (compilation_succeeded, stderr) + """ + env = os.environ.copy() + # the simple pattern recognition fails with interpolated ANSI colors + env["CARGO_TERM_COLOR"] = "never" + try: + process = subprocess.run( + ["cargo", "check", "--bin", bin_name], + capture_output=True, + text=True, + check=False, + env=env, + ) + return process.returncode == 0, process.stderr + except subprocess.SubprocessError as e: + print(f"Error running cargo check: {e}") + return False, str(e) + + +def verify_expectations(file: str, expectations: list[str], output: str) -> list[str]: + """ + Verify that all expectations appear in the compiler output. + Returns a list of unmet expectations. + """ + unmet_expectations = [] + for expectation in expectations: + if expectation not in output: + unmet_expectations.append(expectation) + return unmet_expectations + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/lib.rs b/src/lib.rs index 089bcd6..55618e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,7 @@ mod index; mod num; mod object_vec; mod ownedvalue; +mod reusable; mod ser; mod value; @@ -90,4 +91,5 @@ mod cowstr; pub use object_vec::{KeyStrType, ObjectAsVec, ObjectAsVec as Map}; pub use ownedvalue::OwnedValue; +pub use reusable::{BorrowedMap, ReusableMap}; pub use value::Value; diff --git a/src/object_vec.rs b/src/object_vec.rs index 73a9683..9f9114d 100644 --- a/src/object_vec.rs +++ b/src/object_vec.rs @@ -200,6 +200,12 @@ impl<'ctx> ObjectAsVec<'ctx> { let idx = self.0.len() - 1; &mut self.0[idx].1 } + + /// Clear the underlying vec + #[inline] + pub fn clear(&mut self) { + self.0.clear(); + } } impl<'ctx> From> for serde_json::Map { diff --git a/src/reusable.rs b/src/reusable.rs new file mode 100644 index 0000000..b0ff51e --- /dev/null +++ b/src/reusable.rs @@ -0,0 +1,280 @@ +use std::fmt; +use std::marker::PhantomData; +use std::mem; +use std::ops::Deref; + +use serde::de::{DeserializeSeed, MapAccess, Visitor}; + +use crate::{Map, Value}; + +/// A JSON Deserializer that reuses the same map allocation for each deserialization. +/// +/// # Example +/// +/// ``` +/// use serde_json_borrow::ReusableMap; +/// +/// // Create a deserializer that will reuse the same map allocation +/// let mut reusable_map = ReusableMap::new(); +/// +/// let json_strs = [ +/// r#"{"name":"test","value":42}"#, +/// r#"{"name":"other","other":"value"}"#, +/// ]; +/// +/// for json_str in json_strs { +/// // Get a guard that provides access to the map for the lifetime of json_str +/// let mapped = reusable_map.deserialize(json_str).unwrap(); +/// assert!(mapped.get("name").is_some()); +/// // When the guard is dropped, the ReusableMap is cleared and released for reuse +/// } +/// ``` +/// +/// Note that you cannot use the ReusableMap while there is a guard active: +/// +/// ```rust,compile_fail +/// # use serde_json_borrow::ReusableMap; +/// let mut reusable_map = ReusableMap::new(); +/// +/// let json_str = r#"{"name":"test","value":42}"#; +/// +/// let mapped = reusable_map.deserialize(json_str).unwrap(); +/// let mapped2 = reusable_map.deserialize(json_str).unwrap(); // <-- fails +/// ``` +/// +/// Nor can the guard outlive the json string (or the ReusableMap): +/// +/// ```rust,compile_fail +/// # use serde_json_borrow::ReusableMap; +/// let mut reusable_map = ReusableMap::new(); +/// +/// let json_str = r#"{"name":"test","value":42}"#; +/// +/// let mapped = { +/// let string = json_str.to_string(); +/// let inner = reusable_map.deserialize(&string).unwrap(); +/// inner +/// }; +/// ``` +pub struct ReusableMap { + /// The reusable map that persists between deserializations + map: Map<'static>, +} + +impl ReusableMap { + /// Creates a new empty ReusableMap + pub fn new() -> Self { + Self { + map: Map::default(), + } + } + + /// Deserializes a JSON string and returns a guard that provides safe access to the map. + /// + /// Returns a [`BorrowedMap`] on success, or a deserialization error on + /// failure. The `BorrowedMap` provides safe access to the deserialized + /// map, and must be dropped before the deserializer can be used again. + /// + /// # Example + /// + /// ``` + /// # use std::borrow::Cow; + /// # use serde_json_borrow::{ReusableMap, Value}; + /// let mut map = ReusableMap::new(); + /// let json = r#"{"name": "Alice"}"#; + /// + /// let guard = map.deserialize(json).unwrap(); + /// assert_eq!(guard.get("name"), Some(&Value::Str(Cow::Borrowed("Alice")))); + /// ``` + pub fn deserialize<'json, 'deser>( + &'deser mut self, + json: &'json str, + ) -> Result, serde_json::Error> { + let mut deserializer = serde_json::Deserializer::from_str(json); + + // SAFETY: We're using transmute to convert the map's lifetime. + // This is safe because: + // 1. We're tying the resulting map's lifetime to the input JSON string ('json) and this deserializer + // 2. The BorrowedMap has a mutable reference to this JsonDeserializer, preventing deserialization while the guard exists + // 3. The guard's lifetime parameters ensure the map can't be accessed after the JSON string is invalid + // 4. The Guard clears the map on drop, ensuring no dangling references + let map = + unsafe { mem::transmute::<&mut Map<'static>, &'json mut Map<'json>>(&mut self.map) }; + + let seed = JsonMapSeed { map }; + seed.deserialize(&mut deserializer)?; + + Ok(BorrowedMap { + // SAFETY: We're using transmute to convert the map's lifetime. + // This has the same safety guarantees as the original transmute. + map: unsafe { + mem::transmute::<&mut Map<'static>, &'json mut Map<'json>>(&mut self.map) + }, + _deserializer: PhantomData, + }) + } +} + +impl Default for ReusableMap { + fn default() -> Self { + Self::new() + } +} + +/// A guard that provides safe access to a deserialized JSON map. +/// +/// It dereferences to [`Map`], see it for the methods available on the guard. +/// +/// It can only be created by the [`ReusableMap::deserialize`] method, +/// see that method for more information. +pub struct BorrowedMap<'json, 'deser> { + /// Reference to the map with lifetime tied to the JSON string + map: &'json mut Map<'json>, + /// Phantom data to tie the guard's lifetime to the ReusableMap + _deserializer: PhantomData<&'deser ()>, +} + +impl<'json> Deref for BorrowedMap<'json, '_> { + type Target = Map<'json>; + + fn deref(&self) -> &Self::Target { + self.map + } +} + +impl Drop for BorrowedMap<'_, '_> { + fn drop(&mut self) { + // We clear the map to prevent dangling references from previous calls + self.map.clear(); + } +} + +/// A struct that allows us to deserialize JSON into an existing map. +struct JsonMapSeed<'json> { + map: &'json mut Map<'json>, +} + +impl<'de, 'json> DeserializeSeed<'de> for JsonMapSeed<'json> +where + 'de: 'json, +{ + type Value = (); + + fn deserialize(self, deserializer: D) -> Result<(), D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(self) + } +} + +impl<'de, 'json> Visitor<'de> for JsonMapSeed<'json> +where + 'de: 'json, +{ + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a JSON object") + } + + fn visit_map(self, mut access: M) -> Result<(), M::Error> + where + M: MapAccess<'de>, + { + while let Some((key, value)) = access.next_entry::<&'de str, Value<'de>>()? { + self.map.insert(key, value); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use crate::num::N; + + use super::*; + + #[test] + fn test_map_cleared_on_drop() { + let mut deserializer = ReusableMap::new(); + + // First JSON - create and drop a guard + { + let json_str = r#"{"name":"test"}"#.to_string(); + let guard = deserializer.deserialize(&json_str).unwrap(); + assert_eq!(guard.len(), 1); + assert!(guard.contains_key("name")); + } + + // Second JSON - should start with a clean map + let json_str2 = r#"{"second":"value"}"#.to_string(); + let guard2 = deserializer.deserialize(&json_str2).unwrap(); + + // Verify the map was cleared by confirming it only has the new content + assert_eq!(guard2.len(), 1); + assert!(guard2.contains_key("second")); + assert!(!guard2.contains_key("name")); + } + + #[test] + fn test_json_guard_deserialization() { + let mut deserializer = ReusableMap::new(); + + let json_str = r#"{"name":"test","value":42,"nested":{"key":"val"}}"#.to_string(); + + let guard = deserializer.deserialize(&json_str).unwrap(); + + // Verify the contents were properly deserialized + assert_eq!(guard.len(), 3); + assert_eq!( + guard.get("name").unwrap(), + &Value::Str(Cow::Borrowed("test")) + ); + assert_eq!( + guard.get("value").unwrap(), + &Value::Number(crate::num::Number { n: N::PosInt(42) }) + ); + assert_eq!( + guard.get("nested").unwrap(), + &Value::Object(Map::from(vec![("key", Value::Str(Cow::Borrowed("val")))])) + ); + + // When guard is dropped, the deserializer is released + drop(guard); + + // Deserialize again with a new guard, reusing the same map allocation + let guard2 = deserializer.deserialize(r#"{"another":"value"}"#).unwrap(); + assert_eq!(guard2.len(), 1); + assert_eq!( + guard2.get("another").unwrap(), + &Value::Str(Cow::Borrowed("value")) + ); + } + + #[test] + fn test_invalid_json() { + let invalid_json = r#"{{"name":"test", invalid}}"#.to_string(); + let mut deserializer = ReusableMap::new(); + + let result = deserializer.deserialize(&invalid_json); + assert!( + result.is_err(), + "Deserialization of invalid JSON should fail" + ); + } + + #[test] + fn test_non_object_json() { + let array_json = r#"[1, 2, 3]"#; + let mut deserializer = ReusableMap::new(); + + let result = deserializer.deserialize(array_json); + assert!( + result.is_err(), + "Deserialization of JSON array should fail when expecting object" + ); + } +}