diff --git a/CHANGELOG.md b/CHANGELOG.md index 044ac777..b6e917a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Fixed + +- Panic when schema registry base URI contains an unencoded fragment. + +### Performance + +- Fewer JSON pointer lookups. + ## [0.28.2] - 2025-01-22 ### Fixed diff --git a/crates/jsonschema-py/CHANGELOG.md b/crates/jsonschema-py/CHANGELOG.md index 0c46dfe4..ab2bc145 100644 --- a/crates/jsonschema-py/CHANGELOG.md +++ b/crates/jsonschema-py/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Fixed + +- Panic when schema registry base URI contains an unencoded fragment. + +### Performance + +- Fewer JSON pointer lookups. + ## [0.28.2] - 2025-01-22 ### Fixed diff --git a/crates/jsonschema-referencing/src/registry.rs b/crates/jsonschema-referencing/src/registry.rs index 7d8171e0..a8bc6a3b 100644 --- a/crates/jsonschema-referencing/src/registry.rs +++ b/crates/jsonschema-referencing/src/registry.rs @@ -348,6 +348,7 @@ fn process_resources( let mut queue = VecDeque::with_capacity(32); let mut seen = AHashSet::new(); let mut external = AHashSet::new(); + let mut scratch = String::new(); // Populate the resources & queue from the input for (uri, resource) in pairs { @@ -377,7 +378,13 @@ fn process_resources( } // Collect references to external resources in this resource - collect_external_resources(&base, resource.contents(), &mut external, &mut seen)?; + collect_external_resources( + &base, + resource.contents(), + &mut external, + &mut seen, + &mut scratch, + )?; // Process subresources for subresource in resource.subresources() { @@ -390,6 +397,7 @@ fn process_resources( subresource.contents(), &mut external, &mut seen, + &mut scratch, )?; } else { collect_external_resources( @@ -397,6 +405,7 @@ fn process_resources( subresource.contents(), &mut external, &mut seen, + &mut scratch, )?; }; queue.push_back((base.clone(), subresource)); @@ -445,6 +454,7 @@ fn collect_external_resources( contents: &Value, collected: &mut AHashSet>, seen: &mut AHashSet, + scratch: &mut String, ) -> Result<(), Error> { // URN schemes are not supported for external resolution if base.scheme().as_str() == "urn" { @@ -477,7 +487,7 @@ fn collect_external_resources( // Handle local references separately as they may have nested references to external resources if reference.starts_with('#') { if let Some(referenced) = pointer(contents, reference.trim_start_matches('#')) { - collect_external_resources(base, referenced, collected, seen)?; + collect_external_resources(base, referenced, collected, seen, scratch)?; } continue; } @@ -494,8 +504,16 @@ fn collect_external_resources( let mut resolved = uri::resolve_against(&base_without_fragment.borrow(), path)?; // Add the fragment back if present if let Some(fragment) = fragment { - resolved = - resolved.with_fragment(Some(uri::EncodedString::new_or_panic(fragment))); + // It is cheaper to check if it is properly encoded than allocate given that + // the majority of inputs do not need to be additionally encoded + if let Some(encoded) = uri::EncodedString::new(fragment) { + resolved = resolved.with_fragment(Some(encoded)); + } else { + uri::encode_to(fragment, scratch); + resolved = + resolved.with_fragment(Some(uri::EncodedString::new_or_panic(scratch))); + scratch.clear(); + } } resolved } else { @@ -954,4 +972,11 @@ mod tests { .expect("Lookup failed"); assert_eq!(resolved.contents(), &json!({"type": "object"})); } + + #[test] + fn test_invalid_reference() { + // Found via fuzzing + let resource = Draft::Draft202012.create_resource(json!({"$schema": "$##"})); + let _ = Registry::try_new("http://#/", resource); + } } diff --git a/crates/jsonschema-referencing/tests/suite b/crates/jsonschema-referencing/tests/suite index f49bd3a9..9589db44 160000 --- a/crates/jsonschema-referencing/tests/suite +++ b/crates/jsonschema-referencing/tests/suite @@ -1 +1 @@ -Subproject commit f49bd3a9d50f532dc0ba2452dcb1f1e536c909c0 +Subproject commit 9589db44ef3961cc0437aef77188188ff2713163