Skip to main content

c2pa/
ingredient.rs

1// Copyright 2022 Adobe. All rights reserved.
2// This file is licensed to you under the Apache License,
3// or the MIT license (http://opensource.org/licenses/MIT),
4// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
5// at your option.
6
7// Unless required by applicable law or agreed to in writing,
8// this software is distributed on an "AS IS" BASIS, WITHOUT
9// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
10// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
11// specific language governing permissions and limitations under
12// each license.
13
14#![deny(missing_docs)]
15#[cfg(feature = "file_io")]
16use std::path::{Path, PathBuf};
17use std::{borrow::Cow, io::Cursor};
18
19use async_generic::async_generic;
20use log::{debug, error};
21#[cfg(feature = "json_schema")]
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use uuid::Uuid;
25
26#[cfg(doc)]
27use crate::Manifest;
28use crate::{
29    assertion::{Assertion, AssertionBase},
30    assertions::{
31        self, labels, AssertionMetadata, AssetType, CertificateStatus, EmbeddedData, Relationship,
32    },
33    asset_io::CAIRead,
34    claim::{Claim, ClaimAssetData},
35    context::Context,
36    crypto::base64,
37    error::{Error, Result},
38    hashed_uri::HashedUri,
39    jumbf::{
40        self,
41        labels::{assertion_label_from_uri, manifest_label_from_uri},
42    },
43    log_item,
44    resource_store::{ResourceRef, ResourceStore},
45    status_tracker::StatusTracker,
46    store::Store,
47    utils::{
48        mime::{extension_to_mime, format_to_mime},
49        xmp_inmemory_utils::XmpInfo,
50    },
51    validation_results::ValidationResults,
52    validation_status::{self, ValidationStatus},
53};
54
55#[derive(Clone, Debug, Default, Deserialize, Serialize)]
56#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
57/// An `Ingredient` is any external asset that has been used in the creation of an asset.
58pub struct Ingredient {
59    /// A human-readable title, generally source filename.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    title: Option<String>,
62
63    /// The format of the source file as a MIME type.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    format: Option<String>,
66
67    /// Document ID from `xmpMM:DocumentID` in XMP metadata.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    document_id: Option<String>,
70
71    /// Instance ID from `xmpMM:InstanceID` in XMP metadata.
72    //#[serde(default = "default_instance_id")]
73    #[serde(skip_serializing_if = "Option::is_none")]
74    instance_id: Option<String>,
75
76    /// URI from `dcterms:provenance` in XMP metadata.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    provenance: Option<String>,
79
80    /// A thumbnail image capturing the visual state at the time of import.
81    ///
82    /// A tuple of thumbnail MIME format (for example `image/jpeg`) and binary bits of the image.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    thumbnail: Option<ResourceRef>,
85
86    /// An optional hash of the asset to prevent duplicates.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    hash: Option<String>,
89
90    /// Set to `ParentOf` if this is the parent ingredient.
91    ///
92    /// There can only be one parent ingredient in the ingredients.
93    // is_parent: Option<bool>,
94    #[serde(default = "default_relationship")]
95    relationship: Relationship,
96
97    /// The active manifest label (if one exists).
98    ///
99    /// If this ingredient has a [`ManifestStore`],
100    /// this will hold the label of the active [`Manifest`].
101    ///
102    /// [`Manifest`]: crate::Manifest
103    /// [`ManifestStore`]: crate::ManifestStore
104    #[serde(skip_serializing_if = "Option::is_none")]
105    active_manifest: Option<String>,
106
107    /// Validation status (Ingredient v1 & v2)
108    #[serde(skip_serializing_if = "Option::is_none")]
109    validation_status: Option<Vec<ValidationStatus>>,
110
111    /// Validation results (Ingredient.V3)
112    #[serde(skip_serializing_if = "Option::is_none")]
113    validation_results: Option<ValidationResults>,
114
115    /// A reference to the actual data of the ingredient.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    data: Option<ResourceRef>,
118
119    /// Additional description of the ingredient.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    description: Option<String>,
122
123    /// URI to an informational page about the ingredient or its data.
124    #[serde(rename = "informational_URI", skip_serializing_if = "Option::is_none")]
125    informational_uri: Option<String>,
126
127    /// Any additional [`Metadata`] as defined in the C2PA spec.
128    ///
129    /// [`Metadata`]: crate::Metadata
130    #[serde(skip_serializing_if = "Option::is_none")]
131    metadata: Option<AssertionMetadata>,
132
133    /// Additional information about the data's type to the ingredient V2 structure.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    data_types: Option<Vec<AssetType>>,
136
137    /// A [`ManifestStore`] from the source asset extracted as a binary C2PA blob.
138    ///
139    /// [`ManifestStore`]: crate::ManifestStore
140    #[serde(skip_serializing_if = "Option::is_none")]
141    manifest_data: Option<ResourceRef>,
142
143    /// The ingredient's label as assigned in the manifest.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    label: Option<String>,
146
147    #[serde(skip)]
148    resources: ResourceStore,
149
150    #[serde(skip_serializing_if = "Option::is_none")]
151    ocsp_responses: Option<Vec<ResourceRef>>,
152}
153
154fn default_instance_id() -> String {
155    format!("xmp:iid:{}", Uuid::new_v4())
156}
157
158fn default_relationship() -> Relationship {
159    Relationship::default()
160}
161
162impl Ingredient {
163    /// Constructs a new `Ingredient`.
164    ///
165    /// # Arguments
166    ///
167    /// * `title` - A user-displayable name for this ingredient (often a filename).
168    /// * `format` - The MIME media type of the ingredient, for example `image/jpeg`.
169    /// * `instance_id` - A unique identifier, such as the value of the ingredient's `xmpMM:InstanceID`.
170    ///
171    /// # Example
172    ///
173    /// ```
174    /// use c2pa::Ingredient;
175    /// let ingredient = Ingredient::new("title", "image/jpeg", "ed610ae51f604002be3dbf0c589a2f1f");
176    /// ```
177    pub fn new<S>(title: S, format: S, instance_id: S) -> Self
178    where
179        S: Into<String>,
180    {
181        Self {
182            title: Some(title.into()),
183            format: Some(format.into()),
184            instance_id: Some(instance_id.into()),
185            ..Default::default()
186        }
187    }
188
189    /// Constructs a new V2 `Ingredient`.
190    ///
191    /// # Arguments
192    ///
193    /// * `title` - A user-displayable name for this ingredient (often a filename).
194    /// * `format` - The MIME media type of the ingredient, for example `image/jpeg`.
195    ///
196    /// # Example
197    ///
198    /// ```
199    /// use c2pa::Ingredient;
200    /// let ingredient = Ingredient::new_v2("title", "image/jpeg");
201    /// ```
202    pub fn new_v2<S1, S2>(title: S1, format: S2) -> Self
203    where
204        S1: Into<String>,
205        S2: Into<String>,
206    {
207        Self {
208            title: Some(title.into()),
209            format: Some(format.into()),
210            ..Default::default()
211        }
212    }
213
214    // try to determine if this is a V2 ingredient
215    // pub(crate) fn is_v2(&self) -> bool {
216    //     self.instance_id.is_none()
217    //         || self.data.is_some()
218    //         || self.description.is_some()
219    //         || self.informational_uri.is_some()
220    //         || self.relationship == Relationship::InputTo
221    //         || self.data_types.is_some()
222    // }
223
224    /// Returns a user-displayable title for this ingredient.
225    pub fn title(&self) -> Option<&str> {
226        self.title.as_deref()
227    }
228
229    /// Returns the label for the ingredient if it exists.
230    pub fn label(&self) -> Option<&str> {
231        self.label.as_deref()
232    }
233
234    /// Returns a MIME content_type for this asset associated with this ingredient.
235    pub fn format(&self) -> Option<&str> {
236        self.format.as_deref()
237    }
238
239    /// Returns a document identifier if one exists.
240    pub fn document_id(&self) -> Option<&str> {
241        self.document_id.as_deref()
242    }
243
244    /// Returns the instance identifier.
245    ///
246    /// For v2 ingredients this can return an empty string
247    pub fn instance_id(&self) -> &str {
248        self.instance_id.as_deref().unwrap_or("None") // todo: deprecate and change to Option<&str>
249    }
250
251    /// Returns the provenance URI if available.
252    pub fn provenance(&self) -> Option<&str> {
253        self.provenance.as_deref()
254    }
255
256    /// Returns a ResourceRef or `None`.
257    pub fn thumbnail_ref(&self) -> Option<&ResourceRef> {
258        self.thumbnail.as_ref()
259    }
260
261    /// Returns thumbnail tuple Some((format, bytes)) or None.
262    pub fn thumbnail(&self) -> Option<(&str, Cow<'_, Vec<u8>>)> {
263        self.thumbnail
264            .as_ref()
265            .and_then(|t| Some(t.format.as_str()).zip(self.resources.get(&t.identifier).ok()))
266    }
267
268    /// Returns a Cow of thumbnail bytes or Err(Error::NotFound)`.
269    pub fn thumbnail_bytes(&self) -> Result<Cow<'_, Vec<u8>>> {
270        match self.thumbnail.as_ref() {
271            Some(thumbnail) => self.resources.get(&thumbnail.identifier),
272            None => Err(Error::NotFound),
273        }
274    }
275
276    /// Returns an optional hash to uniquely identify this asset.
277    pub fn hash(&self) -> Option<&str> {
278        self.hash.as_deref()
279    }
280
281    /// Returns `true` if this is labeled as the parent ingredient.
282    pub fn is_parent(&self) -> bool {
283        self.relationship == Relationship::ParentOf
284    }
285
286    /// Returns the relationship status of the ingredient.
287    pub fn relationship(&self) -> &Relationship {
288        &self.relationship
289    }
290
291    /// Returns a reference to the [`ValidationStatus`]s if they exist.
292    pub fn validation_status(&self) -> Option<&[ValidationStatus]> {
293        self.validation_status.as_deref()
294    }
295
296    /// Returns a reference to the [`ValidationResults`]s if they exist.
297    pub fn validation_results(&self) -> Option<&ValidationResults> {
298        self.validation_results.as_ref()
299    }
300
301    /// Returns a reference to [`AssertionMetadata`] if it exists.
302    pub fn metadata(&self) -> Option<&AssertionMetadata> {
303        self.metadata.as_ref()
304    }
305
306    /// Returns the label for the active [`Manifest`] in this ingredient,
307    /// if one exists.
308    ///
309    /// If `None`, the ingredient has no [`Manifest`]s.
310    pub fn active_manifest(&self) -> Option<&str> {
311        self.active_manifest.as_deref()
312    }
313
314    /// Returns a reference to C2PA manifest data if it exists.
315    ///
316    /// manifest_data is the binary form of a manifest store in .c2pa format.
317    pub fn manifest_data_ref(&self) -> Option<&ResourceRef> {
318        self.manifest_data.as_ref()
319    }
320
321    /// Returns a copy on write ref to the manifest data bytes or None`.
322    ///
323    /// manifest_data is the binary form of a manifest store in .c2pa format.
324    pub fn manifest_data(&self) -> Option<Cow<'_, Vec<u8>>> {
325        self.manifest_data
326            .as_ref()
327            .and_then(|r| self.resources.get(&r.identifier).ok())
328    }
329
330    /// Returns a reference to ingredient data if it exists.
331    pub fn data_ref(&self) -> Option<&ResourceRef> {
332        self.data.as_ref()
333    }
334
335    /// Returns a reference to the ocsp responses if it exists.
336    pub(crate) fn ocsp_responses_ref(&self) -> Option<&Vec<ResourceRef>> {
337        self.ocsp_responses.as_ref()
338    }
339
340    /// Returns the detailed description of the ingredient if it exists.
341    pub fn description(&self) -> Option<&str> {
342        self.description.as_deref()
343    }
344
345    /// Returns an informational uri for the ingredient if it exists.
346    pub fn informational_uri(&self) -> Option<&str> {
347        self.informational_uri.as_deref()
348    }
349
350    /// Returns an list AssetType info.
351    pub fn data_types(&self) -> Option<&[AssetType]> {
352        self.data_types.as_deref()
353    }
354
355    /// Sets a human-readable title for this ingredient.
356    pub fn set_title<S: Into<String>>(&mut self, title: S) -> &mut Self {
357        self.title = Some(title.into());
358        self
359    }
360
361    /// Sets the document instanceId.
362    ///
363    /// This call is optional for v2 ingredients.
364    ///
365    /// Typically this is found in XMP under `xmpMM:InstanceID`.
366    pub fn set_instance_id<S: Into<String>>(&mut self, instance_id: S) -> &mut Self {
367        self.instance_id = Some(instance_id.into());
368        self
369    }
370
371    /// Sets the document identifier.
372    ///
373    /// This call is optional.
374    ///
375    /// Typically this is found in XMP under `xmpMM:DocumentID`.
376    pub fn set_document_id<S: Into<String>>(&mut self, document_id: S) -> &mut Self {
377        self.document_id = Some(document_id.into());
378        self
379    }
380
381    /// Sets the provenance URI.
382    ///
383    /// This call is optional.
384    ///
385    /// Typically this is found in XMP under `dcterms:provenance`.
386    pub fn set_provenance<S: Into<String>>(&mut self, provenance: S) -> &mut Self {
387        self.provenance = Some(provenance.into());
388        self
389    }
390
391    /// Identifies this ingredient as the parent.
392    ///
393    /// Only one ingredient should be flagged as a parent.
394    /// Use Manifest.set_parent to ensure this is the only parent ingredient.
395    pub fn set_is_parent(&mut self) -> &mut Self {
396        self.relationship = Relationship::ParentOf;
397        self
398    }
399
400    /// Set the ingredient Relationship status.
401    ///
402    /// Only one ingredient should be set as a parentOf.
403    /// Use Manifest.set_parent to ensure this is the only parent ingredient.
404    pub fn set_relationship(&mut self, relationship: Relationship) -> &mut Self {
405        self.relationship = relationship;
406        self
407    }
408
409    /// Sets the thumbnail from a ResourceRef.
410    pub fn set_thumbnail_ref(&mut self, thumbnail: ResourceRef) -> Result<&mut Self> {
411        self.thumbnail = Some(thumbnail);
412        Ok(self)
413    }
414
415    /// Sets the thumbnail format and image data.
416    pub fn set_thumbnail<S: Into<String>, B: Into<Vec<u8>>>(
417        &mut self,
418        format: S,
419        bytes: B,
420    ) -> Result<&mut Self> {
421        let base_id = self.instance_id().to_string();
422        self.thumbnail = Some(self.resources.add_with(&base_id, &format.into(), bytes)?);
423        Ok(self)
424    }
425
426    /// Sets the thumbnail format and image data only in memory.
427    ///
428    /// This is only used for internally generated thumbnails - when
429    /// reading thumbnails from files, we don't want to write these to file
430    /// So this ensures they stay in memory unless written out.
431    #[deprecated(note = "Please use set_thumbnail instead", since = "0.28.0")]
432    pub fn set_memory_thumbnail<S: Into<String>, B: Into<Vec<u8>>>(
433        &mut self,
434        format: S,
435        bytes: B,
436    ) -> Result<&mut Self> {
437        // Do not write this as a file when reading from files
438        #[cfg(feature = "file_io")]
439        let base_path = self.resources_mut().take_base_path();
440        let base_id = self.instance_id().to_string();
441        self.thumbnail = Some(self.resources.add_with(&base_id, &format.into(), bytes)?);
442        #[cfg(feature = "file_io")]
443        if let Some(path) = base_path {
444            self.resources_mut().set_base_path(path)
445        }
446        Ok(self)
447    }
448
449    /// Sets the hash value generated from the entire asset.
450    pub fn set_hash<S: Into<String>>(&mut self, hash: S) -> &mut Self {
451        self.hash = Some(hash.into());
452        self
453    }
454
455    /// Adds a [ValidationStatus] to this ingredient.
456    pub fn add_validation_status(&mut self, status: ValidationStatus) -> &mut Self {
457        match &mut self.validation_status {
458            None => self.validation_status = Some(vec![status]),
459            Some(validation_status) => validation_status.push(status),
460        }
461        self
462    }
463
464    /// Adds any desired [`AssertionMetadata`] to this ingredient.
465    pub fn set_metadata(&mut self, metadata: AssertionMetadata) -> &mut Self {
466        self.metadata = Some(metadata);
467        self
468    }
469
470    /// Sets the label for the active manifest in the manifest data.
471    pub fn set_active_manifest<S: Into<String>>(&mut self, label: S) -> &mut Self {
472        self.active_manifest = Some(label.into());
473        self
474    }
475
476    /// Sets a reference to Manifest C2PA data.
477    pub fn set_manifest_data_ref(&mut self, data_ref: ResourceRef) -> Result<&mut Self> {
478        self.manifest_data = Some(data_ref);
479        Ok(self)
480    }
481
482    /// Sets the Manifest C2PA data for this ingredient with bytes.
483    pub fn set_manifest_data(&mut self, data: Vec<u8>) -> Result<&mut Self> {
484        let base_id = "manifest_data".to_string();
485        self.manifest_data = Some(
486            self.resources
487                .add_with(&base_id, "application/c2pa", data)?,
488        );
489        Ok(self)
490    }
491
492    /// Sets a reference to Ingredient data.
493    pub fn set_data_ref(&mut self, data_ref: ResourceRef) -> Result<&mut Self> {
494        // verify the resource referenced exists
495        if !self.resources.exists(&data_ref.identifier) {
496            return Err(Error::NotFound);
497        };
498        self.data = Some(data_ref);
499        Ok(self)
500    }
501
502    /// Sets a detailed description for this ingredient.
503    pub fn set_description<S: Into<String>>(&mut self, description: S) -> &mut Self {
504        self.description = Some(description.into());
505        self
506    }
507
508    /// Sets an informational URI if needed.
509    pub fn set_informational_uri<S: Into<String>>(&mut self, uri: S) -> &mut Self {
510        self.informational_uri = Some(uri.into());
511        self
512    }
513
514    /// Add AssetType info for Ingredient.
515    pub fn add_data_type(&mut self, data_type: AssetType) -> &mut Self {
516        if let Some(data_types) = self.data_types.as_mut() {
517            data_types.push(data_type);
518        } else {
519            self.data_types = Some([data_type].to_vec());
520        }
521
522        self
523    }
524
525    /// Return an immutable reference to the ingredient resources.
526    #[doc(hidden)]
527    pub fn resources(&self) -> &ResourceStore {
528        &self.resources
529    }
530
531    /// Return an mutable reference to the ingredient resources.
532    #[doc(hidden)]
533    pub fn resources_mut(&mut self) -> &mut ResourceStore {
534        &mut self.resources
535    }
536
537    /// Gathers filename, extension, and format from a file path.
538    #[cfg(feature = "file_io")]
539    fn get_path_info(path: &std::path::Path) -> (String, String, String) {
540        let title = path
541            .file_name()
542            .map(|name| name.to_string_lossy().into_owned())
543            .unwrap_or_else(|| "".into());
544
545        let extension = path
546            .extension()
547            .map(|e| e.to_string_lossy().into_owned())
548            .unwrap_or_else(|| "".into())
549            .to_lowercase();
550
551        let format = extension_to_mime(&extension)
552            .unwrap_or("application/octet-stream")
553            .to_owned();
554        (title, extension, format)
555    }
556
557    /// Generates an `Ingredient` from a file path, including XMP info
558    /// from the file if available.
559    ///
560    /// This does not read c2pa_data in a file, it only reads XMP.
561    #[cfg(feature = "file_io")]
562    pub fn from_file_info<P: AsRef<Path>>(path: P) -> Self {
563        // get required information from the file path
564        let (title, _, format) = Self::get_path_info(path.as_ref());
565
566        // if we can open the file try to get xmp info
567        match std::fs::File::open(path).map_err(Error::IoError) {
568            Ok(mut file) => Self::from_stream_info(&mut file, &format, &title),
569            Err(_) => Self {
570                title: Some(title),
571                format: Some(format),
572                ..Default::default()
573            },
574        }
575    }
576
577    /// Generates an `Ingredient` from a stream, including XMP info.
578    pub fn from_stream_info<F, S>(stream: &mut dyn CAIRead, format: F, title: S) -> Self
579    where
580        F: Into<String>,
581        S: Into<String>,
582    {
583        let format = format.into();
584
585        // Try to get xmp info, if this fails all XmpInfo fields will be None.
586        let xmp_info = XmpInfo::from_source(stream, &format);
587
588        let id = if let Some(id) = xmp_info.instance_id {
589            id
590        } else {
591            default_instance_id()
592        };
593
594        let mut ingredient = Self::new(title.into(), format, id);
595
596        ingredient.document_id = xmp_info.document_id; // use document id if one exists
597        ingredient.provenance = xmp_info.provenance;
598
599        ingredient
600    }
601
602    // Utility method to set the validation status from store result and log
603    // Also sets the thumbnail from the claim if valid and it exists
604    fn update_validation_status(
605        &mut self,
606        result: Result<Store>,
607        manifest_bytes: Option<Vec<u8>>,
608        validation_log: &StatusTracker,
609    ) -> Result<()> {
610        match result {
611            Ok(store) => {
612                // generate validation results from the store
613                let validation_results = ValidationResults::from_store(&store, validation_log);
614
615                if let Some(claim) = store.provenance_claim() {
616                    // if the parent claim is valid and has a thumbnail, use it
617                    if validation_results
618                        .active_manifest()
619                        .is_some_and(|m| m.failure().is_empty())
620                    {
621                        if let Some(hashed_uri) = claim
622                            .assertions()
623                            .iter()
624                            .find(|hashed_uri| hashed_uri.url().contains(labels::CLAIM_THUMBNAIL))
625                        {
626                            // We found a valid claim thumbnail so just reference it, we don't need to copy it
627                            let thumb_manifest = manifest_label_from_uri(&hashed_uri.url())
628                                .unwrap_or_else(|| claim.label().to_string());
629                            let uri =
630                                jumbf::labels::to_absolute_uri(&thumb_manifest, &hashed_uri.url());
631                            // Try to determine the format from the assertion label in the URL
632                            let format = hashed_uri
633                                .url()
634                                .rsplit_once('.')
635                                .and_then(|(_, ext)| extension_to_mime(ext))
636                                .unwrap_or("image/jpeg"); // default to jpeg??
637                            let mut thumb = crate::resource_store::ResourceRef::new(format, &uri);
638                            // keep track of the alg and hash for reuse
639                            thumb.alg = hashed_uri.alg();
640                            let hash = base64::encode(&hashed_uri.hash());
641                            thumb.hash = Some(hash);
642                            self.set_thumbnail_ref(thumb)?;
643
644                            // add a resource to give clients access, but don't directly reference it.
645                            // this way a client can view the thumbnail without needing to load the manifest
646                            // but the the embedded thumbnail is still the primary reference
647                            let claim_assertion = store.get_claim_assertion_from_uri(&uri)?;
648                            let thumbnail =
649                                EmbeddedData::from_assertion(claim_assertion.assertion())?;
650                            self.resources.add_uri(
651                                &uri,
652                                &thumbnail.content_type,
653                                thumbnail.data,
654                            )?;
655                        }
656                    }
657                    self.active_manifest = Some(claim.label().to_string());
658                }
659
660                if let Some(bytes) = manifest_bytes {
661                    self.set_manifest_data(bytes)?;
662                }
663
664                self.validation_status = validation_results.validation_errors();
665                self.validation_results = Some(validation_results);
666
667                Ok(())
668            }
669            Err(Error::JumbfNotFound)
670            | Err(Error::ProvenanceMissing)
671            | Err(Error::UnsupportedType) => Ok(()), // no claims but valid file
672            Err(Error::BadParam(desc)) if desc == *"unrecognized file type" => Ok(()),
673            Err(Error::RemoteManifestUrl(url)) | Err(Error::RemoteManifestFetch(url)) => {
674                let status =
675                    ValidationStatus::new_failure(validation_status::MANIFEST_INACCESSIBLE)
676                        .set_url(url)
677                        .set_explanation("Remote manifest not fetched".to_string());
678                let mut validation_results = ValidationResults::default();
679                validation_results.add_status(status.clone());
680                self.validation_results = Some(validation_results);
681                self.validation_status = Some(vec![status]);
682                Ok(())
683            }
684            Err(e) => {
685                // we can ignore the error here because it should have a log entry corresponding to it
686                debug!("ingredient {e:?}");
687
688                let mut results = ValidationResults::default();
689                // convert any other error to a validation status
690                let statuses: Vec<ValidationStatus> = validation_log
691                    .logged_items()
692                    .iter()
693                    .filter_map(ValidationStatus::from_log_item)
694                    .collect();
695
696                for status in statuses {
697                    results.add_status(status.clone());
698                }
699                self.validation_status = results.validation_errors();
700                self.validation_results = Some(results);
701                Ok(())
702            }
703        }
704    }
705
706    #[cfg(feature = "file_io")]
707    /// Creates an `Ingredient` from a file path.
708    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
709        Self::from_file_with_options(path.as_ref(), &DefaultOptions { base: None })
710    }
711
712    #[cfg(feature = "file_io")]
713    /// Creates an `Ingredient` from a file path.
714    pub fn from_file_with_folder<P: AsRef<Path>>(path: P, folder: P) -> Result<Self> {
715        Self::from_file_with_options(
716            path.as_ref(),
717            &DefaultOptions {
718                base: Some(PathBuf::from(folder.as_ref())),
719            },
720        )
721    }
722
723    // Internal utility function to get thumbnail from an assertion.
724    fn thumbnail_from_assertion(assertion: &Assertion) -> (&str, &[u8]) {
725        (assertion.content_type(), assertion.data())
726    }
727
728    /// Creates an `Ingredient` from a file path and options.
729    #[cfg(feature = "file_io")]
730    pub fn from_file_with_options<P: AsRef<Path>>(
731        path: P,
732        options: &dyn IngredientOptions,
733    ) -> Result<Self> {
734        // Legacy behavior: explicitly get global settings for backward compatibility
735        let settings = crate::settings::get_thread_local_settings();
736        let context = Context::new().with_settings(settings)?;
737        Self::from_file_impl(path.as_ref(), options, &context)
738    }
739
740    // Internal implementation to avoid code bloat.
741    #[cfg(feature = "file_io")]
742    fn from_file_impl(
743        path: &Path,
744        options: &dyn IngredientOptions,
745        context: &Context,
746    ) -> Result<Self> {
747        #[cfg(feature = "diagnostics")]
748        let _t = crate::utils::time_it::TimeIt::new("Ingredient:from_file_with_options");
749
750        // from the source file we need to get the XMP, JUMBF and generate a thumbnail
751        debug!("ingredient {path:?}");
752
753        // get required information from the file path
754        let mut ingredient = Self::from_file_info(path);
755
756        if !path.exists() {
757            return Err(Error::FileNotFound(ingredient.title.unwrap_or_default()));
758        }
759
760        // configure for writing to folders if that option is set
761        if let Some(folder) = options.base_path().as_ref() {
762            ingredient.with_base_path(folder)?;
763        }
764
765        // if options includes a title, use it
766        if let Some(opt_title) = options.title(path) {
767            ingredient.title = Some(opt_title);
768        }
769
770        // optionally generate a hash so we know if the file has changed
771        ingredient.hash = options.hash(path);
772
773        let mut validation_log = StatusTracker::default();
774
775        // retrieve the manifest bytes from embedded, sidecar or remote and convert to store if found
776        let (result, manifest_bytes) = match Store::load_jumbf_from_path(path, context) {
777            Ok(manifest_bytes) => {
778                (
779                    // generate a store from the buffer and then validate from the asset path
780                    Store::from_jumbf_with_context(&manifest_bytes, &mut validation_log, context)
781                        .and_then(|mut store| {
782                            // verify the store
783                            store
784                                .verify_from_path(path, &mut validation_log, context)
785                                .map(|_| store)
786                        })
787                        .inspect_err(|e| {
788                            // add a log entry for the error so we act like verify
789                            log_item!("asset", "error loading file", "Ingredient::from_file")
790                                .failure_no_throw(&mut validation_log, e);
791                        }),
792                    Some(manifest_bytes),
793                )
794            }
795            Err(err) => (Err(err), None),
796        };
797
798        // set validation status from result and log
799        ingredient.update_validation_status(result, manifest_bytes, &validation_log)?;
800
801        // create a thumbnail if we don't already have a manifest with a thumb we can use
802        if ingredient.thumbnail.is_none() {
803            if let Some((format, image)) = options.thumbnail(path) {
804                ingredient.set_thumbnail(format, image)?;
805            } else {
806                #[cfg(feature = "add_thumbnails")]
807                if let Some(format) = crate::format_from_path(path) {
808                    ingredient.maybe_add_thumbnail(
809                        &format,
810                        &mut std::io::BufReader::new(std::fs::File::open(path)?),
811                        context,
812                    )?;
813                }
814            }
815        }
816        Ok(ingredient)
817    }
818
819    /// Creates an `Ingredient` from a memory buffer.
820    ///
821    /// This does not set title or hash.
822    /// Thumbnail will be set only if one can be retrieved from a previous valid manifest.
823    pub fn from_memory(format: &str, buffer: &[u8]) -> Result<Self> {
824        let mut stream = Cursor::new(buffer);
825        Self::from_stream(format, &mut stream)
826    }
827
828    /// Creates an `Ingredient` from a stream.
829    ///
830    /// This does not set title or hash.
831    /// Thumbnail will be set only if one can be retrieved from a previous valid manifest.
832    pub fn from_stream(format: &str, stream: &mut dyn CAIRead) -> Result<Self> {
833        // Legacy behavior: explicitly get global settings for backward compatibility
834        let settings = crate::settings::get_thread_local_settings();
835        let context = Context::new().with_settings(settings)?;
836        let ingredient = Self::from_stream_info(stream, format, "untitled");
837        stream.rewind()?;
838        ingredient.add_stream_internal(format, stream, &context)
839    }
840
841    /// Create an Ingredient from JSON.
842    pub fn from_json(json: &str) -> Result<Self> {
843        serde_json::from_str(json).map_err(Error::JsonError)
844    }
845
846    /// Adds a stream to an ingredient.
847    ///
848    /// This allows you to predefine fields before adding the stream.
849    /// Sets manifest_data if the stream contains a manifest_store.
850    /// Sets thumbnail if not defined and a valid claim thumbnail is found or add_thumbnails is enabled.
851    /// Instance_id, document_id, and provenance will be overridden if found in the stream.
852    /// Format will be overridden only if it is the default (application/octet-stream).
853    #[async_generic]
854    pub(crate) fn with_stream<S: Into<String>>(
855        mut self,
856        format: S,
857        stream: &mut dyn CAIRead,
858        context: &Context,
859    ) -> Result<Self> {
860        let format = format.into();
861
862        // try to get xmp info, if this fails all XmpInfo fields will be None
863        let xmp_info = XmpInfo::from_source(stream, &format);
864
865        if self.instance_id.is_none() {
866            self.instance_id = xmp_info.instance_id;
867        }
868
869        if let Some(id) = xmp_info.document_id {
870            self.document_id = Some(id);
871        };
872
873        if let Some(provenance) = xmp_info.provenance {
874            self.provenance = Some(provenance);
875        };
876
877        // only override format if it is the default
878        if self.format.is_none() {
879            self.format = Some(format.to_string());
880        };
881
882        // ensure we have an instance Id for v1 ingredients
883        if self.instance_id.is_none() {
884            self.instance_id = Some(default_instance_id());
885        };
886
887        stream.rewind()?;
888
889        if _sync {
890            self.add_stream_internal(&format, stream, context)
891        } else {
892            self.add_stream_internal_async(&format, stream, context)
893                .await
894        }
895    }
896
897    // Internal implementation to avoid code bloat.
898    #[async_generic]
899    fn add_stream_internal(
900        mut self,
901        format: &str,
902        stream: &mut dyn CAIRead,
903        context: &Context,
904    ) -> Result<Self> {
905        let mut validation_log = StatusTracker::default();
906
907        // retrieve the manifest bytes from embedded or remote and convert to store if found
908        let jumbf_result = match self.manifest_data() {
909            Some(data) => Ok(data.into_owned()),
910            None => if _sync {
911                Store::load_jumbf_from_stream(format, stream, context)
912            } else {
913                Store::load_jumbf_from_stream_async(format, stream, context).await
914            }
915            .map(|(manifest_bytes, _)| manifest_bytes),
916        };
917
918        // We can't use functional combinators since we can't use async callbacks (https://github.com/rust-lang/rust/issues/62290)
919        let (mut result, manifest_bytes) = match jumbf_result {
920            Ok(manifest_bytes) => {
921                let result = if _sync {
922                    Store::from_manifest_data_and_stream(
923                        &manifest_bytes,
924                        format,
925                        &mut *stream,
926                        &mut validation_log,
927                        context,
928                    )
929                } else {
930                    Store::from_manifest_data_and_stream_async(
931                        &manifest_bytes,
932                        format,
933                        &mut *stream,
934                        &mut validation_log,
935                        context,
936                    )
937                    .await
938                };
939                (result, Some(manifest_bytes))
940            }
941            Err(err) => (Err(err), None),
942        };
943
944        // Fetch ocsp responses and store it with the ingredient
945        if let Ok(ref mut store) = result {
946            let labels = store.get_manifest_labels_for_ocsp(context.settings());
947
948            let ocsp_response_ders = if _sync {
949                store.get_ocsp_response_ders(labels, &mut validation_log, context)?
950            } else {
951                store
952                    .get_ocsp_response_ders_async(labels, &mut validation_log, context)
953                    .await?
954            };
955
956            let resource_refs: Vec<ResourceRef> = ocsp_response_ders
957                .into_iter()
958                .filter_map(|o| self.resources.add_with(&o.0, "ocsp", o.1).ok())
959                .collect();
960
961            self.ocsp_responses = Some(resource_refs);
962        }
963
964        // set validation status from result and log
965        self.update_validation_status(result, manifest_bytes, &validation_log)?;
966
967        // create a thumbnail if we don't already have a manifest with a thumb we can use
968        #[cfg(feature = "add_thumbnails")]
969        self.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), context)?;
970
971        Ok(self)
972    }
973
974    /// Creates an `Ingredient` from a memory buffer (async version).
975    ///
976    /// This does not set title or hash.
977    /// Thumbnail will be set only if one can be retrieved from a previous valid manifest.
978    pub async fn from_memory_async(format: &str, buffer: &[u8]) -> Result<Self> {
979        let mut stream = Cursor::new(buffer);
980        Self::from_stream_async(format, &mut stream).await
981    }
982
983    /// Creates an `Ingredient` from a stream (async version).
984    ///
985    /// This does not set title or hash.
986    /// Thumbnail will be set only if one can be retrieved from a previous valid manifest.
987    pub async fn from_stream_async(format: &str, stream: &mut dyn CAIRead) -> Result<Self> {
988        // Legacy behavior: explicitly get global settings for backward compatibility
989        let settings = crate::settings::get_thread_local_settings();
990        let context = Context::new().with_settings(settings)?;
991        Self::from_stream_async_with_settings(format, stream, &context).await
992    }
993
994    pub(crate) async fn from_stream_async_with_settings(
995        format: &str,
996        stream: &mut dyn CAIRead,
997        context: &Context,
998    ) -> Result<Self> {
999        let mut ingredient = Self::from_stream_info(stream, format, "untitled");
1000        stream.rewind()?;
1001
1002        let mut validation_log = StatusTracker::default();
1003
1004        // retrieve the manifest bytes from embedded, sidecar or remote and convert to store if found
1005        let (result, manifest_bytes) =
1006            match Store::load_jumbf_from_stream_async(format, stream, context).await {
1007                Ok((manifest_bytes, _)) => {
1008                    (
1009                        // generate a store from the buffer and then validate from the asset path
1010                        match Store::from_jumbf_with_context(
1011                            &manifest_bytes,
1012                            &mut validation_log,
1013                            context,
1014                        ) {
1015                            Ok(store) => {
1016                                // verify the store
1017                                Store::verify_store_async(
1018                                    &store,
1019                                    &mut ClaimAssetData::Stream(stream, format),
1020                                    &mut validation_log,
1021                                    context,
1022                                )
1023                                .await
1024                                .map(|_| store)
1025                            }
1026                            Err(e) => {
1027                                log_item!(
1028                                    "asset",
1029                                    "error loading asset",
1030                                    "Ingredient::from_stream_async"
1031                                )
1032                                .failure_no_throw(&mut validation_log, &e);
1033
1034                                Err(e)
1035                            }
1036                        },
1037                        Some(manifest_bytes),
1038                    )
1039                }
1040                Err(err) => (Err(err), None),
1041            };
1042
1043        // set validation status from result and log
1044        ingredient.update_validation_status(result, manifest_bytes, &validation_log)?;
1045
1046        // create a thumbnail if we don't already have a manifest with a thumb we can use
1047        #[cfg(feature = "add_thumbnails")]
1048        ingredient.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), context)?;
1049
1050        Ok(ingredient)
1051    }
1052
1053    /// Creates an Ingredient from a store and a URI to an ingredient assertion.
1054    /// claim_label identifies the claim for relative paths.
1055    pub(crate) fn from_ingredient_uri(
1056        store: &Store,
1057        claim_label: &str,
1058        ingredient_uri: &str,
1059        #[cfg(feature = "file_io")] resource_path: Option<&Path>,
1060    ) -> Result<Self> {
1061        let assertion =
1062            store
1063                .get_assertion_from_uri(ingredient_uri)
1064                .ok_or(Error::AssertionMissing {
1065                    url: ingredient_uri.to_owned(),
1066                })?;
1067        let ingredient_assertion = assertions::Ingredient::from_assertion(assertion)?;
1068        let mut validation_status = match ingredient_assertion.validation_status.as_ref() {
1069            Some(status) => status.clone(),
1070            None => Vec::new(),
1071        };
1072
1073        // the c2pa_manifest() method will return the active_manifest or c2pa_manifest field
1074        let active_manifest = ingredient_assertion
1075            .c2pa_manifest()
1076            .and_then(|hash_url| manifest_label_from_uri(&hash_url.url()));
1077
1078        debug!(
1079            "Adding Ingredient {:?} {:?}",
1080            ingredient_assertion.title, &active_manifest
1081        );
1082
1083        // keep track of the assertion label for this ingredient.
1084        let label = assertion_label_from_uri(ingredient_uri);
1085        let mut ingredient = Ingredient {
1086            title: ingredient_assertion.title,
1087            format: ingredient_assertion.format,
1088            instance_id: ingredient_assertion.instance_id,
1089            document_id: ingredient_assertion.document_id,
1090            relationship: ingredient_assertion.relationship,
1091            active_manifest,
1092            validation_results: ingredient_assertion.validation_results,
1093            metadata: ingredient_assertion.metadata,
1094            description: ingredient_assertion.description,
1095            informational_uri: ingredient_assertion.informational_uri,
1096            data_types: ingredient_assertion.data_types,
1097            label,
1098            ..Default::default()
1099        };
1100
1101        ingredient.resources.set_label(claim_label); // set the label for relative paths
1102
1103        #[cfg(feature = "file_io")]
1104        if let Some(base_path) = resource_path {
1105            ingredient.resources_mut().set_base_path(base_path)
1106        }
1107
1108        // Find the thumbnail and add as a ResourceRef.
1109        if let Some(hashed_uri) = ingredient_assertion.thumbnail.as_ref() {
1110            // This could be a relative or absolute thumbnail reference to another manifest
1111            let target_claim_label = match manifest_label_from_uri(&hashed_uri.url()) {
1112                Some(label) => label,           // use the manifest from the thumbnail uri
1113                None => claim_label.to_owned(), /* relative so use the whole url from the thumbnail assertion */
1114            };
1115            let maybe_resource_ref = match hashed_uri.url() {
1116                uri if uri.contains(jumbf::labels::ASSERTIONS) => {
1117                    // Get the bits of the thumbnail and convert it to a resource
1118                    // it may be in an assertion or a data box
1119                    store
1120                        .get_assertion_from_uri_and_claim(&hashed_uri.url(), &target_claim_label)
1121                        .map(|assertion| {
1122                            let (format, image) = Self::thumbnail_from_assertion(assertion);
1123                            ingredient
1124                                .resources
1125                                .add_uri(&hashed_uri.url(), format, image)
1126                        })
1127                }
1128                uri if uri.contains(jumbf::labels::DATABOXES) => store
1129                    .get_data_box_from_uri_and_claim(hashed_uri, &target_claim_label)
1130                    .map(|data_box| {
1131                        ingredient.resources.add_uri(
1132                            &hashed_uri.url(),
1133                            &data_box.format,
1134                            data_box.data.clone(),
1135                        )
1136                    }),
1137                _ => None,
1138            };
1139            match maybe_resource_ref {
1140                Some(data_ref) => {
1141                    ingredient.thumbnail = Some(data_ref?);
1142                }
1143                None => {
1144                    error!("failed to get {} from {}", hashed_uri.url(), ingredient_uri);
1145                    validation_status.push(
1146                        ValidationStatus::new_failure(
1147                            validation_status::ASSERTION_MISSING.to_string(),
1148                        )
1149                        .set_url(hashed_uri.url()),
1150                    );
1151                }
1152            }
1153        };
1154
1155        // if the ingredient as a data field, we need to resolve that as well
1156        if let Some(data_uri) = ingredient_assertion.data.as_ref() {
1157            let maybe_data_ref = match data_uri.url() {
1158                uri if uri.contains(jumbf::labels::ASSERTIONS) => {
1159                    // if this is a claim data box, then use the label from the data uri
1160                    store
1161                        .get_assertion_from_uri_and_claim(&uri, claim_label)
1162                        .map(|assertion| {
1163                            let embedded_data = EmbeddedData::from_assertion(assertion)?;
1164                            ingredient.resources.add_uri(
1165                                &data_uri.url(),
1166                                &embedded_data.content_type,
1167                                embedded_data.data,
1168                            )
1169                        })
1170                }
1171                uri if uri.contains(jumbf::labels::DATABOXES) => store
1172                    .get_data_box_from_uri_and_claim(data_uri, claim_label)
1173                    .map(|data_box| {
1174                        ingredient
1175                            .resources
1176                            .add_uri(&uri, &data_box.format, data_box.data.clone())
1177                    }),
1178                _ => None,
1179            };
1180            match maybe_data_ref {
1181                Some(data_ref) => {
1182                    ingredient.data = Some(data_ref?);
1183                }
1184                None => {
1185                    error!("failed to get {} from {}", data_uri.url(), ingredient_uri);
1186                    validation_status.push(
1187                        ValidationStatus::new_failure(
1188                            validation_status::ASSERTION_MISSING.to_string(),
1189                        )
1190                        .set_url(data_uri.url()),
1191                    );
1192                }
1193            }
1194        };
1195
1196        if !validation_status.is_empty() {
1197            ingredient.validation_status = Some(validation_status)
1198        }
1199        Ok(ingredient)
1200    }
1201
1202    /// Converts a higher level Ingredient into the appropriate components in a claim.
1203    pub(crate) fn add_to_claim(
1204        &self,
1205        claim: &mut Claim,
1206        redactions: Option<Vec<String>>,
1207        resources: Option<&ResourceStore>, // use alternate resource store (for Builder model)
1208        context: &Context,
1209    ) -> Result<HashedUri> {
1210        let mut thumbnail = None;
1211        // for Builder model, ingredient resources may be in the manifest
1212        let get_resource = |id: &str| {
1213            self.resources.get(id).or_else(|_| {
1214                resources
1215                    .ok_or_else(|| Error::NotFound)
1216                    .and_then(|r| r.get(id))
1217            })
1218        };
1219
1220        // add the ingredient manifest_data to the claim
1221        // this is how any existing claims are added to the new store
1222        let (active_manifest, claim_signature) = match self.manifest_data_ref() {
1223            Some(resource_ref) => {
1224                // get the c2pa manifest bytes
1225                let manifest_data = get_resource(&resource_ref.identifier)?;
1226
1227                // have Store check and load ingredients and add them to a claim
1228                let ingredient_store =
1229                    Store::load_ingredient_to_claim(claim, &manifest_data, redactions, context)?;
1230
1231                let ingredient_active_claim = ingredient_store
1232                    .provenance_claim()
1233                    .ok_or(Error::JumbfNotFound)?;
1234
1235                let manifest_label = ingredient_active_claim.label();
1236                // get the ingredient map loaded in previous
1237
1238                let hash = ingredient_store
1239                    .get_manifest_box_hashes(ingredient_active_claim)
1240                    .manifest_box_hash; // get C2PA 1.2 JUMBF box
1241                let sig_hash = ingredient_store
1242                    .get_manifest_box_hashes(ingredient_active_claim)
1243                    .signature_box_hash; // needed for v3 ingredients
1244
1245                let uri = jumbf::labels::to_manifest_uri(manifest_label);
1246                let signature_uri = jumbf::labels::to_signature_uri(manifest_label);
1247
1248                // if there are validations and they have all passed, then use the parent claim thumbnail if available
1249                if let Some(validation_results) = self.validation_results() {
1250                    if validation_results.validation_state() != crate::ValidationState::Invalid {
1251                        thumbnail = ingredient_active_claim
1252                            .assertions()
1253                            .iter()
1254                            .find(|hashed_uri| hashed_uri.url().contains(labels::CLAIM_THUMBNAIL))
1255                            .map(|t| {
1256                                // convert ingredient uris to absolute when adding them
1257                                // since this uri references a different manifest
1258                                let url = jumbf::labels::to_absolute_uri(manifest_label, &t.url());
1259                                HashedUri::new(url, t.alg(), &t.hash())
1260                            });
1261                    }
1262                }
1263                // generate c2pa_manifest hashed_uris
1264                (
1265                    Some(crate::hashed_uri::HashedUri::new(
1266                        uri,
1267                        Some(ingredient_active_claim.alg().to_owned()),
1268                        hash.as_ref(),
1269                    )),
1270                    Some(crate::hashed_uri::HashedUri::new(
1271                        signature_uri,
1272                        Some(ingredient_active_claim.alg().to_owned()),
1273                        sig_hash.as_ref(),
1274                    )),
1275                )
1276            }
1277            None => (None, None),
1278        };
1279
1280        // if the ingredient defines a thumbnail, add it to the claim
1281        // otherwise use the parent claim thumbnail if available
1282        if let Some(thumb_ref) = self.thumbnail_ref() {
1283            // if we have a hash, just build the hashed uri
1284            let hash_url = match thumb_ref.hash.as_ref() {
1285                Some(h) => {
1286                    let hash = base64::decode(h)
1287                        .map_err(|_e| Error::BadParam("Invalid hash".to_string()))?;
1288                    HashedUri::new(thumb_ref.identifier.clone(), thumb_ref.alg.clone(), &hash)
1289                }
1290                None => {
1291                    // get the resource data and add it to the claim
1292                    let data = get_resource(&thumb_ref.identifier)?;
1293                    if claim.version() < 2 {
1294                        claim.add_databox(
1295                            &thumb_ref.format,
1296                            data.into_owned(),
1297                            thumb_ref.data_types.clone(),
1298                        )?
1299                    } else {
1300                        // add EmbeddedData thumbnail for v3 assertions in v2 claims
1301                        let thumbnail = EmbeddedData::new(
1302                            labels::INGREDIENT_THUMBNAIL,
1303                            format_to_mime(&thumb_ref.format),
1304                            data.into_owned(),
1305                        );
1306                        claim.add_assertion(&thumbnail)?
1307                    }
1308                }
1309            };
1310            thumbnail = Some(hash_url);
1311        }
1312
1313        // if the ingredient has a data field, resolve and add it to the claim
1314        let mut data = None;
1315        if let Some(data_ref) = self.data_ref() {
1316            let box_data = get_resource(&data_ref.identifier)?;
1317            let hash_uri = match claim.version() {
1318                1 => claim.add_databox(
1319                    &data_ref.format,
1320                    box_data.into_owned(),
1321                    data_ref.data_types.clone(),
1322                )?,
1323                _ => {
1324                    let embedded_data = EmbeddedData::new(
1325                        labels::EMBEDDED_DATA,
1326                        format_to_mime(&data_ref.format),
1327                        box_data.into_owned(),
1328                    );
1329                    claim.add_assertion(&embedded_data)?
1330                }
1331            };
1332
1333            data = Some(hash_uri);
1334        };
1335
1336        // if the ingredient has ocsp responses, resolve and add it to the claim as a certificate status assertion
1337        if let Some(ocsp_responses_ref) = self.ocsp_responses_ref() {
1338            let ocsp_responses: Vec<Vec<u8>> = ocsp_responses_ref
1339                .iter()
1340                .filter_map(|i| get_resource(&i.identifier).ok())
1341                .map(|cow| cow.into_owned())
1342                .collect();
1343            if !ocsp_responses.is_empty() {
1344                let certificate_status =
1345                    if let Some(assertion) = claim.get_assertion(CertificateStatus::LABEL, 0) {
1346                        let certificate_status = CertificateStatus::from_assertion(assertion)?;
1347                        certificate_status.add_ocsp_vals(ocsp_responses)
1348                    } else {
1349                        CertificateStatus::new(ocsp_responses)
1350                    };
1351                claim.add_assertion(&certificate_status)?;
1352            }
1353        }
1354
1355        let mut ingredient_assertion = match claim.version() {
1356            1 => {
1357                // don't make v1 ingredients anymore, they will always be at least v2
1358                assertions::Ingredient::new_v2(
1359                    self.title().unwrap_or_default(),
1360                    self.format().unwrap_or_default(),
1361                )
1362            }
1363            2 => {
1364                let mut assertion = assertions::Ingredient::new_v3(self.relationship.clone());
1365                assertion.title = self.title.clone();
1366                assertion.format = self.format.clone();
1367                assertion
1368            }
1369            _ => return Err(Error::ClaimVersion),
1370        };
1371        ingredient_assertion.instance_id = self.instance_id.clone();
1372        match claim.version() {
1373            1 => {
1374                ingredient_assertion.document_id = self.document_id.clone();
1375                ingredient_assertion.c2pa_manifest = active_manifest;
1376                ingredient_assertion
1377                    .validation_status
1378                    .clone_from(&self.validation_status);
1379            }
1380            2 => {
1381                ingredient_assertion.active_manifest = active_manifest;
1382                ingredient_assertion.claim_signature = claim_signature;
1383                ingredient_assertion.validation_results = self.validation_results.clone();
1384            }
1385            _ => {}
1386        }
1387        ingredient_assertion.relationship = self.relationship.clone();
1388        ingredient_assertion.thumbnail = thumbnail;
1389        ingredient_assertion.metadata.clone_from(&self.metadata);
1390        ingredient_assertion.data = data;
1391        ingredient_assertion
1392            .description
1393            .clone_from(&self.description);
1394        ingredient_assertion
1395            .informational_uri
1396            .clone_from(&self.informational_uri);
1397        ingredient_assertion.data_types.clone_from(&self.data_types);
1398        claim.add_assertion(&ingredient_assertion)
1399    }
1400
1401    /// Setting a base path will make the ingredient use resource files instead of memory buffers.
1402    ///
1403    /// The files will be relative to the given base path.
1404    #[cfg(feature = "file_io")]
1405    pub fn with_base_path<P: AsRef<Path>>(&mut self, base_path: P) -> Result<&Self> {
1406        std::fs::create_dir_all(&base_path)?;
1407        self.resources.set_base_path(base_path.as_ref());
1408        Ok(self)
1409    }
1410
1411    /// Asynchronously create an Ingredient from a binary manifest (.c2pa) and asset bytes.
1412    ///
1413    /// # Example: Create an Ingredient from a binary manifest (.c2pa) and asset bytes
1414    /// ```
1415    /// use c2pa::{Result, Ingredient};
1416    ///
1417    /// # fn main() -> Result<()> {
1418    /// #    async {
1419    ///         let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
1420    ///         let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa");
1421    ///
1422    ///         let ingredient = Ingredient::from_manifest_and_asset_bytes_async(manifest_bytes.to_vec(), "image/jpeg", asset_bytes)
1423    ///             .await
1424    ///             .unwrap();
1425    ///
1426    ///         println!("{}", ingredient);
1427    /// #    };
1428    /// #
1429    /// #    Ok(())
1430    /// }
1431    /// ```
1432    pub async fn from_manifest_and_asset_bytes_async<M: Into<Vec<u8>>>(
1433        manifest_bytes: M,
1434        format: &str,
1435        asset_bytes: &[u8],
1436    ) -> Result<Self> {
1437        let mut stream = Cursor::new(asset_bytes);
1438        Self::from_manifest_and_asset_stream_async(manifest_bytes, format, &mut stream).await
1439    }
1440
1441    /// Asynchronously create an Ingredient from a binary manifest (.c2pa) and asset.
1442    pub async fn from_manifest_and_asset_stream_async<M: Into<Vec<u8>>>(
1443        manifest_bytes: M,
1444        format: &str,
1445        stream: &mut dyn CAIRead,
1446    ) -> Result<Self> {
1447        // Legacy behavior: explicitly get global settings for backward compatibility
1448        let settings = crate::settings::get_thread_local_settings();
1449        let context = Context::new().with_settings(settings)?;
1450        let mut ingredient = Self::from_stream_info(stream, format, "untitled");
1451
1452        let mut validation_log = StatusTracker::default();
1453
1454        let manifest_bytes: Vec<u8> = manifest_bytes.into();
1455        // generate a store from the buffer and then validate from the asset path
1456        let result =
1457            match Store::from_jumbf_with_context(&manifest_bytes, &mut validation_log, &context) {
1458                Ok(store) => {
1459                    // verify the store
1460                    stream.rewind()?;
1461
1462                    Store::verify_store_async(
1463                        &store,
1464                        &mut ClaimAssetData::Stream(stream, format),
1465                        &mut validation_log,
1466                        &context,
1467                    )
1468                    .await
1469                    .map(|_| store)
1470                }
1471                Err(e) => {
1472                    // add a log entry for the error so we act like verify
1473                    log_item!("asset", "error loading file", "Ingredient::from_file")
1474                        .failure_no_throw(&mut validation_log, &e);
1475
1476                    Err(e)
1477                }
1478            };
1479
1480        // set validation status from result and log
1481        ingredient.update_validation_status(result, Some(manifest_bytes), &validation_log)?;
1482
1483        // create a thumbnail if we don't already have a manifest with a thumb we can use
1484        #[cfg(feature = "add_thumbnails")]
1485        ingredient.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), &context)?;
1486
1487        Ok(ingredient)
1488    }
1489
1490    /// Automatically generate a thumbnail for the ingredient if missing and enabled in settings.
1491    ///
1492    /// This function takes into account the [Settings][crate::settings::Settings]:
1493    /// * `builder.thumbnail.enabled`
1494    #[cfg(feature = "add_thumbnails")]
1495    pub(crate) fn maybe_add_thumbnail<R>(
1496        &mut self,
1497        format: &str,
1498        stream: &mut R,
1499        context: &Context,
1500    ) -> Result<()>
1501    where
1502        R: std::io::BufRead + std::io::Seek,
1503    {
1504        let settings = context.settings();
1505        let auto_thumbnail = settings.builder.thumbnail.enabled;
1506
1507        if self.thumbnail.is_none() && auto_thumbnail {
1508            stream.rewind()?;
1509
1510            if let Some((output_format, image)) =
1511                crate::utils::thumbnail::make_thumbnail_bytes_from_stream(format, stream, settings)?
1512            {
1513                self.set_thumbnail(output_format.to_string(), image)?;
1514            }
1515        }
1516
1517        Ok(())
1518    }
1519
1520    // allows overriding fields in an ingredient with another ingredient
1521    pub(crate) fn merge(&mut self, other: &Ingredient) {
1522        // println!("before merge: {}", self);
1523        self.relationship = other.relationship.clone();
1524
1525        if let Some(title) = &other.title {
1526            self.title = Some(title.clone());
1527        }
1528        if let Some(format) = &other.format {
1529            self.format = Some(format.clone());
1530        }
1531        if let Some(instance_id) = &other.instance_id {
1532            self.instance_id = Some(instance_id.clone());
1533        }
1534        if let Some(provenance) = &other.provenance {
1535            self.provenance = Some(provenance.clone());
1536        }
1537        if let Some(hash) = &other.hash {
1538            self.hash = Some(hash.clone());
1539        }
1540        if let Some(document_id) = &other.document_id {
1541            self.document_id = Some(document_id.clone());
1542        }
1543        if let Some(description) = &other.description {
1544            self.description = Some(description.clone());
1545        }
1546        if let Some(informational_uri) = &other.informational_uri {
1547            self.informational_uri = Some(informational_uri.clone());
1548        }
1549        if let Some(data) = &other.data {
1550            self.data = Some(data.clone());
1551        }
1552        if let Some(thumbnail) = &other.thumbnail {
1553            self.thumbnail = Some(thumbnail.clone());
1554        }
1555        if let Some(metadata) = &other.metadata {
1556            self.metadata = Some(metadata.clone());
1557        }
1558        if let Some(label) = &other.label {
1559            self.label = Some(label.clone());
1560        }
1561        //println!("after merge: {}", self);
1562    }
1563}
1564
1565impl std::fmt::Display for Ingredient {
1566    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1567        let report = serde_json::to_string_pretty(self).unwrap_or_default();
1568        f.write_str(&report)
1569    }
1570}
1571
1572/// This defines optional operations when creating [`Ingredient`] structs from files.
1573#[cfg(feature = "file_io")]
1574pub trait IngredientOptions {
1575    /// This allows setting the title for the ingredient.
1576    ///
1577    /// If it returns `None`, then the default behavior is to use the file's name.
1578    fn title(&self, _path: &Path) -> Option<String> {
1579        None
1580    }
1581
1582    /// Returns an optional hash value for the ingredient.
1583    ///
1584    /// Use the hash value to test for duplicate ingredients or if a source file has changed.
1585    /// If hash is_some() Manifest.add_ingredient will dedup matching hashes
1586    fn hash(&self, _path: &Path) -> Option<String> {
1587        None
1588    }
1589
1590    /// Returns an optional thumbnail image representing the asset.
1591    ///
1592    /// The first value is the content type of the thumbnail, for example `image/jpeg`.
1593    /// The second value is bytes of the thumbnail image.
1594    /// The default is no thumbnail, so you must provide an override to have a thumbnail image.
1595    fn thumbnail(&self, _path: &Path) -> Option<(String, Vec<u8>)> {
1596        None
1597    }
1598
1599    /// Returns an optional folder path.
1600    ///
1601    /// If Some, binary data will be stored in files in the given folder.
1602    fn base_path(&self) -> Option<&Path> {
1603        None
1604    }
1605}
1606
1607/// DefaultOptions returns None for Title and Hash and generates thumbnail for supported thumbnails.
1608///
1609/// This can be use with `Ingredient::from_file_with_options`.
1610#[cfg(feature = "file_io")]
1611pub struct DefaultOptions {
1612    /// If Some, the ingredient will read/write binary assets using this folder.
1613    ///
1614    /// If None, the assets will be kept in memory.
1615    pub base: Option<std::path::PathBuf>,
1616}
1617
1618#[cfg(feature = "file_io")]
1619impl IngredientOptions for DefaultOptions {
1620    fn base_path(&self) -> Option<&Path> {
1621        self.base.as_deref()
1622    }
1623}
1624
1625#[cfg(test)]
1626mod tests {
1627    #![allow(clippy::expect_used)]
1628    #![allow(clippy::unwrap_used)]
1629
1630    use c2pa_macros::c2pa_test_async;
1631    #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1632    use wasm_bindgen_test::*;
1633
1634    use super::*;
1635    #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1636    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1637
1638    #[test]
1639    #[cfg_attr(
1640        all(target_arch = "wasm32", not(target_os = "wasi")),
1641        wasm_bindgen_test
1642    )]
1643    fn test_ingredient_api() {
1644        let mut ingredient = Ingredient::new("title", "format", "instance_id");
1645        ingredient
1646            .resources_mut()
1647            .add("id", "data".as_bytes().to_vec())
1648            .expect("add");
1649        ingredient
1650            .set_document_id("document_id")
1651            .set_title("title2")
1652            .set_hash("hash")
1653            .set_provenance("provenance")
1654            .set_is_parent()
1655            .set_relationship(Relationship::ParentOf)
1656            .set_metadata(AssertionMetadata::new())
1657            .set_thumbnail("format", "thumbnail".as_bytes().to_vec())
1658            .unwrap()
1659            .set_active_manifest("active_manifest")
1660            .set_manifest_data("data".as_bytes().to_vec())
1661            .expect("set_manifest")
1662            .set_description("description")
1663            .set_informational_uri("uri")
1664            .set_data_ref(ResourceRef::new("format", "id"))
1665            .expect("set_data_ref")
1666            .add_validation_status(ValidationStatus::new("status_code"));
1667        assert_eq!(ingredient.title(), Some("title2"));
1668        assert_eq!(ingredient.format(), Some("format"));
1669        assert_eq!(ingredient.instance_id(), "instance_id");
1670        assert_eq!(ingredient.document_id(), Some("document_id"));
1671        assert_eq!(ingredient.provenance(), Some("provenance"));
1672        assert_eq!(ingredient.hash(), Some("hash"));
1673        assert!(ingredient.is_parent());
1674        assert_eq!(ingredient.relationship(), &Relationship::ParentOf);
1675        assert_eq!(ingredient.description(), Some("description"));
1676        assert_eq!(ingredient.informational_uri(), Some("uri"));
1677        assert_eq!(ingredient.data_ref().unwrap().format, "format");
1678        assert_eq!(ingredient.data_ref().unwrap().identifier, "id");
1679        assert!(ingredient.metadata().is_some());
1680        assert_eq!(ingredient.thumbnail().unwrap().0, "format");
1681        assert_eq!(
1682            *ingredient.thumbnail().unwrap().1,
1683            "thumbnail".as_bytes().to_vec()
1684        );
1685        assert_eq!(
1686            *ingredient.thumbnail_bytes().unwrap(),
1687            "thumbnail".as_bytes().to_vec()
1688        );
1689        assert_eq!(ingredient.active_manifest(), Some("active_manifest"));
1690
1691        assert_eq!(
1692            ingredient.validation_status().unwrap()[0].code(),
1693            "status_code"
1694        );
1695    }
1696
1697    #[c2pa_test_async]
1698    async fn test_stream_async_jpg() {
1699        let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
1700        let title = "Test Image";
1701        let format = "image/jpeg";
1702        let mut ingredient = Ingredient::from_memory_async(format, image_bytes)
1703            .await
1704            .expect("from_memory");
1705        ingredient.set_title(title);
1706
1707        println!("ingredient = {ingredient}");
1708        assert_eq!(ingredient.title(), Some(title));
1709        assert_eq!(ingredient.format(), Some(format));
1710        assert!(ingredient.manifest_data().is_some());
1711        assert_eq!(ingredient.metadata(), None);
1712        #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1713        web_sys::console::debug_2(
1714            &"ingredient_from_memory_async:".into(),
1715            &ingredient.to_string().into(),
1716        );
1717        assert_eq!(ingredient.validation_status(), None);
1718    }
1719
1720    #[test]
1721    fn test_stream_jpg() {
1722        let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
1723        let title = "Test Image";
1724        let format = "image/jpeg";
1725        let mut ingredient = Ingredient::from_memory(format, image_bytes).expect("from_memory");
1726        ingredient.set_title(title);
1727
1728        println!("ingredient = {ingredient}");
1729        assert_eq!(ingredient.title(), Some(title));
1730        assert_eq!(ingredient.format(), Some(format));
1731        assert!(ingredient.manifest_data().is_some());
1732        assert_eq!(ingredient.metadata(), None);
1733        assert_eq!(ingredient.validation_status(), None);
1734    }
1735
1736    #[cfg(feature = "add_thumbnails")]
1737    #[test]
1738    fn test_stream_thumbnail() {
1739        use crate::settings::Settings;
1740
1741        #[cfg(target_os = "wasi")]
1742        Settings::reset().unwrap();
1743
1744        Settings::from_toml(
1745            &toml::toml! {
1746                [builder.thumbnail]
1747                enabled = true
1748            }
1749            .to_string(),
1750        )
1751        .unwrap();
1752
1753        let image_bytes = include_bytes!("../tests/fixtures/sample1.png");
1754        let ingredient = Ingredient::from_memory("image/png", image_bytes).unwrap();
1755        assert!(ingredient.thumbnail().is_some());
1756
1757        Settings::from_toml(
1758            &toml::toml! {
1759                [builder.thumbnail]
1760                enabled = false
1761            }
1762            .to_string(),
1763        )
1764        .unwrap();
1765
1766        let ingredient = Ingredient::from_memory("image/png", image_bytes).unwrap();
1767        assert!(ingredient.thumbnail().is_none());
1768        #[cfg(target_os = "wasi")]
1769        Settings::reset().unwrap();
1770    }
1771
1772    #[c2pa_test_async]
1773    async fn test_stream_ogp() {
1774        let image_bytes = include_bytes!("../tests/fixtures/XCA.jpg");
1775        let title = "XCA.jpg";
1776        let format = "image/jpeg";
1777        let mut ingredient = Ingredient::from_memory_async(format, image_bytes)
1778            .await
1779            .expect("from_memory");
1780        ingredient.set_title(title);
1781
1782        println!("ingredient = {ingredient}");
1783        assert_eq!(ingredient.title(), Some(title));
1784        assert_eq!(ingredient.format(), Some(format));
1785        #[cfg(feature = "add_thumbnails")]
1786        assert!(ingredient.thumbnail().is_some());
1787        assert!(ingredient.manifest_data().is_some());
1788        assert_eq!(ingredient.metadata(), None);
1789        assert!(ingredient.validation_status().is_some());
1790        assert_eq!(
1791            ingredient.validation_status().unwrap()[0].code(),
1792            validation_status::ASSERTION_DATAHASH_MISMATCH
1793        );
1794    }
1795
1796    #[cfg(feature = "fetch_remote_manifests")]
1797    #[c2pa_test_async]
1798    async fn test_jpg_cloud_from_memory() {
1799        crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
1800        crate::settings::set_settings_value("verify.remote_manifest_fetch", true).unwrap();
1801
1802        let image_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
1803        let format = "image/jpeg";
1804
1805        let ingredient = Ingredient::from_memory_async(format, image_bytes)
1806            .await
1807            .expect("from_memory_async");
1808
1809        // println!("ingredient = {ingredient}");
1810        assert_eq!(ingredient.title(), Some("untitled"));
1811        assert_eq!(ingredient.format(), Some(format));
1812        assert!(ingredient.provenance().is_some());
1813        assert!(ingredient.provenance().unwrap().starts_with("https:"));
1814        assert!(ingredient.manifest_data().is_some());
1815        assert_eq!(ingredient.validation_status(), None);
1816    }
1817
1818    #[cfg(not(any(feature = "fetch_remote_manifests", feature = "file_io")))]
1819    #[c2pa_test_async]
1820    async fn test_jpg_cloud_from_memory_no_file_io() {
1821        crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
1822        crate::settings::set_settings_value("verify.remote_manifest_fetch", true).unwrap();
1823
1824        let image_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
1825        let format = "image/jpeg";
1826
1827        let ingredient = Ingredient::from_memory_async(format, image_bytes)
1828            .await
1829            .expect("from_memory_async");
1830
1831        assert!(ingredient.validation_status().is_some());
1832        assert_eq!(
1833            ingredient.validation_status().unwrap()[0].code(),
1834            validation_status::MANIFEST_INACCESSIBLE
1835        );
1836        assert!(ingredient.validation_status().unwrap()[0]
1837            .url()
1838            .unwrap()
1839            .starts_with("http"));
1840        assert_eq!(ingredient.manifest_data(), None);
1841    }
1842
1843    #[c2pa_test_async]
1844    async fn test_jpg_cloud_from_memory_and_manifest() {
1845        crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
1846
1847        let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
1848        let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa");
1849        let format = "image/jpeg";
1850        let ingredient = Ingredient::from_manifest_and_asset_bytes_async(
1851            manifest_bytes.to_vec(),
1852            format,
1853            asset_bytes,
1854        )
1855        .await
1856        .unwrap();
1857        #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1858        web_sys::console::debug_2(
1859            &"ingredient_from_memory_async:".into(),
1860            &ingredient.to_string().into(),
1861        );
1862        assert_eq!(ingredient.validation_status(), None);
1863        assert!(ingredient.manifest_data().is_some());
1864        assert!(ingredient.provenance().is_some());
1865    }
1866}
1867
1868#[cfg(test)]
1869#[cfg(feature = "file_io")]
1870mod tests_file_io {
1871    #![allow(clippy::expect_used)]
1872    #![allow(clippy::unwrap_used)]
1873
1874    use super::*;
1875    use crate::{assertion::AssertionData, utils::test::fixture_path};
1876
1877    const NO_MANIFEST_JPEG: &str = "earth_apollo17.jpg";
1878    const MANIFEST_JPEG: &str = "C.jpg";
1879    const BAD_SIGNATURE_JPEG: &str = "E-sig-CA.jpg";
1880
1881    fn stats(ingredient: &Ingredient) -> usize {
1882        let thumb_size = ingredient.thumbnail_bytes().map_or(0, |i| i.len());
1883        let manifest_data_size = ingredient.manifest_data().map_or(0, |r| r.len());
1884
1885        println!(
1886            "  {} instance_id: {}, thumb size: {}, manifest_data size: {}",
1887            ingredient.title().unwrap_or_default(),
1888            ingredient.instance_id(),
1889            thumb_size,
1890            manifest_data_size,
1891        );
1892        ingredient.title().unwrap_or_default().len()
1893            + ingredient.instance_id().len()
1894            + thumb_size
1895            + manifest_data_size
1896    }
1897
1898    // check for correct thumbnail generation with or without add_thumbnails feature
1899    fn test_thumbnail(ingredient: &Ingredient, format: &str) {
1900        if cfg!(feature = "add_thumbnails") {
1901            assert!(ingredient.thumbnail().is_some());
1902            assert_eq!(ingredient.thumbnail().unwrap().0, format);
1903        } else {
1904            assert_eq!(ingredient.thumbnail(), None);
1905        }
1906    }
1907
1908    #[test]
1909    #[cfg(feature = "file_io")]
1910    fn test_psd() {
1911        // std::env::set_var("RUST_LOG", "debug");
1912        // env_logger::init();
1913        let ap = fixture_path("Purple Square.psd");
1914        let ingredient = Ingredient::from_file(ap).expect("from_file");
1915        stats(&ingredient);
1916
1917        println!("ingredient = {ingredient}");
1918        assert_eq!(ingredient.title(), Some("Purple Square.psd"));
1919        assert_eq!(ingredient.format(), Some("image/vnd.adobe.photoshop"));
1920        assert!(ingredient.thumbnail().is_none()); // should always be none
1921        assert!(ingredient.manifest_data().is_none());
1922    }
1923
1924    #[test]
1925    #[cfg(feature = "file_io")]
1926    fn test_manifest_jpg() {
1927        let ap = fixture_path(MANIFEST_JPEG);
1928        let ingredient = Ingredient::from_file(ap).expect("from_file");
1929        stats(&ingredient);
1930
1931        println!("ingredient = {ingredient}");
1932        assert_eq!(ingredient.title(), Some(MANIFEST_JPEG));
1933        assert_eq!(ingredient.format(), Some("image/jpeg"));
1934        assert!(ingredient.thumbnail_ref().is_some()); // we don't generate this thumbnail
1935        assert!(ingredient
1936            .thumbnail_ref()
1937            .unwrap()
1938            .identifier
1939            .starts_with("self#jumbf="));
1940        assert!(ingredient.manifest_data().is_some());
1941        assert_eq!(ingredient.metadata(), None);
1942    }
1943
1944    #[test]
1945    #[cfg(feature = "file_io")]
1946    fn test_no_manifest_jpg() {
1947        let ap = fixture_path(NO_MANIFEST_JPEG);
1948        let ingredient = Ingredient::from_file(ap).expect("from_file");
1949        stats(&ingredient);
1950
1951        println!("ingredient = {ingredient}");
1952        assert_eq!(ingredient.title(), Some(NO_MANIFEST_JPEG));
1953        assert_eq!(ingredient.format(), Some("image/jpeg"));
1954        test_thumbnail(&ingredient, "image/jpeg");
1955        assert_eq!(ingredient.provenance(), None);
1956        assert_eq!(ingredient.manifest_data(), None);
1957        assert_eq!(ingredient.metadata(), None);
1958        assert!(ingredient.instance_id().starts_with("xmp.iid:"));
1959        #[cfg(feature = "add_thumbnails")]
1960        assert!(ingredient
1961            .thumbnail_ref()
1962            .unwrap()
1963            .identifier
1964            .starts_with("xmp.iid"));
1965    }
1966
1967    #[test]
1968    #[cfg(feature = "file_io")]
1969    fn test_jpg_options() {
1970        struct MyOptions {}
1971        impl IngredientOptions for MyOptions {
1972            fn title(&self, _path: &Path) -> Option<String> {
1973                Some("MyTitle".to_string())
1974            }
1975
1976            fn hash(&self, _path: &Path) -> Option<String> {
1977                Some("1234568abcdef".to_string())
1978            }
1979
1980            fn thumbnail(&self, _path: &Path) -> Option<(String, Vec<u8>)> {
1981                Some(("image/foo".to_string(), "bits".as_bytes().to_owned()))
1982            }
1983        }
1984
1985        let ap = fixture_path(NO_MANIFEST_JPEG);
1986        let ingredient = Ingredient::from_file_with_options(ap, &MyOptions {}).expect("from_file");
1987        stats(&ingredient);
1988
1989        assert_eq!(ingredient.title(), Some("MyTitle"));
1990        assert_eq!(ingredient.format(), Some("image/jpeg"));
1991        assert_eq!(ingredient.hash(), Some("1234568abcdef"));
1992        assert_eq!(ingredient.thumbnail_ref().unwrap().format, "image/foo"); // always generated
1993        assert_eq!(ingredient.manifest_data(), None);
1994        assert_eq!(ingredient.metadata(), None);
1995    }
1996
1997    #[test]
1998    #[cfg(feature = "file_io")]
1999    fn test_png_no_claim() {
2000        let ap = fixture_path("libpng-test.png");
2001        let ingredient = Ingredient::from_file(ap).expect("from_file");
2002        stats(&ingredient);
2003
2004        println!("ingredient = {ingredient}");
2005        assert_eq!(ingredient.title(), Some("libpng-test.png"));
2006        test_thumbnail(&ingredient, "image/png");
2007        assert_eq!(ingredient.provenance(), None);
2008        assert_eq!(ingredient.manifest_data, None);
2009    }
2010
2011    #[test]
2012    #[cfg(feature = "file_io")]
2013    fn test_jpg_bad_signature() {
2014        let ap = fixture_path(BAD_SIGNATURE_JPEG);
2015        let ingredient = Ingredient::from_file(ap).expect("from_file");
2016        stats(&ingredient);
2017
2018        println!("ingredient = {ingredient}");
2019        assert_eq!(ingredient.title(), Some(BAD_SIGNATURE_JPEG));
2020        assert_eq!(ingredient.format(), Some("image/jpeg"));
2021        test_thumbnail(&ingredient, "image/jpeg");
2022        assert!(ingredient.manifest_data().is_some());
2023        assert!(
2024            ingredient
2025                .validation_results()
2026                .unwrap()
2027                .active_manifest()
2028                .unwrap()
2029                .informational
2030                .iter()
2031                .any(|info| info.code() == validation_status::TIMESTAMP_MISMATCH),
2032            "No informational item with TIMESTAMP_MISMATCH found"
2033        );
2034    }
2035
2036    #[test]
2037    #[cfg(all(feature = "file_io", feature = "add_thumbnails"))]
2038    fn test_jpg_prerelease() {
2039        const PRERELEASE_JPEG: &str = "prerelease.jpg";
2040        let ap = fixture_path(PRERELEASE_JPEG);
2041        let ingredient = Ingredient::from_file(ap).expect("from_file");
2042        stats(&ingredient);
2043
2044        println!("ingredient = {ingredient}");
2045        assert_eq!(ingredient.title(), Some(PRERELEASE_JPEG));
2046        assert_eq!(ingredient.format(), Some("image/jpeg"));
2047        test_thumbnail(&ingredient, "image/jpeg");
2048        assert!(ingredient.provenance().is_some());
2049        assert_eq!(ingredient.manifest_data(), None);
2050        assert!(ingredient.validation_status().is_some());
2051        assert_eq!(
2052            ingredient.validation_status().unwrap()[0].code(),
2053            validation_status::STATUS_PRERELEASE
2054        );
2055    }
2056
2057    #[test]
2058    #[cfg(feature = "file_io")]
2059    fn test_jpg_nested_err() {
2060        let ap = fixture_path("CIE-sig-CA.jpg");
2061        let ingredient = Ingredient::from_file(ap).expect("from_file");
2062        // println!("ingredient = {ingredient}");
2063        assert_eq!(ingredient.validation_status(), None);
2064        assert!(ingredient.manifest_data().is_some());
2065    }
2066
2067    #[test]
2068    #[cfg(feature = "fetch_remote_manifests")]
2069    fn test_jpg_cloud_failure() {
2070        let ap = fixture_path("cloudx.jpg");
2071        let ingredient = Ingredient::from_file(ap).expect("from_file");
2072        println!("ingredient = {ingredient}");
2073        assert!(ingredient.validation_status().is_some());
2074        assert_eq!(
2075            ingredient.validation_status().unwrap()[0].code(),
2076            validation_status::MANIFEST_INACCESSIBLE
2077        );
2078    }
2079
2080    #[test]
2081    #[cfg(feature = "file_io")]
2082    fn test_jpg_with_path() {
2083        use crate::utils::io_utils::tempdirectory;
2084
2085        let ap = fixture_path("CA.jpg");
2086        let temp_dir = tempdirectory().expect("Failed to create temp directory");
2087        let folder = temp_dir.path().join("ingredient");
2088        std::fs::create_dir_all(&folder).expect("Failed to create subdirectory");
2089
2090        let ingredient = Ingredient::from_file_with_folder(ap, folder).expect("from_file");
2091        println!("ingredient = {ingredient}");
2092        assert_eq!(ingredient.validation_status(), None);
2093
2094        // verify ingredient thumbnail is an absolute url reference to a claim thumbnail
2095        assert!(ingredient
2096            .thumbnail_ref()
2097            .unwrap()
2098            .identifier
2099            .contains(labels::JPEG_CLAIM_THUMBNAIL));
2100
2101        // verify manifest_data exists
2102        assert!(ingredient.manifest_data_ref().is_some());
2103        assert_eq!(ingredient.thumbnail_ref().unwrap().format, "image/jpeg");
2104        assert!(ingredient
2105            .thumbnail_ref()
2106            .unwrap()
2107            .identifier
2108            .starts_with("self#jumbf="));
2109    }
2110
2111    #[test]
2112    #[cfg(feature = "file_io")]
2113    fn test_file_based_ingredient() {
2114        let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2115        folder.push("tests/fixtures");
2116        let mut ingredient = Ingredient::new("title", "format", "instance_id");
2117        ingredient.resources.set_base_path(folder);
2118
2119        assert_eq!(ingredient.thumbnail_ref(), None);
2120        // assert!(ingredient
2121        //     .set_manifest_data_ref(ResourceRef::new("image/jpeg", "foo"))
2122        //     .is_err());
2123        assert_eq!(ingredient.manifest_data_ref(), None);
2124        // verify we can set a reference
2125        assert!(ingredient
2126            .set_thumbnail_ref(ResourceRef::new("image/jpeg", "C.jpg"))
2127            .is_ok());
2128        assert!(ingredient.thumbnail_ref().is_some());
2129        assert!(ingredient
2130            .set_manifest_data_ref(ResourceRef::new("application/c2pa", "cloud_manifest.c2pa"))
2131            .is_ok());
2132        assert!(ingredient.manifest_data_ref().is_some());
2133    }
2134
2135    #[test]
2136    fn test_input_to_ingredient() {
2137        // create an inputTo ingredient
2138        let mut ingredient = Ingredient::new_v2("prompt", "text/plain");
2139        ingredient.relationship = Relationship::InputTo;
2140
2141        // add a resource containing our data
2142        ingredient
2143            .resources_mut()
2144            .add("prompt_id", "pirate with bird on shoulder")
2145            .expect("add");
2146
2147        // create a resource reference for the data
2148        let mut data_ref = ResourceRef::new("text/plain", "prompt_id");
2149        let data_type = crate::assertions::AssetType {
2150            asset_type: "c2pa.types.generator.prompt".to_string(),
2151            version: None,
2152        };
2153        data_ref.data_types = Some([data_type].to_vec());
2154
2155        // add the data reference to the ingredient
2156        ingredient.set_data_ref(data_ref).expect("set_data_ref");
2157
2158        println!("ingredient = {ingredient}");
2159
2160        assert_eq!(ingredient.title(), Some("prompt"));
2161        assert_eq!(ingredient.format(), Some("text/plain"));
2162        assert_eq!(ingredient.instance_id(), "None");
2163        assert_eq!(ingredient.data_ref().unwrap().identifier, "prompt_id");
2164        assert_eq!(ingredient.data_ref().unwrap().format, "text/plain");
2165        assert_eq!(ingredient.relationship(), &Relationship::InputTo);
2166        assert_eq!(
2167            ingredient.data_ref().unwrap().data_types.as_ref().unwrap()[0].asset_type,
2168            "c2pa.types.generator.prompt"
2169        );
2170    }
2171
2172    #[test]
2173    #[cfg(feature = "file_io")]
2174    fn test_input_to_file_based_ingredient() {
2175        let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2176        folder.push("tests/fixtures");
2177        let mut ingredient = Ingredient::new_v2("title", "format");
2178        ingredient.resources.set_base_path(folder);
2179        //let mut _data_ref = ResourceRef::new("image/jpeg", "foo");
2180        //data_ref.data_types = vec!["c2pa.types.dataset.pytorch".to_string()];
2181    }
2182
2183    #[test]
2184    fn test_thumbnail_from_assertion_for_svg() {
2185        let assertion = Assertion::new(
2186            "c2pa.thumbnail.ingredient",
2187            None,
2188            AssertionData::Binary(include_bytes!("../tests/fixtures/sample1.svg").to_vec()),
2189        )
2190        .set_content_type("image/svg+xml");
2191        let (format, image) = Ingredient::thumbnail_from_assertion(&assertion);
2192        assert_eq!(format, "image/svg+xml");
2193        assert_eq!(
2194            image,
2195            include_bytes!("../tests/fixtures/sample1.svg").to_vec()
2196        );
2197    }
2198}