-
Notifications
You must be signed in to change notification settings - Fork 119
feat: commit to JSON array values individually #808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
d583da3
de50f00
b05961a
2d74e72
a19cb5e
66d309f
a7d1664
89fe3c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -66,6 +66,46 @@ impl TranscriptCommitConfig { | |||||||
| .any(|(_, kind)| matches!(kind, TranscriptCommitmentKind::Encoding)) | ||||||||
| } | ||||||||
|
|
||||||||
| /// Returns whether the builder has a commitment for the given direction and | ||||||||
| /// range. | ||||||||
| /// | ||||||||
| /// # Arguments | ||||||||
| /// | ||||||||
| /// * `direction` - The direction of the transcript. | ||||||||
| /// * `range` - The range of the commitment. | ||||||||
| pub fn contains( | ||||||||
| &self, | ||||||||
| ranges: &dyn ToRangeSet<usize>, | ||||||||
| direction: Direction, | ||||||||
| ) -> bool { | ||||||||
| let idx = Idx::new(ranges.to_range_set()); | ||||||||
|
|
||||||||
| self.commits | ||||||||
| .iter() | ||||||||
| .any(|((d, i), _)| *d == direction && *i == idx) | ||||||||
|
Comment on lines
+83
to
+85
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This being |
||||||||
| } | ||||||||
|
|
||||||||
| /// Returns whether the builder has a commitment for the given direction and | ||||||||
| /// range, with the given kind. | ||||||||
|
Comment on lines
+88
to
+89
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| /// | ||||||||
| /// # Arguments | ||||||||
| /// | ||||||||
| /// * `direction` - The direction of the transcript. | ||||||||
| /// * `ranges` - The range of the commitment. | ||||||||
| /// * `kind` - The kind of commitment. | ||||||||
| pub fn contains_with_kind( | ||||||||
| &self, | ||||||||
| ranges: &dyn ToRangeSet<usize>, | ||||||||
| direction: Direction, | ||||||||
| kind: TranscriptCommitmentKind, | ||||||||
| ) -> bool { | ||||||||
| let idx = Idx::new(ranges.to_range_set()); | ||||||||
|
|
||||||||
| self.commits | ||||||||
| .iter() | ||||||||
| .any(|((d, i), k)| *d == direction && *i == idx && *k == kind) | ||||||||
| } | ||||||||
|
|
||||||||
| /// Returns an iterator over the encoding commitment indices. | ||||||||
| pub fn iter_encoding(&self) -> impl Iterator<Item = &(Direction, Idx)> { | ||||||||
| self.commits.iter().filter_map(|(idx, kind)| match kind { | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| [1,2,3,4,5,6,"7",false,3.14] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -9007199254740991 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"foo": "bar", "bazz": 123, "buzz": [1,"5", {"buzz_foo": "bing", "buzz_bazz": 123}]} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| { | ||
| "string_tests": { | ||
| "empty": "", | ||
| "basic": "Hello, World!", | ||
| "escaped": "Quote(\") Tab(\t) Newline(\n) Backslash(\\)", | ||
| "unicode": "こんにちは 你好 안녕하세요 مرحبا Привет", | ||
| "emojis": "🌟 🌍 🎉 🚀 💻", | ||
| "mixed_unicode": "Hello 世界 with emojis 🎮 and symbols ™ ©️ ®️", | ||
| "control_chars": "\u0001\u0002\u0003", | ||
| "surrogate_pairs": "\uD834\uDD1E", | ||
| "special_whitespace": "Space\u2003Em\u2002En\u2000Quarter" | ||
| }, | ||
| "number_tests": { | ||
| "integer": 42, | ||
| "negative": -17, | ||
| "zero": 0, | ||
| "large": 9007199254740991, | ||
| "small": -9007199254740991, | ||
| "float": 3.14159, | ||
| "exponential": 1.23e-4, | ||
| "negative_exponential": -4.56E+3 | ||
| }, | ||
| "boolean_tests": { | ||
| "true_value": true, | ||
| "false_value": false | ||
| }, | ||
| "null_test": null, | ||
| "array_tests": { | ||
| "empty": [], | ||
| "numbers": [1, 2, 3, 4, 5], | ||
| "mixed": [1, "two", true, null, {"key": "value"}, [1, 2]], | ||
| "nested": [[1, 2], [3, 4], [["deep", "array"]]] | ||
| }, | ||
| "object_tests": { | ||
| "empty": {}, | ||
| "nested": { | ||
| "level1": { | ||
| "level2": { | ||
| "level3": "deep nesting" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "special_cases": { | ||
| "zero_fraction": 0.0, | ||
| "one_fraction": 1.0, | ||
| "long_string": "This is a somewhat longer string that extends beyond typical lengths to test buffer handling.................................", | ||
| "url_safe": "https://example.com/path?param=value&other=123#fragment", | ||
| "all_escaped": "\u0022\u005C\u002F\u0008\u000C\u000A\u000D\u0009" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| //! JSON data fixtures | ||
|
|
||
| use crate::define_fixture; | ||
|
|
||
| define_fixture!( | ||
| ARRAY, | ||
| "A JSON array.", | ||
| "../data/json/array" | ||
| ); | ||
|
|
||
| define_fixture!( | ||
| INTEGER, | ||
| "A JSON integer.", | ||
| "../data/json/integer" | ||
| ); | ||
|
|
||
| define_fixture!( | ||
| NESTED_OBJECT, | ||
| "A nested JSON object.", | ||
| "../data/json/nested_object" | ||
| ); | ||
|
|
||
| define_fixture!( | ||
| VALUES, | ||
| "A JSON object with various values.", | ||
| "../data/json/values" | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| pub mod http; | ||
| pub mod json; | ||
|
|
||
| macro_rules! define_fixture { | ||
| ($name:ident, $doc:tt, $path:tt) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||
| use std::error::Error; | ||||||||
|
|
||||||||
| use rangeset::{Difference, RangeSet, ToRangeSet}; | ||||||||
| use spansy::{json::KeyValue, Spanned}; | ||||||||
| use tlsn_core::transcript::{Direction, TranscriptCommitConfigBuilder}; | ||||||||
|
|
||||||||
|
|
@@ -150,15 +151,32 @@ pub trait JsonCommit { | |||||||
| .map_err(|e| JsonCommitError::new_with_source("failed to commit array", e))?; | ||||||||
|
|
||||||||
| if !array.elems.is_empty() { | ||||||||
| builder | ||||||||
| .commit(&array.without_values(), direction) | ||||||||
| .map_err(|e| { | ||||||||
| JsonCommitError::new_with_source("failed to commit array excluding values", e) | ||||||||
| let without_values = array.without_values(); | ||||||||
|
|
||||||||
| // Commit to the array excluding all values and separators. | ||||||||
| builder.commit(&without_values, direction).map_err(|e| { | ||||||||
| JsonCommitError::new_with_source("failed to commit array excluding values", e) | ||||||||
| })?; | ||||||||
|
|
||||||||
| // Commit to the separators and whitespace of the array | ||||||||
| let array_range: RangeSet<usize> = array.to_range_set().difference(&without_values); | ||||||||
| let difference = array | ||||||||
| .elems | ||||||||
| .iter() | ||||||||
| .map(|e| e.to_range_set()) | ||||||||
| .fold(array_range.clone(), |acc, range| acc.difference(&range)); | ||||||||
|
|
||||||||
| for range in difference.iter_ranges() { | ||||||||
| builder.commit(&range, direction).map_err(|e| { | ||||||||
| JsonCommitError::new_with_source("failed to commit array element", e) | ||||||||
| })?; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| // TODO: Commit each value separately, but we need a strategy for handling | ||||||||
| // separators. | ||||||||
| // Commit to the values of the array | ||||||||
| for elem in &array.elems { | ||||||||
| self.commit_value(builder, elem, direction)?; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| Ok(()) | ||||||||
| } | ||||||||
|
|
@@ -250,3 +268,76 @@ pub trait JsonCommit { | |||||||
| pub struct DefaultJsonCommitter {} | ||||||||
|
|
||||||||
| impl JsonCommit for DefaultJsonCommitter {} | ||||||||
|
|
||||||||
| #[cfg(test)] | ||||||||
| mod tests { | ||||||||
| use super::*; | ||||||||
| use rstest::*; | ||||||||
| use spansy::json::{parse_slice, JsonValue, JsonVisit}; | ||||||||
| use tlsn_core::transcript::{ | ||||||||
| Transcript, TranscriptCommitConfig, TranscriptCommitConfigBuilder, | ||||||||
| }; | ||||||||
| use tlsn_data_fixtures::json as fixtures; | ||||||||
|
|
||||||||
| #[rstest] | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| #[case::array(fixtures::ARRAY)] | ||||||||
| #[case::integer(fixtures::INTEGER)] | ||||||||
| #[case::json_object(fixtures::NESTED_OBJECT)] | ||||||||
| #[case::values(fixtures::VALUES)] | ||||||||
| fn test_json_commit(#[case] src: &'static [u8]) { | ||||||||
| let transcript = Transcript::new([], src); | ||||||||
| let json_data = parse_slice(src).unwrap(); | ||||||||
| let mut committer = DefaultJsonCommitter::default(); | ||||||||
| let mut builder = TranscriptCommitConfigBuilder::new(&transcript); | ||||||||
|
|
||||||||
| committer | ||||||||
| .commit_value(&mut builder, &json_data, Direction::Received) | ||||||||
| .unwrap(); | ||||||||
|
|
||||||||
| let config = builder.build().unwrap(); | ||||||||
|
|
||||||||
| struct CommitChecker<'a> { | ||||||||
| config: &'a TranscriptCommitConfig, | ||||||||
| } | ||||||||
| impl<'a> JsonVisit for CommitChecker<'a> { | ||||||||
| fn visit_value(&mut self, node: &JsonValue) { | ||||||||
| match node { | ||||||||
| JsonValue::Object(obj) => { | ||||||||
| assert!(self | ||||||||
| .config | ||||||||
| .contains(&obj.without_pairs(), Direction::Received)); | ||||||||
|
|
||||||||
| for kv in &obj.elems { | ||||||||
| assert!(self | ||||||||
| .config | ||||||||
| .contains(&kv.without_value(), Direction::Received)); | ||||||||
| } | ||||||||
|
|
||||||||
| JsonVisit::visit_object(self, obj); | ||||||||
| } | ||||||||
|
|
||||||||
| JsonValue::Array(arr) => { | ||||||||
| assert!(self | ||||||||
| .config | ||||||||
| .contains(&arr.without_values(), Direction::Received)); | ||||||||
|
|
||||||||
| JsonVisit::visit_array(self, arr); | ||||||||
| } | ||||||||
|
|
||||||||
| _ => { | ||||||||
| if !node.span().is_empty() { | ||||||||
| assert!( | ||||||||
| self.config.contains(node, Direction::Received), | ||||||||
| "failed to commit to value ({}), at {:?}", | ||||||||
| node.span().as_str(), | ||||||||
| node.span() | ||||||||
| ); | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| CommitChecker { config: &config }.visit_value(&json_data); | ||||||||
| } | ||||||||
| } | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.