1use crate::cc_attestation::mock::MockReport;
2use crate::error::{Error, Result};
3use crate::hash;
4use crate::storage::traits::StorageBackend;
5use atlas_c2pa_lib::cose::HashAlgorithm;
6use atlas_c2pa_lib::cross_reference::CrossReference;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::fs::File;
10use std::io::Write;
11use uuid::Uuid;
12pub mod common;
13pub mod config;
14pub mod dataset;
15pub mod evaluation;
16pub mod model;
17pub mod signer;
18pub mod software;
19pub mod utils;
20pub use dataset::create_manifest as create_dataset_manifest;
21pub use dataset::list_dataset_manifests as list_dataset_manifest;
22pub use dataset::verify_dataset_manifest;
23
24pub use model::create_manifest as create_model_manifest;
25pub use model::list_model_manifests as list_model_manifest;
26pub use model::verify_model_manifest;
27
28pub use software::create_manifest as create_software_manifest;
29pub use software::list_software_manifests;
30pub use software::verify_software_manifest;
31
32pub use evaluation::create_manifest as create_evaluation_manifest;
33
34pub use utils::{
35 determine_manifest_type, manifest_type_to_str, manifest_type_to_string, parse_manifest_type,
36};
37
38pub fn validate_hash_format(hash: &str) -> Result<()> {
59 if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
60 return Err(crate::error::Error::Validation(
61 "Invalid hash format".to_string(),
62 ));
63 }
64 if !is_supported_c2pa_hash_length(hash.len()) {
66 return Err(Error::Validation(format!(
67 "Expected 64, 96 or 128 characters for SHA-256, SHA-384, or SHA-512 got {}",
68 hash.len()
69 )));
70 }
71 Ok(())
72}
73
74pub fn link_manifests(
75 source_id: &str,
76 target_id: &str,
77 storage: &(impl StorageBackend + ?Sized),
78) -> Result<()> {
79 validate_manifest_id(source_id)?;
81 validate_manifest_id(target_id)?;
82
83 let mut source_manifest = match storage.retrieve_manifest(source_id) {
85 Ok(manifest) => manifest,
86 Err(e) => {
87 return Err(Error::Manifest(format!(
88 "Failed to retrieve source manifest {source_id}: {e}"
89 )));
90 }
91 };
92
93 let target_manifest = match storage.retrieve_manifest(target_id) {
94 Ok(manifest) => manifest,
95 Err(e) => {
96 return Err(Error::Manifest(format!(
97 "Failed to retrieve target manifest {target_id}: {e}"
98 )));
99 }
100 };
101
102 let algorithm = if let Some(first_ingredient) = source_manifest.ingredients.first() {
104 hash::parse_algorithm(first_ingredient.data.alg.as_str())?
105 } else {
106 if let Some(first_cross_ref) = source_manifest.cross_references.first() {
108 hash::detect_hash_algorithm(&first_cross_ref.manifest_hash) } else {
111 HashAlgorithm::Sha384 }
113 };
114
115 let duplicate_ref = source_manifest
117 .cross_references
118 .iter()
119 .find(|cr| cr.manifest_url == target_id);
120
121 if let Some(existing_ref) = duplicate_ref {
122 println!("Warning: A cross-reference to {target_id} already exists");
123
124 let target_json = serde_json::to_string(&target_manifest)
126 .map_err(|e| Error::Serialization(e.to_string()))?;
127 let target_hash = hash::calculate_hash_with_algorithm(target_json.as_bytes(), &algorithm);
128
129 if existing_ref.manifest_hash != target_hash {
130 println!("Manifest hash conflict detected, creating versioned reference");
132 return create_versioned_link(
133 source_manifest,
134 target_manifest,
135 source_id,
136 target_id,
137 storage,
138 &algorithm,
139 );
140 } else {
141 println!("Existing cross-reference is identical, no changes needed");
142 return Ok(());
143 }
144 }
145
146 let target_json =
148 serde_json::to_string(&target_manifest).map_err(|e| Error::Serialization(e.to_string()))?;
149 let target_hash = hash::calculate_hash_with_algorithm(target_json.as_bytes(), &algorithm);
150
151 let target_urn = ensure_c2pa_urn(target_id);
153
154 let cross_reference = CrossReference::new(target_urn, target_hash);
156
157 source_manifest.cross_references.push(cross_reference);
159
160 let updated_id = storage.store_manifest(&source_manifest)?;
162
163 println!("Successfully linked manifest {source_id} to {target_id}");
164 println!("Updated manifest ID: {updated_id}");
165 println!("Using hash algorithm: {}", algorithm.as_str());
166
167 Ok(())
168}
169
170fn create_versioned_link(
172 mut source_manifest: atlas_c2pa_lib::manifest::Manifest,
173 target_manifest: atlas_c2pa_lib::manifest::Manifest,
174 source_id: &str,
175 target_id: &str,
176 storage: &(impl StorageBackend + ?Sized),
177 algorithm: &HashAlgorithm,
178) -> Result<()> {
179 let parts: Vec<&str> = target_id.split(':').collect();
185 let uuid_part = if parts.len() >= 3 {
186 parts[2] } else {
188 target_id };
190
191 let claim_generator = target_manifest.claim.claim_generator_info.clone();
193
194 let mut max_version = 0;
196 for cr in &source_manifest.cross_references {
197 if cr
198 .manifest_url
199 .starts_with(&format!("urn:c2pa:{uuid_part}:"))
200 {
201 let parts: Vec<&str> = cr.manifest_url.split(':').collect();
202 if parts.len() >= 5 {
203 if let Some(version_reason) = parts.get(4) {
204 if let Some(version_str) = version_reason.split('_').next() {
205 if let Ok(version) = version_str.parse::<i32>() {
206 max_version = max_version.max(version);
207 }
208 }
209 }
210 }
211 }
212 }
213
214 let versioned_id = format!(
217 "urn:c2pa:{}:{}:{}_{}",
218 uuid_part,
219 claim_generator,
220 max_version + 1,
221 1
222 );
223
224 let target_json =
226 serde_json::to_string(&target_manifest).map_err(|e| Error::Serialization(e.to_string()))?;
227 let target_hash = hash::calculate_hash_with_algorithm(target_json.as_bytes(), algorithm);
228
229 let cross_reference = CrossReference::new(versioned_id.clone(), target_hash);
231
232 source_manifest.cross_references.push(cross_reference);
234
235 let updated_id = storage.store_manifest(&source_manifest)?;
237
238 println!(
239 "Successfully linked manifest {source_id} to {target_id} (versioned as {versioned_id})"
240 );
241 println!("Updated manifest ID: {updated_id}");
242 println!("Using hash algorithm: {}", algorithm.as_str());
243
244 Ok(())
245}
246
247pub fn show_manifest(id: &str, storage: &(impl StorageBackend + ?Sized)) -> Result<()> {
248 let manifest = storage.retrieve_manifest(id)?;
249
250 println!("============ Manifest Details ============");
251 println!("ID: {}", manifest.instance_id);
252 println!("Title: {}", manifest.title);
253 println!("Created: {}", manifest.created_at.0);
254 println!("Claim Generator: {}", manifest.claim_generator);
255 println!("Active: {}", manifest.is_active);
256
257 println!("\n------------ Claim Details -------------");
259 println!("Claim ID: {}", manifest.claim.instance_id);
260 println!("Claim Generated: {}", manifest.claim.created_at.0);
261 println!("Claim Generator: {}", manifest.claim.claim_generator_info);
262
263 if let Some(signature) = &manifest.claim.signature {
264 println!("\nSignature: {signature}");
265 } else {
266 println!("\nSignature: None (unsigned)");
267 }
268
269 println!("\n------------ Assertions -------------");
271 for (i, assertion) in manifest.claim.created_assertions.iter().enumerate() {
272 println!("\nAssertion #{}", i + 1);
273 match assertion {
274 atlas_c2pa_lib::assertion::Assertion::CreativeWork(creative) => {
275 println!(" Type: CreativeWork");
276 println!(" Context: {}", creative.context);
277 println!(" Creative Type: {}", creative.creative_type);
278
279 println!(" Authors:");
280 for author in &creative.author {
281 println!(" - {} ({})", author.name, author.author_type);
282 }
283 }
284 atlas_c2pa_lib::assertion::Assertion::Action(action) => {
285 println!(" Type: Action");
286 println!(" Actions:");
287 for action in &action.actions {
288 println!(" - Action: {}", action.action);
289 if let Some(agent) = &action.software_agent {
290 println!(" Software Agent: {agent}");
291 }
292 if let Some(source_type) = &action.digital_source_type {
293 println!(" Digital Source Type: {source_type}");
294 }
295 if let Some(params) = &action.parameters {
296 println!(
297 " Parameters: {}",
298 serde_json::to_string_pretty(params)
299 .unwrap_or_else(|_| format!("{params:?}"))
300 );
301 }
302 }
303 }
304 _ => println!(" Unknown assertion type"),
305 }
306 }
307
308 println!("\n------------ Ingredients -------------");
310 for (i, ingredient) in manifest.ingredients.iter().enumerate() {
311 println!("\nIngredient #{}: {}", i + 1, ingredient.title);
312 println!(" Document ID: {}", ingredient.document_id);
313 println!(" Instance ID: {}", ingredient.instance_id);
314 println!(" Format: {}", ingredient.format);
315 println!(" Relationship: {}", ingredient.relationship);
316
317 println!(" Data:");
318 println!(" URL: {}", ingredient.data.url);
319 println!(" Hash Algorithm: {}", ingredient.data.alg);
320 println!(" Hash: {}", ingredient.data.hash);
321
322 println!(" Data Types:");
323 for data_type in &ingredient.data.data_types {
324 println!(" - {data_type:?}");
325 }
326
327 if let Some(linked) = &ingredient.linked_ingredient {
328 println!(" Linked Ingredient: {linked:?}");
329 }
330
331 if let Some(key) = &ingredient.public_key {
332 println!(" Public Key: {key:?}");
333 }
334 }
335
336 if !manifest.cross_references.is_empty() {
338 println!("\n------------ Cross References -------------");
339 for (i, cross_ref) in manifest.cross_references.iter().enumerate() {
340 println!("\nReference #{}", i + 1);
341 println!(" URL: {}", cross_ref.manifest_url);
342 println!(" Hash: {}", cross_ref.manifest_hash);
343 }
344 }
345
346 Ok(())
347}
348
349pub mod linking {
350 use crate::error::{Error, Result};
351 use crate::storage::traits::StorageBackend;
352 use atlas_c2pa_lib::ingredient::{Ingredient, LinkedIngredient};
353 use atlas_c2pa_lib::manifest::Manifest;
354
355 pub fn link_dataset_to_model(
357 model_manifest_id: &str,
358 dataset_manifest_id: &str,
359 storage: &dyn StorageBackend,
360 ) -> Result<Manifest> {
361 let mut model_manifest = storage.retrieve_manifest(model_manifest_id)?;
363 let dataset_manifest = storage.retrieve_manifest(dataset_manifest_id)?;
364
365 if !is_dataset_manifest(&dataset_manifest) {
367 return Err(Error::Validation(format!(
368 "Manifest {dataset_manifest_id} is not a dataset manifest"
369 )));
370 }
371
372 let dataset_ingredients = dataset_manifest.ingredients;
374
375 for model_ingredient in &mut model_manifest.ingredients {
377 for dataset_ingredient in &dataset_ingredients {
378 let linked_ingredient = create_linked_ingredient(dataset_ingredient)?;
380 model_ingredient.data.linked_ingredient_url =
381 Some(dataset_ingredient.data.url.clone());
382 model_ingredient.data.linked_ingredient_hash =
383 Some(dataset_ingredient.data.hash.clone());
384 model_ingredient.linked_ingredient = Some(linked_ingredient);
385 }
386 }
387
388 storage.store_manifest(&model_manifest)?;
390
391 Ok(model_manifest)
392 }
393
394 fn is_dataset_manifest(manifest: &Manifest) -> bool {
396 manifest.ingredients.iter().any(|i| {
397 matches!(
398 i.data.data_types[0],
399 atlas_c2pa_lib::asset_type::AssetType::Dataset
400 | atlas_c2pa_lib::asset_type::AssetType::DatasetOnnx
401 | atlas_c2pa_lib::asset_type::AssetType::DatasetTensorFlow
402 | atlas_c2pa_lib::asset_type::AssetType::DatasetPytorch
403 )
404 })
405 }
406
407 fn create_linked_ingredient(dataset_ingredient: &Ingredient) -> Result<LinkedIngredient> {
409 Ok(LinkedIngredient {
410 url: dataset_ingredient.data.url.clone(),
411 hash: dataset_ingredient.data.hash.clone(),
412 media_type: dataset_ingredient.format.clone(),
413 })
414 }
415}
416
417pub fn validate_linked_manifests(
418 manifest_id: &str,
419 storage: &(impl StorageBackend + ?Sized),
420) -> Result<()> {
421 let manifest = storage.retrieve_manifest(manifest_id)?;
422
423 println!("Validating cross-references for manifest: {manifest_id}");
424
425 if manifest.cross_references.is_empty() {
426 println!("No cross-references found in manifest");
427 return Ok(());
428 }
429
430 println!("Found {} cross-references", manifest.cross_references.len());
431
432 let mut validation_errors = Vec::new();
433
434 for (index, cross_ref) in manifest.cross_references.iter().enumerate() {
435 println!(
436 "\nValidating cross-reference #{}: {}",
437 index + 1,
438 cross_ref.manifest_url
439 );
440
441 if let Err(hash_err) = validate_hash_format(&cross_ref.manifest_hash) {
443 let error = format!("Invalid hash format: {hash_err}");
444 validation_errors.push(error.clone());
445 println!(" ❌ {error}");
446 continue;
447 }
448
449 match storage.retrieve_manifest(&cross_ref.manifest_url) {
451 Ok(referenced_manifest) => {
452 let ref_json = match serde_json::to_string(&referenced_manifest) {
454 Ok(json) => json,
455 Err(e) => {
456 let error = format!("Failed to serialize referenced manifest: {e}");
457 validation_errors.push(error.clone());
458 println!(" ❌ {error}");
459 continue;
460 }
461 };
462
463 let algorithm = hash::detect_hash_algorithm(&cross_ref.manifest_hash);
464
465 let calculated_hash =
466 hash::calculate_hash_with_algorithm(ref_json.as_bytes(), &algorithm);
467
468 if calculated_hash == cross_ref.manifest_hash {
470 println!(" ✓ Hash verification successful");
471 } else {
472 let error = format!(
473 "Hash mismatch for manifest {}: stored={}, calculated={}",
474 cross_ref.manifest_url, cross_ref.manifest_hash, calculated_hash
475 );
476 validation_errors.push(error.clone());
477 println!(" ❌ {error}");
478 }
479
480 match atlas_c2pa_lib::manifest::validate_manifest(&referenced_manifest) {
482 Ok(_) => println!(" ✓ Manifest structure validation successful"),
483 Err(e) => {
484 let error = format!("Manifest structure validation failed: {e}");
485 validation_errors.push(error.clone());
486 println!(" ❌ {error}");
487 }
488 }
489 }
490 Err(e) => {
491 let error = format!("Failed to retrieve referenced manifest: {e}");
492 validation_errors.push(error.clone());
493 println!(" ❌ {error}");
494 }
495 }
496 }
497
498 if validation_errors.is_empty() {
500 println!("\nAll cross-references validated successfully");
501 Ok(())
502 } else {
503 println!(
504 "\nValidation failed with {} errors:",
505 validation_errors.len()
506 );
507 for (i, error) in validation_errors.iter().enumerate() {
508 println!(" {}. {}", i + 1, error);
509 }
510 Err(Error::Validation(
511 "Cross-reference validation failed".to_string(),
512 ))
513 }
514}
515
516fn is_supported_c2pa_hash_length(hash_len: usize) -> bool {
519 matches!(hash_len, 64 | 96 | 128)
520}
521
522pub fn verify_manifest_link(
524 source_id: &str,
525 target_id: &str,
526 storage: &(impl StorageBackend + ?Sized),
527) -> Result<bool> {
528 let source_manifest = storage.retrieve_manifest(source_id)?;
529
530 let target_urn = ensure_c2pa_urn(target_id);
532 let cross_ref = source_manifest
533 .cross_references
534 .iter()
535 .find(|cr| cr.manifest_url == target_id || cr.manifest_url == target_urn);
536
537 match cross_ref {
538 Some(reference) => {
539 let target_manifest = storage.retrieve_manifest(target_id)?;
541 let target_json = serde_json::to_string(&target_manifest)
542 .map_err(|e| Error::Serialization(e.to_string()))?;
543 let algorithm = hash::detect_hash_algorithm(&reference.manifest_hash);
544 let calculated_hash =
545 hash::calculate_hash_with_algorithm(target_json.as_bytes(), &algorithm);
546
547 if calculated_hash == reference.manifest_hash {
548 println!("Manifest link verified: {source_id} -> {target_id}");
549 println!("Hash verification successful");
550 Ok(true)
551 } else {
552 println!("Hash mismatch for linked manifest: {target_id}");
553 println!(" Stored hash: {}", reference.manifest_hash);
554 println!(" Calculated hash: {calculated_hash}");
555 Ok(false)
556 }
557 }
558 None => {
559 println!("No link found from {source_id} to {target_id}");
560 Ok(false)
561 }
562 }
563}
564
565pub fn validate_manifest_id(id: &str) -> Result<()> {
588 if id.is_empty() {
590 return Err(Error::Validation("Manifest ID cannot be empty".to_string()));
591 }
592
593 if id.starts_with("urn:c2pa:") {
595 let parts: Vec<&str> = id.split(':').collect();
597
598 if parts.len() < 3 {
600 return Err(Error::Validation(
601 "Invalid C2PA URN format. Expected urn:c2pa:UUID[:claim_generator[:version_reason]]".to_string()
602 ));
603 }
604
605 if Uuid::parse_str(parts[2]).is_err() {
607 return Err(Error::Validation(format!(
608 "Invalid UUID in C2PA URN: '{}'",
609 parts[2]
610 )));
611 }
612
613 if parts.len() >= 5 {
615 let version_reason = parts[4];
616 let vr_parts: Vec<&str> = version_reason.split('_').collect();
617
618 if vr_parts.len() != 2 {
619 return Err(Error::Validation(format!(
620 "Invalid version_reason format: expected 'version_reason', got '{version_reason}'"
621 )));
622 }
623
624 if vr_parts[0].parse::<u32>().is_err() {
626 return Err(Error::Validation(format!(
627 "Invalid version number in version_reason: '{}'",
628 vr_parts[0]
629 )));
630 }
631
632 if vr_parts[1].parse::<u32>().is_err() {
633 return Err(Error::Validation(format!(
634 "Invalid reason code in version_reason: '{}'",
635 vr_parts[1]
636 )));
637 }
638 }
639 } else {
640 if Uuid::parse_str(id).is_ok() {
642 } else if !id
644 .chars()
645 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
646 {
647 return Err(Error::Validation(format!(
649 "Invalid manifest ID format: '{id}'. Expected URN, UUID, or alphanumeric ID"
650 )));
651 }
652 }
653
654 Ok(())
655}
656
657pub fn ensure_c2pa_urn(id: &str) -> String {
679 if id.starts_with("urn:c2pa:") {
680 id.to_string() } else if Uuid::parse_str(id).is_ok() {
682 format!("urn:c2pa:{id}")
684 } else {
685 let uuid = Uuid::new_v4(); format!("urn:c2pa:{uuid}")
688 }
689}
690
691pub fn extract_uuid_from_urn(urn: &str) -> Result<Uuid> {
709 let parts: Vec<&str> = urn.split(':').collect();
710
711 if parts.len() < 3 || parts[0] != "urn" || parts[1] != "c2pa" {
712 return Err(Error::Validation(format!(
713 "Invalid C2PA URN format: '{urn}'"
714 )));
715 }
716
717 Uuid::parse_str(parts[2])
718 .map_err(|e| Error::Validation(format!("Invalid UUID in C2PA URN '{urn}': {e}")))
719}
720
721#[derive(Debug, Serialize, Deserialize)]
722pub struct ManifestNode {
723 pub id: String,
724 pub title: String,
725 pub manifest_type: String,
726 pub created_at: String,
727 pub ingredients: Vec<String>,
728 pub assertions: Vec<AssertionInfo>,
729 pub references: Vec<ReferenceInfo>,
730 pub signature: Option<bool>,
731}
732
733#[derive(Debug, Serialize, Deserialize)]
735pub struct AssertionInfo {
736 pub type_name: String,
737 pub details: serde_json::Value,
738}
739
740#[derive(Debug, Serialize, Deserialize)]
742pub struct ReferenceInfo {
743 pub target_id: String,
744 pub relation_type: String, }
746
747#[derive(Debug, Serialize, Deserialize)]
749pub struct ProvenanceGraph {
750 pub root_id: String,
751 pub nodes: HashMap<String, ManifestNode>,
752 pub edges: Vec<Edge>,
753}
754
755#[derive(Debug, Serialize, Deserialize)]
757pub struct Edge {
758 pub source: String,
759 pub target: String,
760 pub relation_type: String,
761}
762
763pub fn export_provenance(
765 id: &str,
766 storage: &(impl StorageBackend + ?Sized),
767 format: &str,
768 output_path: Option<&str>,
769 max_depth: u32,
770) -> Result<()> {
771 let _root_manifest = match storage.retrieve_manifest(id) {
773 Ok(manifest) => manifest,
774 Err(e) => {
775 return Err(Error::Manifest(format!(
776 "Failed to retrieve root manifest {id}: {e}"
777 )));
778 }
779 };
780
781 let mut graph = ProvenanceGraph {
783 root_id: id.to_string(),
784 nodes: HashMap::new(),
785 edges: Vec::new(),
786 };
787
788 let mut visited = HashSet::new();
790
791 build_provenance_graph(id, storage, &mut graph, &mut visited, max_depth, 0)?;
793
794 let serialized = match format.to_lowercase().as_str() {
796 "json" => serde_json::to_string_pretty(&graph)
797 .map_err(|e| Error::Serialization(format!("Failed to serialize to JSON: {e}")))?,
798 "yaml" => {
799 #[cfg(feature = "yaml")]
800 {
801 serde_yaml::to_string(&graph).map_err(|e| {
802 Error::Serialization(format!("Failed to serialize to YAML: {e}"))
803 })?
804 }
805
806 #[cfg(not(feature = "yaml"))]
807 {
808 return Err(Error::Validation("YAML format not supported. Add serde_yaml to dependencies and enable the 'yaml' feature.".to_string()));
809 }
810 }
811 _ => {
812 return Err(Error::Validation(format!(
813 "Invalid output format '{format}'. Valid options are: json, yaml"
814 )));
815 }
816 };
817
818 if let Some(path) = output_path {
820 let mut file = File::create(path).map_err(Error::Io)?;
821 file.write_all(serialized.as_bytes()).map_err(Error::Io)?;
822 println!("Provenance graph exported to: {path}");
823 } else {
824 println!("{serialized}");
826 }
827
828 Ok(())
829}
830fn build_provenance_graph(
832 id: &str,
833 storage: &(impl StorageBackend + ?Sized),
834 graph: &mut ProvenanceGraph,
835 visited: &mut HashSet<String>,
836 max_depth: u32,
837 current_depth: u32,
838) -> Result<()> {
839 if visited.contains(id) || current_depth > max_depth {
841 return Ok(());
842 }
843
844 visited.insert(id.to_string());
846
847 let manifest = match storage.retrieve_manifest(id) {
849 Ok(manifest) => manifest,
850 Err(e) => {
851 return Err(Error::Manifest(format!(
852 "Failed to retrieve manifest {id}: {e}"
853 )));
854 }
855 };
856
857 let manifest_type = determine_manifest_type(&manifest);
859
860 let mut assertions = Vec::new();
862 if let Some(claim) = &manifest.claim_v2 {
863 for assertion in &claim.created_assertions {
864 let details = extract_assertion_details(assertion);
865 let type_name = match assertion {
866 atlas_c2pa_lib::assertion::Assertion::CreativeWork(_) => "CreativeWork",
867 atlas_c2pa_lib::assertion::Assertion::Action(_) => "Action",
868 atlas_c2pa_lib::assertion::Assertion::DoNotTrain(_) => "DoNotTrain",
869 atlas_c2pa_lib::assertion::Assertion::CustomAssertion(_) => "TrustedHardware",
870 _ => "Other",
871 };
872 assertions.push(AssertionInfo {
873 type_name: type_name.to_string(),
874 details,
875 });
876 }
877 }
878
879 let ingredient_ids = manifest
881 .ingredients
882 .iter()
883 .map(|ingredient| ingredient.instance_id.clone())
884 .collect::<Vec<String>>();
885
886 let node = ManifestNode {
888 id: id.to_string(),
889 title: manifest.title.clone(),
890 manifest_type: manifest_type_to_string(&manifest_type),
891 created_at: manifest.created_at.0.to_string(),
892 ingredients: ingredient_ids,
893 assertions,
894 references: Vec::new(), signature: manifest.claim_v2.as_ref().map(|c| c.signature.is_some()),
896 };
897
898 graph.nodes.insert(id.to_string(), node);
900
901 for cross_ref in &manifest.cross_references {
903 let target_id = &cross_ref.manifest_url;
904
905 if let Some(node) = graph.nodes.get_mut(id) {
907 node.references.push(ReferenceInfo {
908 target_id: target_id.clone(),
909 relation_type: "references".to_string(),
910 });
911 }
912
913 graph.edges.push(Edge {
915 source: id.to_string(),
916 target: target_id.clone(),
917 relation_type: "references".to_string(),
918 });
919
920 build_provenance_graph(
922 target_id,
923 storage,
924 graph,
925 visited,
926 max_depth,
927 current_depth + 1,
928 )?;
929
930 if let Some(node) = graph.nodes.get_mut(target_id) {
932 node.references.push(ReferenceInfo {
933 target_id: id.to_string(),
934 relation_type: "isReferencedBy".to_string(),
935 });
936 }
937
938 graph.edges.push(Edge {
940 source: target_id.clone(),
941 target: id.to_string(),
942 relation_type: "isReferencedBy".to_string(),
943 });
944 }
945
946 Ok(())
947}
948
949fn extract_assertion_details(
951 assertion: &atlas_c2pa_lib::assertion::Assertion,
952) -> serde_json::Value {
953 match assertion {
954 atlas_c2pa_lib::assertion::Assertion::CreativeWork(creative) => {
955 serde_json::json!({
956 "creative_type": creative.creative_type,
957 "authors": creative.author.iter().map(|a| {
958 serde_json::json!({
959 "type": a.author_type,
960 "name": a.name,
961 })
962 }).collect::<Vec<_>>(),
963 })
964 }
965 atlas_c2pa_lib::assertion::Assertion::Action(action) => {
966 serde_json::json!({
967 "actions": action.actions.iter().map(|a| {
968 let mut action_obj = serde_json::json!({
969 "action": a.action,
970 });
971
972 if let Some(agent) = &a.software_agent {
973 action_obj.as_object_mut().unwrap().insert(
974 "software_agent".to_string(),
975 serde_json::Value::String(agent.clone())
976 );
977 }
978
979 if let Some(params) = &a.parameters {
980 action_obj.as_object_mut().unwrap().insert(
981 "parameters".to_string(),
982 params.clone()
983 );
984 }
985
986 action_obj
987 }).collect::<Vec<_>>(),
988 })
989 }
990 atlas_c2pa_lib::assertion::Assertion::DoNotTrain(do_not_train) => {
991 serde_json::json!({
992 "reason": do_not_train.reason,
993 "enforced": do_not_train.enforced,
994 })
995 }
996 atlas_c2pa_lib::assertion::Assertion::CustomAssertion(custom) => {
997 let r_str = custom.data.as_str().unwrap();
998 let r: MockReport = serde_json::from_str(r_str).unwrap();
999 serde_json::json!({
1000 "label": custom.label,
1001 "data": r,
1002 })
1003 }
1004 _ => serde_json::json!({"type": "Unknown"}),
1005 }
1006}