c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

ingredient.rs (74490B)


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