atlas_cli/manifest/
mod.rs

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
38/// Validate that a hash string is in the correct format.
39/// Supported formats are SHA-256, SHA-384, or SHA-512.
40///
41/// # Examples
42///
43/// ```
44/// use atlas_cli::manifest::validate_hash_format;
45///
46/// // Valid 96-character hex string
47/// let valid_hash = "a".repeat(96);
48/// assert!(validate_hash_format(&valid_hash).is_ok());
49///
50/// // Invalid: wrong length
51/// let short_hash = "abc123";
52/// assert!(validate_hash_format(&short_hash).is_err());
53///
54/// // Invalid: non-hex characters
55/// let invalid_chars = "g".repeat(96);
56/// assert!(validate_hash_format(&invalid_chars).is_err());
57/// ```
58pub 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    // Check if the hash has the expected length for SHA-256, SHA-384 or SHA-512
65    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 IDs format
80    validate_manifest_id(source_id)?;
81    validate_manifest_id(target_id)?;
82
83    // Retrieve both manifests
84    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    // Detect the hash algorithm used in the source manifest
103    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 no ingredients, check if source manifest has any cross-references
107        if let Some(first_cross_ref) = source_manifest.cross_references.first() {
108            // Detect algorithm from existing cross-reference hash length
109            hash::detect_hash_algorithm(&first_cross_ref.manifest_hash) // This already returns HashAlgorithm
110        } else {
111            HashAlgorithm::Sha384 // Default if no ingredients or cross-references
112        }
113    };
114
115    // Check if a cross-reference to this target already exists
116    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        // Check if hash matches (if it doesn't, this could indicate a conflict)
125        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            // Handle conflict by creating a versioned reference
131            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    // Create a hash of the target manifest using the detected algorithm
147    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    // Convert IDs to proper C2PA URNs if they're not already
152    let target_urn = ensure_c2pa_urn(target_id);
153
154    // Create a cross-reference from source to target
155    let cross_reference = CrossReference::new(target_urn, target_hash);
156
157    // Add the cross-reference to the source manifest
158    source_manifest.cross_references.push(cross_reference);
159
160    // Update the source manifest in storage
161    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
170// Create a versioned link when there's a conflict
171fn 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    // Generate a versioned ID following C2PA spec section 8.2
180    // Format: original_urn:claim_generator:version_reason
181    // where version_reason is version_number_reason_code
182
183    // Parse the existing ID to maintain the UUID part
184    let parts: Vec<&str> = target_id.split(':').collect();
185    let uuid_part = if parts.len() >= 3 {
186        parts[2] // Extract UUID from urn:c2pa:UUID format
187    } else {
188        target_id // Use as-is if not in expected format
189    };
190
191    // Extract claim generator info
192    let claim_generator = target_manifest.claim.claim_generator_info.clone();
193
194    // Find next version number by looking at existing references
195    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    // Create new versioned ID
215    // Reason code 1 = Conflict with another C2PA Manifest
216    let versioned_id = format!(
217        "urn:c2pa:{}:{}:{}_{}",
218        uuid_part,
219        claim_generator,
220        max_version + 1,
221        1
222    );
223
224    // Create a hash of the target manifest using the specified algorithm
225    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    // Create a cross-reference with the versioned ID
230    let cross_reference = CrossReference::new(versioned_id.clone(), target_hash);
231
232    // Add the cross-reference to the source manifest
233    source_manifest.cross_references.push(cross_reference);
234
235    // Update the source manifest in storage
236    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    // Display claim details
258    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    // Display assertions
270    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    // Display ingredients
309    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    // Display cross-references if any
337    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    /// Links a dataset ingredient to a model ingredient
356    pub fn link_dataset_to_model(
357        model_manifest_id: &str,
358        dataset_manifest_id: &str,
359        storage: &dyn StorageBackend,
360    ) -> Result<Manifest> {
361        // Retrieve both manifests
362        let mut model_manifest = storage.retrieve_manifest(model_manifest_id)?;
363        let dataset_manifest = storage.retrieve_manifest(dataset_manifest_id)?;
364
365        // Verify the dataset manifest type
366        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        // Get all dataset ingredients
373        let dataset_ingredients = dataset_manifest.ingredients;
374
375        // Update each model ingredient with dataset links
376        for model_ingredient in &mut model_manifest.ingredients {
377            for dataset_ingredient in &dataset_ingredients {
378                // Create linked ingredient
379                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        // Store updated model manifest
389        storage.store_manifest(&model_manifest)?;
390
391        Ok(model_manifest)
392    }
393
394    /// Checks if a manifest is a dataset manifest
395    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    /// Creates linked ingredient from a dataset ingredient
408    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        // Validate the hash format first
442        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        // Try to retrieve the referenced manifest
450        match storage.retrieve_manifest(&cross_ref.manifest_url) {
451            Ok(referenced_manifest) => {
452                // Calculate hash of the referenced manifest
453                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                // Compare calculated hash with stored hash
469                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                // Check manifest structure
481                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    // Summarize validation results
499    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
516/// Check whether a given hash (hex-encoded) length matches one of the
517/// C2PA-supported algorithms (must be one of SHA-256, SHA-384, SHA-512).
518fn is_supported_c2pa_hash_length(hash_len: usize) -> bool {
519    matches!(hash_len, 64 | 96 | 128)
520}
521
522/// Helper function to verify a single manifest link
523pub 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    // Find the cross-reference to the target
531    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            // Target reference found, verify hash
540            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
565/// Validate a manifest ID format
566///
567/// # Examples
568///
569/// ```
570/// use atlas_cli::manifest::validate_manifest_id;
571/// use uuid::Uuid;
572///
573/// // Valid UUID
574/// let uuid = Uuid::new_v4().to_string();
575/// assert!(validate_manifest_id(&uuid).is_ok());
576///
577/// // Valid C2PA URN
578/// let urn = format!("urn:c2pa:{}", uuid);
579/// assert!(validate_manifest_id(&urn).is_ok());
580///
581/// // Invalid: empty string
582/// assert!(validate_manifest_id("").is_err());
583///
584/// // Valid: alphanumeric ID
585/// assert!(validate_manifest_id("model-123").is_ok());
586/// ```
587pub fn validate_manifest_id(id: &str) -> Result<()> {
588    // Basic validation
589    if id.is_empty() {
590        return Err(Error::Validation("Manifest ID cannot be empty".to_string()));
591    }
592
593    // Check if it's already a C2PA URN
594    if id.starts_with("urn:c2pa:") {
595        // Full validation according to spec
596        let parts: Vec<&str> = id.split(':').collect();
597
598        // Minimum: urn:c2pa:UUID
599        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        // Validate UUID part
606        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 version_reason is present, validate it (format: version_reason)
614        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            // Validate that both parts are numeric
625            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 not a URN, try to validate as UUID or other format
641        if Uuid::parse_str(id).is_ok() {
642            // Valid UUID, which is good
643        } else if !id
644            .chars()
645            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
646        {
647            // Basic validation for other ID formats
648            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
657/// Ensure an ID is in C2PA URN format
658///
659/// # Examples
660///
661/// ```
662/// use atlas_cli::manifest::ensure_c2pa_urn;
663/// use uuid::Uuid;
664///
665/// // UUID gets converted to URN
666/// let uuid = Uuid::new_v4().to_string();
667/// let urn = ensure_c2pa_urn(&uuid);
668/// assert!(urn.starts_with("urn:c2pa:"));
669///
670/// // Already a URN remains unchanged
671/// let existing_urn = "urn:c2pa:12345678-1234-1234-1234-123456789012";
672/// assert_eq!(ensure_c2pa_urn(existing_urn), existing_urn);
673///
674/// // Non-UUID gets new UUID generated
675/// let result = ensure_c2pa_urn("custom-id");
676/// assert!(result.starts_with("urn:c2pa:"));
677/// ```
678pub fn ensure_c2pa_urn(id: &str) -> String {
679    if id.starts_with("urn:c2pa:") {
680        id.to_string() // Already in correct format
681    } else if Uuid::parse_str(id).is_ok() {
682        // It's a valid UUID, convert to URN
683        format!("urn:c2pa:{id}")
684    } else {
685        // Not a UUID, generate a new one
686        let uuid = Uuid::new_v4(); // Using new_v4() instead of new_v5()
687        format!("urn:c2pa:{uuid}")
688    }
689}
690
691/// Extract UUID from a C2PA URN
692///
693/// # Examples
694///
695/// ```
696/// use atlas_cli::manifest::extract_uuid_from_urn;
697/// use uuid::Uuid;
698///
699/// let uuid = Uuid::new_v4();
700/// let urn = format!("urn:c2pa:{}", uuid);
701///
702/// let extracted = extract_uuid_from_urn(&urn).unwrap();
703/// assert_eq!(extracted, uuid);
704///
705/// // Invalid URN format
706/// assert!(extract_uuid_from_urn("invalid:urn").is_err());
707/// ```
708pub 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/// Simplified representation of an assertion for export
734#[derive(Debug, Serialize, Deserialize)]
735pub struct AssertionInfo {
736    pub type_name: String,
737    pub details: serde_json::Value,
738}
739
740/// Simplified representation of a cross-reference for export
741#[derive(Debug, Serialize, Deserialize)]
742pub struct ReferenceInfo {
743    pub target_id: String,
744    pub relation_type: String, // "references", "isReferencedBy", etc.
745}
746
747/// Full provenance graph representation
748#[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/// Edge in the provenance graph
756#[derive(Debug, Serialize, Deserialize)]
757pub struct Edge {
758    pub source: String,
759    pub target: String,
760    pub relation_type: String,
761}
762
763/// Export the full provenance graph for a manifest
764pub 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    // Retrieve the root manifest, we just care if exisit, so _
772    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    // Initialize provenance graph data structure
782    let mut graph = ProvenanceGraph {
783        root_id: id.to_string(),
784        nodes: HashMap::new(),
785        edges: Vec::new(),
786    };
787
788    // Keep track of visited manifests to avoid cycles
789    let mut visited = HashSet::new();
790
791    // Build the graph recursively starting from the root manifest
792    build_provenance_graph(id, storage, &mut graph, &mut visited, max_depth, 0)?;
793
794    // Serialize the graph based on the requested format
795    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    // Output the serialized graph
819    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        // Print to stdout
825        println!("{serialized}");
826    }
827
828    Ok(())
829}
830/// Recursively build the provenance graph
831fn 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    // Check if we've already visited this manifest or exceeded max depth
840    if visited.contains(id) || current_depth > max_depth {
841        return Ok(());
842    }
843
844    // Mark as visited
845    visited.insert(id.to_string());
846
847    // Retrieve the manifest
848    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    // Determine manifest type using the new function
858    let manifest_type = determine_manifest_type(&manifest);
859
860    // Extract assertions
861    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    // Extract ingredient IDs
880    let ingredient_ids = manifest
881        .ingredients
882        .iter()
883        .map(|ingredient| ingredient.instance_id.clone())
884        .collect::<Vec<String>>();
885
886    // Create node for this manifest
887    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(), // Will populate below
895        signature: manifest.claim_v2.as_ref().map(|c| c.signature.is_some()),
896    };
897
898    // Add node to graph
899    graph.nodes.insert(id.to_string(), node);
900
901    // Process cross-references
902    for cross_ref in &manifest.cross_references {
903        let target_id = &cross_ref.manifest_url;
904
905        // Add references to the node
906        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        // Add edge to the graph
914        graph.edges.push(Edge {
915            source: id.to_string(),
916            target: target_id.clone(),
917            relation_type: "references".to_string(),
918        });
919
920        // Recursively process the referenced manifest
921        build_provenance_graph(
922            target_id,
923            storage,
924            graph,
925            visited,
926            max_depth,
927            current_depth + 1,
928        )?;
929
930        // Add backward edge for the referenced manifest
931        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        // Add backward edge to the graph
939        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
949/// Extract details from an assertion in a simplified form
950fn 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}