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

builder.rs (42370B)


      1 // Copyright 2024 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 use std::{
     15     collections::HashMap,
     16     io::{Read, Seek, Write},
     17 };
     18 
     19 use async_generic::async_generic;
     20 use serde::{de::DeserializeOwned, Deserialize, Serialize};
     21 use serde_with::skip_serializing_none;
     22 use uuid::Uuid;
     23 use zip::{write::SimpleFileOptions, ZipArchive, ZipWriter};
     24 
     25 use crate::{
     26     assertion::{AssertionBase, AssertionDecodeError},
     27     assertions::{labels, Actions, CreativeWork, Exif, SoftwareAgent, Thumbnail, User, UserCbor},
     28     claim::Claim,
     29     error::{Error, Result},
     30     ingredient::Ingredient,
     31     resource_store::{ResourceRef, ResourceResolver, ResourceStore},
     32     salt::DefaultSalt,
     33     store::Store,
     34     utils::mime::format_to_mime,
     35     AsyncSigner, ClaimGeneratorInfo, Signer,
     36 };
     37 
     38 /// A Manifest Definition
     39 /// This is used to define a manifest and is used to build a ManifestStore
     40 /// A Manifest is a collection of ingredients and assertions
     41 /// It is used to define a claim that can be signed and embedded into a file
     42 #[skip_serializing_none]
     43 #[derive(Debug, Default, Deserialize, Serialize)]
     44 #[non_exhaustive]
     45 pub struct ManifestDefinition {
     46     /// Optional prefix added to the generated Manifest Label
     47     /// This is typically Internet domain name for the vendor (i.e. `adobe`)
     48     pub vendor: Option<String>,
     49 
     50     /// Clam Generator Info is always required with at least one entry
     51     #[serde(default = "default_claim_generator_info")]
     52     pub claim_generator_info: Vec<ClaimGeneratorInfo>,
     53 
     54     /// A human-readable title, generally source filename.
     55     pub title: Option<String>,
     56 
     57     /// The format of the source file as a MIME type.
     58     #[serde(default = "default_format")]
     59     pub format: String,
     60 
     61     /// Instance ID from `xmpMM:InstanceID` in XMP metadata.
     62     #[serde(default = "default_instance_id")]
     63     pub instance_id: String,
     64 
     65     pub thumbnail: Option<ResourceRef>,
     66 
     67     /// A List of ingredients
     68     #[serde(default = "default_vec::<Ingredient>")]
     69     pub ingredients: Vec<Ingredient>,
     70 
     71     /// A list of assertions
     72     #[serde(default = "default_vec::<AssertionDefinition>")]
     73     pub assertions: Vec<AssertionDefinition>,
     74 
     75     /// A list of redactions - URIs to a redacted assertions
     76     pub redactions: Option<Vec<String>>,
     77 
     78     pub label: Option<String>,
     79 }
     80 
     81 fn default_instance_id() -> String {
     82     format!("xmp:iid:{}", Uuid::new_v4())
     83 }
     84 
     85 fn default_claim_generator_info() -> Vec<ClaimGeneratorInfo> {
     86     [ClaimGeneratorInfo::default()].to_vec()
     87 }
     88 
     89 fn default_format() -> String {
     90     "application/octet-stream".to_owned()
     91 }
     92 
     93 const fn default_vec<T>() -> Vec<T> {
     94     Vec::new()
     95 }
     96 
     97 #[derive(Debug, Deserialize, Serialize, Clone)]
     98 #[serde(untagged)]
     99 pub enum AssertionData {
    100     Cbor(serde_cbor::Value),
    101     Json(serde_json::Value),
    102 }
    103 
    104 #[derive(Debug, Deserialize, Serialize, Clone)]
    105 #[non_exhaustive]
    106 pub struct AssertionDefinition {
    107     pub label: String,
    108     pub data: AssertionData,
    109 }
    110 
    111 impl AssertionDefinition {
    112     pub(crate) fn to_assertion<T: DeserializeOwned>(&self) -> Result<T> {
    113         match &self.data {
    114             AssertionData::Json(value) => serde_json::from_value(value.clone()).map_err(|e| {
    115                 Error::AssertionDecoding(AssertionDecodeError::from_err(
    116                     self.label.to_owned(),
    117                     None,
    118                     "application/json".to_owned(),
    119                     e,
    120                 ))
    121             }),
    122             AssertionData::Cbor(value) => {
    123                 serde_cbor::value::from_value(value.clone()).map_err(|e| {
    124                     Error::AssertionDecoding(AssertionDecodeError::from_err(
    125                         self.label.to_owned(),
    126                         None,
    127                         "application/cbor".to_owned(),
    128                         e,
    129                     ))
    130                 })
    131             }
    132         }
    133     }
    134 }
    135 
    136 /// A Builder is used to add a signed manifest to an asset.
    137 ///
    138 /// # Example: Building and signing a manifest
    139 ///
    140 /// ```
    141 /// # use c2pa::Result;
    142 /// use std::path::PathBuf;
    143 ///
    144 /// use c2pa::{create_signer, Builder, SigningAlg};
    145 /// use serde::Serialize;
    146 /// use serde_json::json;
    147 /// use tempfile::tempdir;
    148 ///
    149 /// #[derive(Serialize)]
    150 /// struct Test {
    151 ///     my_tag: usize,
    152 /// }
    153 ///
    154 /// # fn main() -> Result<()> {
    155 /// let manifest_json = json!({
    156 ///    "claim_generator_info": [
    157 ///       {
    158 ///           "name": "c2pa_test",
    159 ///           "version": "1.0.0"
    160 ///       }
    161 ///    ],
    162 ///    "title": "Test_Manifest"
    163 /// }).to_string();
    164 ///
    165 /// let mut builder = Builder::from_json(&manifest_json)?;
    166 /// builder.add_assertion("org.contentauth.test", &Test { my_tag: 42 })?;
    167 ///
    168 /// let source = PathBuf::from("tests/fixtures/C.jpg");
    169 /// let dir = tempdir()?;
    170 /// let dest = dir.path().join("test_file.jpg");
    171 ///
    172 /// // Create a ps256 signer using certs and key files
    173 /// let signcert_path = "tests/fixtures/certs/ps256.pub";
    174 /// let pkey_path = "tests/fixtures/certs/ps256.pem";
    175 /// let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?;
    176 ///
    177 /// // embed a manifest using the signer
    178 /// builder.sign(
    179 ///     signer.as_ref(),
    180 ///     "image/jpeg",
    181 ///     &mut std::fs::File::open(&source)?,
    182 ///     &mut std::fs::File::create(&dest)?,
    183 /// )?;
    184 /// # Ok(())
    185 /// # }
    186 /// ```
    187 #[skip_serializing_none]
    188 #[derive(Debug, Default, Deserialize, Serialize)]
    189 pub struct Builder {
    190     #[serde(flatten)]
    191     pub definition: ManifestDefinition,
    192 
    193     /// Optional remote URL for the manifest
    194     pub remote_url: Option<String>,
    195 
    196     // If true, the manifest store will not be embedded in the asset on sign
    197     pub no_embed: bool,
    198 
    199     /// container for binary assets (like thumbnails)
    200     #[serde(skip)]
    201     resources: ResourceStore,
    202 }
    203 
    204 impl AsRef<Builder> for Builder {
    205     fn as_ref(&self) -> &Self {
    206         self
    207     }
    208 }
    209 
    210 impl Builder {
    211     /// Creates a new builder from a JSON [`ManifestDefinition`] string.
    212     ///
    213     /// # Arguments
    214     /// * `json` - A JSON string representing the [`ManifestDefinition`].
    215     /// # Returns
    216     /// * A new [`Builder`].
    217     pub fn from_json(json: &str) -> Result<Self> {
    218         Ok(Self {
    219             definition: serde_json::from_str(json).map_err(Error::JsonError)?,
    220             ..Default::default()
    221         })
    222     }
    223 
    224     /// Sets the MIME format for this [`Builder`].
    225     ///
    226     /// # Arguments
    227     /// * `format` - The format of the asset associated with this [`Builder`].
    228     /// # Returns
    229     /// * A mutable reference to the [`Builder`].
    230     pub fn set_format(&mut self, format: &str) -> &mut Self {
    231         self.definition.format = format.to_string();
    232         self
    233     }
    234 
    235     /// Sets a thumbnail for the [`Builder`].
    236     ///
    237     /// The thumbnail should represent the associated asset for this [`Builder`].
    238     ///
    239     /// # Arguments
    240     /// * `format` - The format of the thumbnail.
    241     /// * `stream` - A stream to read the thumbnail from.
    242     /// # Returns
    243     /// * A mutable reference to the [`Builder`].
    244     /// # Errors
    245     /// * If the thumbnail is not valid.
    246     pub fn set_thumbnail<R>(&mut self, format: &str, stream: &mut R) -> Result<&mut Self>
    247     where
    248         R: Read + Seek + ?Sized,
    249     {
    250         // just read into a buffer until resource store handles reading streams
    251         let mut resource = Vec::new();
    252         stream.read_to_end(&mut resource)?;
    253         // add the resource and set the resource reference
    254         self.resources
    255             .add(self.definition.instance_id.clone(), resource)?;
    256         self.definition.thumbnail = Some(ResourceRef::new(
    257             format,
    258             self.definition.instance_id.clone(),
    259         ));
    260         Ok(self)
    261     }
    262 
    263     /// Adds a CBOR assertion to the manifest.
    264     /// # Arguments
    265     /// * `label` - A label for the assertion.
    266     /// * `data` - The data for the assertion. The data is any Serde Serializable type.
    267     /// # Returns
    268     /// * A mutable reference to the [`Builder`].
    269     /// # Errors
    270     /// * If the assertion is not valid.
    271     pub fn add_assertion<S, T>(&mut self, label: S, data: &T) -> Result<&mut Self>
    272     where
    273         S: Into<String>,
    274         T: Serialize,
    275     {
    276         self.definition.assertions.push(AssertionDefinition {
    277             label: label.into(),
    278             data: AssertionData::Cbor(serde_cbor::value::to_value(data)?),
    279         });
    280         Ok(self)
    281     }
    282 
    283     /// Adds a Json assertion to the manifest.
    284     /// # Arguments
    285     /// * `label` - A label for the assertion.
    286     /// * `data` - The data for the assertion. The data is any Serde Serializable type.
    287     /// # Returns
    288     /// * A mutable reference to the [`Builder`].
    289     /// # Errors
    290     /// * If the assertion is not valid.
    291     pub fn add_assertion_json<S, T>(&mut self, label: S, data: &T) -> Result<&mut Self>
    292     where
    293         S: Into<String>,
    294         T: Serialize,
    295     {
    296         self.definition.assertions.push(AssertionDefinition {
    297             label: label.into(),
    298             data: AssertionData::Json(serde_json::to_value(data)?),
    299         });
    300         Ok(self)
    301     }
    302 
    303     /// Adds an [`Ingredient`] to the manifest
    304     /// # Arguments
    305     /// * `ingredient_json` - A JSON string representing the [`Ingredient`].
    306     /// * `format` - The format of the [`Ingredient`].
    307     /// * `stream` - A stream to read the [`Ingredient`] from.
    308     /// # Returns
    309     /// * A mutable reference to the [`Ingredient`].
    310     /// # Errors
    311     /// * If the [`Ingredient`] is not valid
    312     pub fn add_ingredient<'a, T, R>(
    313         &'a mut self,
    314         ingredient_json: T,
    315         format: &str,
    316         stream: &mut R,
    317     ) -> Result<&'a mut Ingredient>
    318     where
    319         T: Into<String>,
    320         R: Read + Seek + Send,
    321     {
    322         let ingredient: Ingredient = Ingredient::from_json(&ingredient_json.into())?;
    323         let ingredient = ingredient.with_stream(format, stream)?;
    324         self.definition.ingredients.push(ingredient);
    325         #[allow(clippy::unwrap_used)]
    326         Ok(self.definition.ingredients.last_mut().unwrap()) // ok since we just added it
    327     }
    328 
    329     /// Adds a resource to the manifest.
    330     /// The id should match up with an identifier in the manifest.
    331     /// # Arguments
    332     /// * `id` - The identifier for the resource.
    333     /// * `stream` - A stream to read the resource from.
    334     /// # Returns
    335     /// * A mutable reference to the builder.
    336     /// # Errors
    337     /// * If the resource is not valid.
    338     pub fn add_resource(
    339         &mut self,
    340         id: &str,
    341         mut stream: impl Read + Seek + Send,
    342     ) -> Result<&mut Self> {
    343         if self.resources.exists(id) {
    344             return Err(Error::BadParam(id.to_string())); // todo add specific error
    345         }
    346         let mut buf = Vec::new();
    347         let _size = stream.read_to_end(&mut buf)?;
    348         self.resources.add(id, buf)?;
    349         Ok(self)
    350     }
    351 
    352     /// Convert the Builder into a archive formatted stream.
    353     ///
    354     /// The archive is a zip formatted stream containing the manifest.json, resources, and ingredients.
    355     /// # Arguments
    356     /// * `stream` - A stream to write the zip into.
    357     /// # Errors
    358     /// * If the archive cannot be written.
    359     pub fn to_archive(&mut self, stream: impl Write + Seek) -> Result<()> {
    360         drop(
    361             // this drop seems to be required to force a flush before reading back.
    362             {
    363                 let mut zip = ZipWriter::new(stream);
    364                 let options =
    365                     SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
    366                 zip.start_file("manifest.json", options)
    367                     .map_err(|e| Error::OtherError(Box::new(e)))?;
    368                 zip.write_all(&serde_json::to_vec(self)?)?;
    369                 // add a folder to the zip file
    370                 zip.start_file("resources/", options)
    371                     .map_err(|e| Error::OtherError(Box::new(e)))?;
    372                 for (id, data) in self.resources.resources() {
    373                     zip.start_file(format!("resources/{}", id), options)
    374                         .map_err(|e| Error::OtherError(Box::new(e)))?;
    375                     zip.write_all(data)?;
    376                 }
    377                 for (index, ingredient) in self.definition.ingredients.iter().enumerate() {
    378                     zip.start_file(format!("ingredients/{}/", index), options)
    379                         .map_err(|e| Error::OtherError(Box::new(e)))?;
    380                     for (id, data) in ingredient.resources().resources() {
    381                         //println!("adding ingredient {}/{}", index, id);
    382                         zip.start_file(format!("ingredients/{}/{}", index, id), options)
    383                             .map_err(|e| Error::OtherError(Box::new(e)))?;
    384                         zip.write_all(data)?;
    385                     }
    386                 }
    387                 zip.finish()
    388             }
    389             .map_err(|e| Error::OtherError(Box::new(e)))?,
    390         );
    391         Ok(())
    392     }
    393 
    394     /// Unpacks an archive stream into a Builder.
    395     /// # Arguments
    396     /// * `stream` - A stream to read the archive from.
    397     /// # Returns
    398     /// * A new Builder.
    399     /// # Errors
    400     /// * If the archive cannot be read.
    401     pub fn from_archive(stream: impl Read + Seek) -> Result<Self> {
    402         let mut zip = ZipArchive::new(stream).map_err(|e| Error::OtherError(Box::new(e)))?;
    403         let mut manifest = zip
    404             .by_name("manifest.json")
    405             .map_err(|e| Error::OtherError(Box::new(e)))?;
    406         let mut manifest_json = Vec::new();
    407         manifest.read_to_end(&mut manifest_json)?;
    408         let mut builder: Builder =
    409             serde_json::from_slice(&manifest_json).map_err(|e| Error::OtherError(Box::new(e)))?;
    410         drop(manifest);
    411         for i in 0..zip.len() {
    412             let mut file = zip
    413                 .by_index(i)
    414                 .map_err(|e| Error::OtherError(Box::new(e)))?;
    415 
    416             if file.name().starts_with("resources/") && file.name() != "resources/" {
    417                 let mut data = Vec::new();
    418                 file.read_to_end(&mut data)?;
    419                 let id = file
    420                     .name()
    421                     .split('/')
    422                     .nth(1)
    423                     .ok_or(Error::BadParam("Invalid resource path".to_string()))?;
    424                 //println!("adding resource {}", id);
    425                 builder.resources.add(id, data)?;
    426             }
    427             if file.name().starts_with("ingredients/") && file.name() != "ingredients/" {
    428                 let mut data = Vec::new();
    429                 file.read_to_end(&mut data)?;
    430                 let index: usize = file
    431                     .name()
    432                     .split('/')
    433                     .nth(1)
    434                     .ok_or_else(|| Error::BadParam("Invalid ingredient path".to_string()))?
    435                     .parse::<usize>()
    436                     .map_err(|_| Error::BadParam("Invalid ingredient path".to_string()))?;
    437                 let id = file.name().split('/').nth(2).unwrap_or_default();
    438                 if index >= builder.definition.ingredients.len() {
    439                     return Err(Error::OtherError(Box::new(std::io::Error::new(
    440                         std::io::ErrorKind::Other,
    441                         format!("Invalid ingredient index {}", index),
    442                     ))))?; // todo add specific error
    443                 }
    444                 builder.definition.ingredients[index]
    445                     .resources_mut()
    446                     .add(id, data)?;
    447             }
    448         }
    449         Ok(builder)
    450     }
    451 
    452     // Convert a Manifest into a Claim
    453     fn to_claim(&self) -> Result<Claim> {
    454         let definition = &self.definition;
    455         let mut claim_generator_info = definition.claim_generator_info.clone();
    456         // add the default claim generator info for this library
    457         claim_generator_info.push(ClaimGeneratorInfo::default());
    458 
    459         // build the claim_generator string since this is required
    460         let claim_generator: String = claim_generator_info
    461             .iter()
    462             .map(|s| {
    463                 let name = s.name.replace(' ', "_");
    464                 if let Some(version) = s.version.as_deref() {
    465                     format!("{}/{}", name.to_lowercase(), version)
    466                 } else {
    467                     name
    468                 }
    469             })
    470             .collect::<Vec<String>>()
    471             .join(" ");
    472 
    473         let mut claim = match definition.label.as_ref() {
    474             Some(label) => Claim::new_with_user_guid(&claim_generator, &label.to_string()),
    475             None => Claim::new(&claim_generator, definition.vendor.as_deref()),
    476         };
    477 
    478         // add claim generator info to claim resolving icons
    479         for info in &claim_generator_info {
    480             let mut claim_info = info.to_owned();
    481             if let Some(icon) = claim_info.icon.as_ref() {
    482                 claim_info.icon = Some(icon.to_hashed_uri(&self.resources, &mut claim)?);
    483             }
    484             claim.add_claim_generator_info(claim_info);
    485         }
    486 
    487         if let Some(remote_url) = &self.remote_url {
    488             if self.no_embed {
    489                 claim.set_remote_manifest(remote_url)?;
    490             } else {
    491                 claim.set_embed_remote_manifest(remote_url)?;
    492             }
    493         } else if self.no_embed {
    494             claim.set_external_manifest()
    495         }
    496 
    497         if let Some(title) = definition.title.as_ref() {
    498             claim.set_title(Some(title.to_owned()));
    499         }
    500         definition.format.clone_into(&mut claim.format);
    501         definition.instance_id.clone_into(&mut claim.instance_id);
    502 
    503         if let Some(thumb_ref) = definition.thumbnail.as_ref() {
    504             // Setting the format to "none" will ensure that no claim thumbnail is added
    505             if thumb_ref.format != "none" {
    506                 //let data = self.resources.get(&thumb_ref.identifier)?;
    507                 let mut stream = self.resources.open(thumb_ref)?;
    508                 let mut data = Vec::new();
    509                 stream.read_to_end(&mut data)?;
    510                 claim.add_assertion(&Thumbnail::new(
    511                     &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, &thumb_ref.format),
    512                     data,
    513                 ))?;
    514             }
    515         }
    516 
    517         let mut ingredient_map = HashMap::new();
    518         // add all ingredients to the claim
    519         for ingredient in &definition.ingredients {
    520             //let ingredient = ingredient_builder.build(self)?;
    521             let uri = ingredient.add_to_claim(
    522                 &mut claim,
    523                 definition.redactions.clone(),
    524                 Some(&self.resources),
    525             )?;
    526             ingredient_map.insert(ingredient.instance_id().to_string(), uri);
    527         }
    528 
    529         let salt = DefaultSalt::default();
    530 
    531         // add any additional assertions
    532         for manifest_assertion in &definition.assertions {
    533             match manifest_assertion.label.as_str() {
    534                 l if l.starts_with(Actions::LABEL) => {
    535                     let version = labels::version(l);
    536 
    537                     let mut actions: Actions = manifest_assertion.to_assertion()?;
    538 
    539                     let ingredients_key = match version {
    540                         None | Some(1) => "ingredient",
    541                         Some(2) => "ingredients",
    542                         _ => return Err(Error::AssertionUnsupportedVersion),
    543                     };
    544 
    545                     // fixup parameters field from instance_id to ingredient uri
    546                     let needs_ingredient: Vec<(usize, crate::assertions::Action)> = actions
    547                         .actions()
    548                         .iter()
    549                         .enumerate()
    550                         .filter_map(|(i, a)| {
    551                             if a.instance_id().is_some()
    552                                 && a.get_parameter(ingredients_key).is_none()
    553                             {
    554                                 Some((i, a.clone()))
    555                             } else {
    556                                 None
    557                             }
    558                         })
    559                         .collect();
    560 
    561                     for (index, action) in needs_ingredient {
    562                         if let Some(id) = action.instance_id() {
    563                             if let Some(hash_url) = ingredient_map.get(id) {
    564                                 let update = match ingredients_key {
    565                                     "ingredient" => {
    566                                         action.set_parameter(ingredients_key, hash_url.clone())
    567                                     }
    568                                     _ => {
    569                                         // we only support on instanceId for actions, so only one ingredient on writing
    570                                         action.set_parameter(ingredients_key, [hash_url.clone()])
    571                                     }
    572                                 }?;
    573                                 actions = actions.update_action(index, update);
    574                             }
    575                         }
    576                     }
    577 
    578                     if let Some(templates) = actions.templates.as_mut() {
    579                         for template in templates {
    580                             // replace icon with hashed_uri
    581                             template.icon = match template.icon.take() {
    582                                 Some(icon) => {
    583                                     Some(icon.to_hashed_uri(&self.resources, &mut claim)?)
    584                                 }
    585                                 None => None,
    586                             };
    587 
    588                             // replace software agent with hashed_uri
    589                             template.software_agent = match template.software_agent.take() {
    590                                 Some(SoftwareAgent::ClaimGeneratorInfo(mut info)) => {
    591                                     if let Some(icon) = info.icon.as_mut() {
    592                                         let icon =
    593                                             icon.to_hashed_uri(&self.resources, &mut claim)?;
    594                                         info.set_icon(icon);
    595                                     }
    596                                     Some(SoftwareAgent::ClaimGeneratorInfo(info))
    597                                 }
    598                                 agent => agent,
    599                             };
    600                         }
    601                     }
    602 
    603                     // convert icons in software agents to hashed uris
    604                     let actions_mut = actions.actions_mut();
    605                     #[allow(clippy::needless_range_loop)]
    606                     // clippy is wrong here, we reference index twice
    607                     for index in 0..actions_mut.len() {
    608                         let action = &actions_mut[index];
    609                         if let Some(SoftwareAgent::ClaimGeneratorInfo(info)) =
    610                             action.software_agent()
    611                         {
    612                             if let Some(icon) = info.icon.as_ref() {
    613                                 let mut info = info.to_owned();
    614                                 let icon_uri = icon.to_hashed_uri(&self.resources, &mut claim)?;
    615                                 let update = info.set_icon(icon_uri);
    616                                 let mut action = action.to_owned();
    617                                 action = action.set_software_agent(update.to_owned());
    618                                 actions_mut[index] = action;
    619                             }
    620                         }
    621                     }
    622 
    623                     claim.add_assertion(&actions)
    624                 }
    625                 CreativeWork::LABEL => {
    626                     let cw: CreativeWork = manifest_assertion.to_assertion()?;
    627 
    628                     claim.add_assertion_with_salt(&cw, &salt)
    629                 }
    630                 Exif::LABEL => {
    631                     let exif: Exif = manifest_assertion.to_assertion()?;
    632                     claim.add_assertion_with_salt(&exif, &salt)
    633                 }
    634                 _ => match &manifest_assertion.data {
    635                     AssertionData::Json(value) => claim.add_assertion_with_salt(
    636                         &User::new(&manifest_assertion.label, &serde_json::to_string(&value)?),
    637                         &salt,
    638                     ),
    639                     AssertionData::Cbor(value) => claim.add_assertion_with_salt(
    640                         &UserCbor::new(&manifest_assertion.label, serde_cbor::to_vec(value)?),
    641                         &salt,
    642                     ),
    643                 },
    644             }?;
    645         }
    646 
    647         Ok(claim)
    648     }
    649 
    650     // Convert a Manifest into a Store
    651     fn to_store(&self) -> Result<Store> {
    652         let claim = self.to_claim()?;
    653         // commit the claim
    654         let mut store = Store::new();
    655         let _provenance = store.commit_claim(claim)?;
    656         Ok(store)
    657     }
    658 
    659     #[cfg(feature = "add_thumbnails")]
    660     fn maybe_add_thumbnail<R>(&mut self, format: &str, stream: &mut R) -> Result<&mut Self>
    661     where
    662         R: Read + Seek + ?Sized,
    663     {
    664         // check settings to see if we should auto generate a thumbnail
    665         let auto_thumbnail = crate::settings::get_settings_value::<bool>("builder.auto_thumbnail")?;
    666         if self.definition.thumbnail.is_none() && auto_thumbnail {
    667             stream.rewind()?;
    668             if let Ok((format, image)) =
    669                 crate::utils::thumbnail::make_thumbnail_from_stream(format, stream)
    670             {
    671                 stream.rewind()?;
    672                 self.resources
    673                     .add(self.definition.instance_id.clone(), image)?;
    674                 self.definition.thumbnail = Some(ResourceRef::new(
    675                     format,
    676                     self.definition.instance_id.clone(),
    677                 ));
    678             }
    679         }
    680         Ok(self)
    681     }
    682 
    683     /// Embed a signed manifest into a stream using a supplied signer.
    684     /// # Arguments
    685     /// * `format` - The format of the stream
    686     /// * `source` - The stream to read from
    687     /// * `dest` - The stream to write to
    688     /// * `signer` - The signer to use
    689     /// # Returns
    690     /// * The bytes of c2pa_manifest that was embedded.
    691     /// # Errors
    692     /// * If the manifest cannot be signed.
    693     #[async_generic(async_signature(
    694         &mut self,
    695         signer: &dyn AsyncSigner,
    696         format: &str,
    697         source: &mut R,
    698         dest: &mut W,
    699     ))]
    700     pub fn sign<R, W>(
    701         &mut self,
    702         signer: &dyn Signer,
    703         format: &str,
    704         source: &mut R,
    705         dest: &mut W,
    706     ) -> Result<Vec<u8>>
    707     where
    708         R: Read + Seek + Send,
    709         W: Write + Read + Seek + Send,
    710     {
    711         let format = format_to_mime(format);
    712         self.definition.format.clone_from(&format);
    713         // todo:: read instance_id from xmp from stream ?
    714         self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4());
    715 
    716         // generate thumbnail if we don't already have one
    717         #[cfg(feature = "add_thumbnails")]
    718         self.maybe_add_thumbnail(&format, source)?;
    719 
    720         // convert the manifest to a store
    721         let mut store = self.to_store()?;
    722 
    723         // sign and write our store to to the output image file
    724         if _sync {
    725             store.save_to_stream(&format, source, dest, signer)
    726         } else {
    727             store
    728                 .save_to_stream_async(&format, source, dest, signer)
    729                 .await
    730         }
    731     }
    732 
    733     #[cfg(feature = "file_io")]
    734     /// Sign a file using a supplied signer.
    735     /// # Arguments
    736     /// * `source` - The path to the file to read from.
    737     /// * `dest` - The path to the file to write to (this must not already exist).
    738     /// * `signer` - The signer to use.
    739     /// # Returns
    740     /// * The bytes of c2pa_manifest that was created.
    741     /// # Errors
    742     /// * If the manifest cannot be signed.
    743     pub fn sign_file<S, D>(&mut self, signer: &dyn Signer, source: S, dest: D) -> Result<Vec<u8>>
    744     where
    745         S: AsRef<std::path::Path>,
    746         D: AsRef<std::path::Path>,
    747     {
    748         let source = source.as_ref();
    749         let dest = dest.as_ref();
    750         // formats must match but allow extensions to be slightly different (i.e. .jpeg vs .jpg)s
    751         let format = crate::format_from_path(source).ok_or(crate::Error::UnsupportedType)?;
    752         let format_dest = crate::format_from_path(dest).ok_or(crate::Error::UnsupportedType)?;
    753         if format != format_dest {
    754             return Err(crate::Error::BadParam(
    755                 "Source and destination file formats must match".to_string(),
    756             ));
    757         }
    758         let mut source = std::fs::File::open(source)?;
    759         if !dest.exists() {
    760             // ensure the path to the file exists
    761             if let Some(output_dir) = dest.parent() {
    762                 std::fs::create_dir_all(output_dir)?;
    763             }
    764         } else {
    765             // if the file exists, we need to remove it to avoid appending to it
    766             return Err(crate::Error::BadParam(
    767                 "Destination file already exists".to_string(),
    768             ));
    769         };
    770         let mut dest = std::fs::File::create(dest)?;
    771 
    772         self.sign(signer, &format, &mut source, &mut dest)
    773     }
    774 }
    775 
    776 #[cfg(test)]
    777 mod tests {
    778     #![allow(clippy::expect_used)]
    779     #![allow(clippy::unwrap_used)]
    780 
    781     use std::io::Cursor;
    782 
    783     use serde_json::json;
    784     #[cfg(target_arch = "wasm32")]
    785     use wasm_bindgen_test::*;
    786 
    787     use super::*;
    788     use crate::{utils::test::temp_signer, Reader};
    789 
    790     #[cfg(target_arch = "wasm32")]
    791     wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
    792 
    793     fn parent_json() -> String {
    794         json!({
    795             "title": "Parent Test",
    796             "format": "image/jpeg",
    797             "instance_id": "12345",
    798             "relationship": "parentOf"
    799         })
    800         .to_string()
    801     }
    802 
    803     fn manifest_json() -> String {
    804         json!({
    805             "vendor": "test",
    806             "claim_generator_info": [
    807                 {
    808                     "name": "c2pa_test",
    809                     "version": "1.0.0"
    810                 }
    811             ],
    812             "title": "Test_Manifest",
    813             "format": "image/tiff",
    814             "instance_id": "1234",
    815             "thumbnail": {
    816                 "format": "image/jpeg",
    817                 "identifier": "thumbnail1.jpg"
    818             },
    819             "ingredients": [
    820                 {
    821                     "title": "Test",
    822                     "format": "image/jpeg",
    823                     "instance_id": "12345",
    824                     "relationship": "componentOf"
    825                 }
    826             ],
    827             "assertions": [
    828                 {
    829                     "label": "org.test.assertion",
    830                     "data": "assertion"
    831                 }
    832             ]
    833         })
    834         .to_string()
    835     }
    836 
    837     #[cfg(not(target_arch = "wasm32"))]
    838     const TEST_IMAGE_CLEAN: &[u8] = include_bytes!("../tests/fixtures/IMG_0003.jpg");
    839     const TEST_IMAGE: &[u8] = include_bytes!("../tests/fixtures/CA.jpg");
    840 
    841     #[test]
    842     /// example of creating a builder directly with a [`ManifestDefinition`]
    843     fn test_manifest_store_builder() {
    844         let mut image = Cursor::new(TEST_IMAGE);
    845 
    846         let thumbnail_ref = ResourceRef::new("ingredient/jpeg", "5678");
    847 
    848         let definition = ManifestDefinition {
    849             vendor: Some("test".to_string()),
    850             claim_generator_info: [ClaimGeneratorInfo::default()].to_vec(),
    851             format: "image/tiff".to_string(),
    852             title: Some("Test_Manifest".to_string()),
    853             instance_id: "1234".to_string(),
    854             thumbnail: Some(thumbnail_ref.clone()),
    855             label: Some("ABCDE".to_string()),
    856             ..Default::default()
    857         };
    858 
    859         let mut builder = Builder {
    860             definition,
    861             ..Default::default()
    862         };
    863 
    864         builder
    865             .add_ingredient(parent_json(), "image/jpeg", &mut image)
    866             .unwrap();
    867 
    868         builder
    869             .add_assertion("org.test.assertion", &"assertion".to_string())
    870             .unwrap();
    871 
    872         builder
    873             .add_resource(&thumbnail_ref.identifier, Cursor::new(b"12345"))
    874             .unwrap();
    875 
    876         let definition = &builder.definition;
    877         assert_eq!(definition.vendor, Some("test".to_string()));
    878         assert_eq!(definition.title, Some("Test_Manifest".to_string()));
    879         assert_eq!(definition.format, "image/tiff".to_string());
    880         assert_eq!(definition.instance_id, "1234".to_string());
    881         assert_eq!(definition.thumbnail, Some(thumbnail_ref));
    882         assert_eq!(definition.ingredients[0].title(), "Parent Test".to_string());
    883         assert_eq!(
    884             definition.assertions[0].label,
    885             "org.test.assertion".to_string()
    886         );
    887         assert_eq!(definition.label, Some("ABCDE".to_string()));
    888         assert_eq!(
    889             builder
    890                 .resources
    891                 .get(&builder.definition.thumbnail.unwrap().identifier)
    892                 .unwrap()
    893                 .into_owned(),
    894             b"12345"
    895         );
    896     }
    897 
    898     #[test]
    899     fn test_from_json() {
    900         // strip whitespace so we can compare later
    901         let mut stripped_json = manifest_json();
    902         stripped_json.retain(|c| !c.is_whitespace());
    903         let mut builder = Builder::from_json(&stripped_json).unwrap();
    904         builder.resources.add("5678", "12345").unwrap();
    905         let definition = &builder.definition;
    906         assert_eq!(definition.vendor, Some("test".to_string()));
    907         assert_eq!(definition.title, Some("Test_Manifest".to_string()));
    908         assert_eq!(definition.format, "image/tiff".to_string());
    909         assert_eq!(definition.instance_id, "1234".to_string());
    910         assert_eq!(
    911             definition.thumbnail.clone().unwrap().identifier.as_str(),
    912             "thumbnail1.jpg"
    913         );
    914         assert_eq!(definition.ingredients[0].title(), "Test".to_string());
    915         assert_eq!(
    916             definition.assertions[0].label,
    917             "org.test.assertion".to_string()
    918         );
    919 
    920         // convert back to json and compare to original
    921         let builder_json = serde_json::to_string(&builder.definition).unwrap();
    922         assert_eq!(builder_json, stripped_json);
    923     }
    924 
    925     #[test]
    926     fn test_builder_sign() {
    927         #[derive(Serialize, Deserialize)]
    928         struct TestAssertion {
    929             answer: usize,
    930         }
    931         let format = "image/jpeg";
    932         let mut source = Cursor::new(TEST_IMAGE);
    933         let mut dest = Cursor::new(Vec::new());
    934 
    935         let mut builder = Builder::from_json(&manifest_json()).unwrap();
    936         builder
    937             .add_ingredient(parent_json().to_string(), format, &mut source)
    938             .unwrap();
    939 
    940         builder
    941             .resources
    942             .add("thumbnail1.jpg", TEST_IMAGE.to_vec())
    943             .unwrap();
    944 
    945         builder
    946             .add_assertion("org.life.meaning", &TestAssertion { answer: 42 })
    947             .unwrap();
    948 
    949         builder
    950             .add_assertion_json("org.life.meaning.json", &TestAssertion { answer: 42 })
    951             .unwrap();
    952 
    953         // write the manifest builder to a zipped stream
    954         let mut zipped = Cursor::new(Vec::new());
    955         builder.to_archive(&mut zipped).unwrap();
    956 
    957         // write the zipped stream to a file for debugging
    958         std::fs::write("../target/test.zip", zipped.get_ref()).unwrap();
    959 
    960         // unzip the manifest builder from the zipped stream
    961         zipped.rewind().unwrap();
    962         let mut _builder = Builder::from_archive(&mut zipped).unwrap();
    963 
    964         // sign and write to the output stream
    965         let signer = temp_signer();
    966         builder
    967             .sign(signer.as_ref(), format, &mut source, &mut dest)
    968             .unwrap();
    969 
    970         // read and validate the signed manifest store
    971         dest.rewind().unwrap();
    972         let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes");
    973 
    974         println!("{}", manifest_store);
    975         assert!(manifest_store.validation_status().is_none());
    976         assert!(manifest_store.active_manifest().is_some());
    977         let manifest = manifest_store.active_manifest().unwrap();
    978         assert_eq!(manifest.title().unwrap(), "Test_Manifest");
    979         let test_assertion: TestAssertion = manifest.find_assertion("org.life.meaning").unwrap();
    980         assert_eq!(test_assertion.answer, 42);
    981     }
    982 
    983     #[test]
    984     #[cfg(feature = "file_io")]
    985     fn test_builder_sign_file() {
    986         let source = "tests/fixtures/CA.jpg";
    987         let dir = tempfile::tempdir().unwrap();
    988         let dest = dir.path().join("test_file.jpg");
    989 
    990         let mut builder = Builder::from_json(&manifest_json()).unwrap();
    991 
    992         builder
    993             .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE))
    994             .unwrap();
    995 
    996         // sign and write to the output stream
    997         let signer = temp_signer();
    998         builder.sign_file(signer.as_ref(), source, &dest).unwrap();
    999 
   1000         // read and validate the signed manifest store
   1001         let manifest_store = Reader::from_file(&dest).expect("from_bytes");
   1002 
   1003         println!("{}", manifest_store);
   1004         assert!(manifest_store.validation_status().is_none());
   1005         assert_eq!(
   1006             manifest_store.active_manifest().unwrap().title().unwrap(),
   1007             "Test_Manifest"
   1008         );
   1009     }
   1010 
   1011     #[test]
   1012     #[cfg(feature = "file_io")]
   1013     fn test_builder_sign_assets() {
   1014         const TESTFILES: &[&str] = &[
   1015             "IMG_0003.jpg",
   1016             "sample1.png",
   1017             "sample1.webp",
   1018             "TUSCANY.TIF",
   1019             "sample1.svg",
   1020             "sample1.wav",
   1021             "test.avi",
   1022             "sample1.mp3",
   1023             "sample1.avif",
   1024             "sample1.heic",
   1025             "sample1.heif",
   1026             "video1.mp4",
   1027             "cloud_manifest.c2pa",
   1028         ];
   1029         for file_name in TESTFILES {
   1030             let extension = file_name.split('.').last().unwrap();
   1031             let format = extension;
   1032 
   1033             let path = format!("tests/fixtures/{}", file_name);
   1034             println!("path: {}", path);
   1035             let mut source = std::fs::File::open(path).unwrap();
   1036             let mut dest = Cursor::new(Vec::new());
   1037 
   1038             let mut builder = Builder::from_json(&manifest_json()).unwrap();
   1039             builder
   1040                 .add_ingredient(parent_json(), format, &mut source)
   1041                 .unwrap();
   1042 
   1043             builder
   1044                 .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE))
   1045                 .unwrap();
   1046 
   1047             // sign and write to the output stream
   1048             let signer = temp_signer();
   1049             builder
   1050                 .sign(signer.as_ref(), format, &mut source, &mut dest)
   1051                 .unwrap();
   1052 
   1053             // read and validate the signed manifest store
   1054             dest.rewind().unwrap();
   1055             let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes");
   1056 
   1057             println!("{}", manifest_store);
   1058             if format != "c2pa" {
   1059                 // c2pa files will not validate since they have no associated asset
   1060                 assert!(manifest_store.validation_status().is_none());
   1061             }
   1062             assert_eq!(
   1063                 manifest_store.active_manifest().unwrap().title().unwrap(),
   1064                 "Test_Manifest"
   1065             );
   1066 
   1067             // enable to write the signed manifests to a file for debugging
   1068             // let dest_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
   1069             //     .join("../target")
   1070             //     .join("signed")
   1071             //     .join(file_name);
   1072 
   1073             // std::fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
   1074             // std::fs::write(&dest_path, dest.get_ref()).unwrap();
   1075         }
   1076     }
   1077 
   1078     #[cfg_attr(not(target_arch = "wasm32"), actix::test)]
   1079     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
   1080     async fn test_builder_remote_sign() {
   1081         let format = "image/jpeg";
   1082         let mut source = Cursor::new(TEST_IMAGE);
   1083         let mut dest = Cursor::new(Vec::new());
   1084 
   1085         let mut builder = Builder::from_json(&manifest_json()).unwrap();
   1086         builder
   1087             .add_ingredient(&parent_json(), format, &mut source)
   1088             .unwrap();
   1089 
   1090         builder
   1091             .resources
   1092             .add("thumbnail1.jpg", TEST_IMAGE.to_vec())
   1093             .unwrap();
   1094 
   1095         // sign the ManifestStoreBuilder and write it to the output stream
   1096         let signer = crate::utils::test::temp_async_remote_signer();
   1097         builder
   1098             .sign_async(signer.as_ref(), format, &mut source, &mut dest)
   1099             .await
   1100             .unwrap();
   1101 
   1102         // read and validate the signed manifest store
   1103         dest.rewind().unwrap();
   1104         let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes");
   1105 
   1106         println!("{}", manifest_store);
   1107         #[cfg(not(target_arch = "wasm32"))] // skip this until we get wasm async signing working
   1108         assert!(manifest_store.validation_status().is_none());
   1109         assert_eq!(
   1110             manifest_store.active_manifest().unwrap().title().unwrap(),
   1111             "Test_Manifest"
   1112         );
   1113     }
   1114 
   1115     #[test]
   1116     #[cfg(not(target_arch = "wasm32"))]
   1117     fn test_builder_remote_url() {
   1118         let mut source = Cursor::new(TEST_IMAGE_CLEAN);
   1119         let mut dest = Cursor::new(Vec::new());
   1120 
   1121         let mut builder = Builder::from_json(&manifest_json()).unwrap();
   1122         builder.remote_url = Some("http://my_remote_url".to_string());
   1123         builder.no_embed = true;
   1124 
   1125         builder
   1126             .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE))
   1127             .unwrap();
   1128 
   1129         // sign the ManifestStoreBuilder and write it to the output stream
   1130         let signer = temp_signer();
   1131         let manifest_data = builder
   1132             .sign(signer.as_ref(), "image/jpeg", &mut source, &mut dest)
   1133             .unwrap();
   1134 
   1135         // check to make sure we have a remote url and no manifest data
   1136         dest.set_position(0);
   1137         let _err = c2pa::Reader::from_stream("image/jpeg", &mut dest).expect_err("from_bytes");
   1138 
   1139         // now validate the manifest against the written asset
   1140         dest.set_position(0);
   1141         let reader =
   1142             c2pa::Reader::from_manifest_data_and_stream(&manifest_data, "image/jpeg", &mut dest)
   1143                 .expect("from_bytes");
   1144 
   1145         println!("{}", reader.json());
   1146         assert!(reader.validation_status().is_none());
   1147     }
   1148 }