Skip to content

Commit ff14d73

Browse files
Merge branch 'main' into scouten/update-wasi-test-nightly
2 parents 477b09a + d832104 commit ff14d73

5 files changed

Lines changed: 568 additions & 29 deletions

File tree

.github/workflows/tier-1b.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ jobs:
160160
- name: Install Rust toolchain
161161
uses: dtolnay/rust-toolchain@master
162162
with:
163-
toolchain: nightly-2025-07-28
163+
toolchain: nightly-2025-12-06
164164

165165
- name: Cache Rust dependencies
166166
uses: Swatinem/rust-cache@v2
@@ -169,7 +169,7 @@ jobs:
169169
env:
170170
FEATURES: ${{needs.get-features.outputs.openssl-features}}
171171
run: |
172-
cargo +nightly-2025-07-28 test -Z direct-minimal-versions --all-targets --features "$FEATURES"
172+
cargo +nightly-2025-12-06 test -Z direct-minimal-versions --all-targets --features "$FEATURES"
173173
174174
docs_rs:
175175
name: Preflight docs.rs build
@@ -203,7 +203,7 @@ jobs:
203203
- name: Install Rust toolchain
204204
uses: dtolnay/rust-toolchain@master
205205
with:
206-
toolchain: nightly-2025-11-04
206+
toolchain: nightly-2025-12-06
207207
# Pinning to specific nightly build for now. More recent versions
208208
# introduce a lifetime check that creates a whole slew of build
209209
# errors.

sdk/src/builder.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,6 @@ impl Builder {
867867
// we should be able to call Reader::from_stream and then convert to Builder
868868
// but we need to disable validation since we are not signing yet
869869
// so we will read the store directly here
870-
//crate::Reader::from_stream("application/c2pa", stream).and_then(|r| r.into_builder())
871870
let settings = crate::settings::get_settings().unwrap_or_default();
872871
let mut http_resolver = RestrictedResolver::new(SyncGenericResolver::new());
873872
http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone());
@@ -1814,9 +1813,7 @@ impl Builder {
18141813
let mut store = Store::new();
18151814
store.commit_claim(claim)?;
18161815

1817-
//store.to_jumbf_internal(1000)
18181816
store.get_data_hashed_manifest_placeholder(100, "application/c2pa")
1819-
//store.get_box_hashed_embeddable_manifest(signer.as_ref(), settings)
18201817
}
18211818
}
18221819

sdk/src/reader.rs

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -839,10 +839,65 @@ impl Reader {
839839
claim: &Claim,
840840
active_claim: &mut Claim,
841841
) -> Result<()> {
842+
let mut visited = std::collections::HashSet::new();
843+
let mut path = Vec::new();
844+
Self::collect_ingredient_claims_for_store_impl(
845+
store,
846+
claim,
847+
active_claim,
848+
&mut visited,
849+
&mut path,
850+
)
851+
}
852+
853+
/// Uses the similar cycle detection strategy as Store::get_claim_referenced_manifests_impl
854+
fn collect_ingredient_claims_for_store_impl(
855+
store: &Store,
856+
claim: &Claim,
857+
active_claim: &mut Claim,
858+
visited: &mut std::collections::HashSet<String>,
859+
path: &mut Vec<String>,
860+
) -> Result<()> {
861+
Self::collect_ingredient_claims_impl(store, claim, active_claim, visited, path)
862+
}
863+
864+
/// Shared implementation for recursively collecting ingredient claims.
865+
///
866+
/// Uses path-based cycle detection similar to Store::get_claim_referenced_manifests_impl:
867+
/// - `visited`: Claims that have been fully processed (allows DAG convergence)
868+
/// - `path`: Current traversal path to detect and skip cycles
869+
fn collect_ingredient_claims_impl(
870+
store: &Store,
871+
claim: &Claim,
872+
active_claim: &mut Claim,
873+
visited: &mut std::collections::HashSet<String>,
874+
path: &mut Vec<String>,
875+
) -> Result<()> {
876+
let claim_label = claim.label();
877+
878+
if visited.contains(claim_label) {
879+
return Ok(());
880+
}
881+
882+
// Check for cycle: is this claim already in our current path?
883+
// If so, skip it silently (validation should have already caught this when enabled)
884+
if path.iter().any(|p| p == claim_label) {
885+
return Ok(());
886+
}
887+
888+
path.push(claim_label.to_string());
889+
842890
for ingredient in claim.claim_ingredients() {
843-
if let Some(ingredient_claim) = store.get_claim(ingredient.label()) {
844-
// First, recursively collect any ingredients this claim references
845-
Self::collect_ingredient_claims_for_store(store, ingredient_claim, active_claim)?;
891+
let ingredient_label = ingredient.label();
892+
893+
if let Some(ingredient_claim) = store.get_claim(ingredient_label) {
894+
Self::collect_ingredient_claims_impl(
895+
store,
896+
ingredient_claim,
897+
active_claim,
898+
visited,
899+
path,
900+
)?;
846901

847902
// Then add this ingredient claim to the primary claim
848903
active_claim.replace_ingredient_or_insert(
@@ -851,6 +906,13 @@ impl Reader {
851906
);
852907
}
853908
}
909+
910+
// Mark as fully processed
911+
visited.insert(claim_label.to_string());
912+
913+
// Remove from current path
914+
path.pop();
915+
854916
Ok(())
855917
}
856918

@@ -997,16 +1059,31 @@ impl Reader {
9971059
builder.definition.instance_id = manifest.instance_id().to_owned();
9981060
builder.definition.thumbnail = manifest.thumbnail_ref().cloned();
9991061
builder.definition.redactions = manifest.redactions.take();
1000-
let ingredients = std::mem::take(&mut manifest.ingredients); //manifest.ingredients.drain(..).collect::<Vec<_>>();
1062+
let ingredients = std::mem::take(&mut manifest.ingredients);
10011063
for mut ingredient in ingredients {
10021064
if let Some(active_manifest) = ingredient.active_manifest() {
1003-
let ingredient_claim = self.store.remove_claim(active_manifest);
1065+
let ingredient_claim = self.store.get_claim(active_manifest);
10041066
if let Some(claim) = ingredient_claim {
10051067
// recreate an ingredient store to get the jumbf data
1006-
let mut ingredient_store = Store::new();
1007-
ingredient_store.commit_claim(claim.clone())?;
1068+
// ... recursively collect all nested ingredient claims
1069+
let ingredient_store = {
1070+
let mut ingredient_store = Store::new();
1071+
let mut active_claim = claim.clone();
1072+
1073+
// Recursion happens here for claims collection - re-embed nested claims from store
1074+
Self::collect_ingredient_claims_for_store(
1075+
&self.store,
1076+
claim,
1077+
&mut active_claim,
1078+
)?;
1079+
1080+
// Add the main claim with all nested ingredients
1081+
ingredient_store.commit_claim(active_claim)?;
1082+
ingredient_store
1083+
};
10081084
let jumbf = ingredient_store.to_jumbf_internal(0)?;
1009-
let manifest_data_ref = manifest.resources_mut().add_with(
1085+
// Add manifest_data to the ingredient's own resources
1086+
let manifest_data_ref = ingredient.resources_mut().add_with(
10101087
"manifest_data",
10111088
"application/c2pa",
10121089
jumbf,
@@ -1059,7 +1136,14 @@ impl Reader {
10591136
let mut active_claim = claim.clone();
10601137

10611138
// Recursively collect all ingredient claims and add them to primary_claim
1062-
self.collect_ingredient_claims_recursive(claim, &mut active_claim)?;
1139+
let mut visited = std::collections::HashSet::new();
1140+
let mut path = Vec::new();
1141+
self.collect_ingredient_claims_recursive(
1142+
claim,
1143+
&mut active_claim,
1144+
&mut visited,
1145+
&mut path,
1146+
)?;
10631147

10641148
// Add the main claim last
10651149
store.commit_claim(active_claim)?;
@@ -1077,20 +1161,10 @@ impl Reader {
10771161
&self,
10781162
claim: &Claim,
10791163
active_claim: &mut Claim,
1164+
visited: &mut std::collections::HashSet<String>,
1165+
path: &mut Vec<String>,
10801166
) -> Result<()> {
1081-
for ingredient in claim.claim_ingredients() {
1082-
if let Some(ingredient_claim) = self.store.get_claim(ingredient.label()) {
1083-
// First, recursively collect any ingredients this claim references
1084-
self.collect_ingredient_claims_recursive(ingredient_claim, active_claim)?;
1085-
1086-
// Then add this ingredient claim to the primary claim
1087-
active_claim.replace_ingredient_or_insert(
1088-
ingredient_claim.label().to_string(),
1089-
ingredient_claim.clone(),
1090-
);
1091-
}
1092-
}
1093-
Ok(())
1167+
Self::collect_ingredient_claims_impl(&self.store, claim, active_claim, visited, path)
10941168
}
10951169
}
10961170

sdk/src/store.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,63 @@ impl Store {
15411541
store.insert_restored_claim(cai_store_desc_box.label(), claim);
15421542
}
15431543

1544+
// Reconstruct nested claim relationships after all claims are loaded
1545+
// When claims are serialized, nested ingredients are extracted as top-level claims
1546+
// We need to restore them back into the parent claims' ingredient stores
1547+
use crate::assertions::Ingredient as IngredientAssertion;
1548+
let claim_labels: Vec<String> = store
1549+
.claims()
1550+
.iter()
1551+
.map(|c| c.label().to_string())
1552+
.collect();
1553+
for label in &claim_labels {
1554+
if let Some(claim) = store.get_claim(label) {
1555+
// Find ingredient assertions in this claim
1556+
let ingredient_refs: Vec<String> = claim
1557+
.ingredient_assertions()
1558+
.iter()
1559+
.filter_map(|ing_assertion| {
1560+
// Parse the ingredient assertion to get active_manifest or c2pa_manifest
1561+
match IngredientAssertion::from_assertion(ing_assertion.assertion()) {
1562+
Ok(ingredient) => {
1563+
// Check both active_manifest (v3) and c2pa_manifest (v2)
1564+
let hashed_uri = ingredient
1565+
.active_manifest
1566+
.as_ref()
1567+
.or(ingredient.c2pa_manifest.as_ref());
1568+
1569+
if let Some(hashed_uri) = hashed_uri {
1570+
let url = hashed_uri.url();
1571+
// Extract the manifest label from the JUMBF URI
1572+
jumbf::labels::manifest_label_from_uri(&url)
1573+
.map(|l| l.to_string())
1574+
} else {
1575+
None
1576+
}
1577+
}
1578+
Err(_) => None,
1579+
}
1580+
})
1581+
.collect();
1582+
1583+
if !ingredient_refs.is_empty() {
1584+
// Clone the claim for modification
1585+
let mut claim_mut = claim.clone();
1586+
for ing_ref in &ingredient_refs {
1587+
// Check if this referenced claim exists in the store
1588+
if let Some(nested_claim) = store.get_claim(ing_ref) {
1589+
claim_mut.replace_ingredient_or_insert(
1590+
ing_ref.to_string(),
1591+
nested_claim.clone(),
1592+
);
1593+
}
1594+
}
1595+
// Replace the claim in the store with the updated version
1596+
store.claims_map.insert(label.to_string(), claim_mut);
1597+
}
1598+
}
1599+
}
1600+
15441601
Ok(store)
15451602
}
15461603

@@ -4194,8 +4251,9 @@ impl Store {
41944251
final_redactions.append(&mut to_both);
41954252
}
41964253

4254+
let claims_to_add: Vec<Claim> = i_store_mut.claims().into_iter().cloned().collect();
41974255
claim.add_ingredient_data(
4198-
i_store_mut.claims().into_iter().cloned().collect(),
4256+
claims_to_add,
41994257
Some(final_redactions),
42004258
&svi.ingredient_references,
42014259
)?;

0 commit comments

Comments
 (0)