Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions crates/core/src/transcript/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,46 @@ impl TranscriptCommitConfig {
.any(|(_, kind)| matches!(kind, TranscriptCommitmentKind::Encoding))
}

/// Returns whether the builder has a commitment for the given direction and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Returns whether the builder has a commitment for the given direction and
/// Returns `true` if the configuration contains the provided ranges.

/// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This being O(N) isn't great, but ok for now.

}

/// Returns whether the builder has a commitment for the given direction and
/// range, with the given kind.
Comment on lines +88 to +89
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Returns whether the builder has a commitment for the given direction and
/// range, with the given kind.
/// Returns `true` if the configuration contains the provided ranges with the given commitment kind.

///
/// # 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 {
Expand Down
1 change: 1 addition & 0 deletions crates/data-fixtures/data/json/array
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[1,2,3,4,5,6,"7",false,3.14]
1 change: 1 addition & 0 deletions crates/data-fixtures/data/json/integer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-9007199254740991
1 change: 1 addition & 0 deletions crates/data-fixtures/data/json/nested_object
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}]}
51 changes: 51 additions & 0 deletions crates/data-fixtures/data/json/values
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"
}
}
27 changes: 27 additions & 0 deletions crates/data-fixtures/src/json.rs
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"
);
1 change: 1 addition & 0 deletions crates/data-fixtures/src/lib.rs
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) => {
Expand Down
1 change: 1 addition & 0 deletions crates/formats/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ tlsn-core = { workspace = true }

bytes = { workspace = true }
spansy = { workspace = true, features = ["serde"] }
rangeset = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
Expand Down
105 changes: 98 additions & 7 deletions crates/formats/src/json/commit.rs
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};

Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[rstest]
// Asserts the behavior that every value in the transcript is committed individually.
#[rstest]

#[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);
}
}