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

resource_store.rs (15861B)


      1 // Copyright 2023 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     borrow::Cow,
     16     collections::HashMap,
     17     io::{Read, Seek, Write},
     18 };
     19 #[cfg(feature = "file_io")]
     20 use std::{
     21     fs::{create_dir_all, read, write},
     22     path::{Path, PathBuf},
     23 };
     24 
     25 #[cfg(feature = "json_schema")]
     26 use schemars::JsonSchema;
     27 use serde::{Deserialize, Serialize};
     28 
     29 #[cfg(feature = "unstable_api")]
     30 use crate::asset_io::CAIRead;
     31 use crate::{
     32     assertions::{labels, AssetType},
     33     claim::Claim,
     34     hashed_uri::HashedUri,
     35     jumbf::labels::assertion_label_from_uri,
     36     Error, Result,
     37 };
     38 
     39 /// Function that is used by serde to determine whether or not we should serialize
     40 /// resources based on the `serialize_resources` flag.
     41 /// (Serialization is disabled by default.)
     42 pub(crate) const fn skip_serializing_resources(_: &ResourceStore) -> bool {
     43     !cfg!(feature = "serialize_thumbnails") || cfg!(test) || cfg!(not(target_arch = "wasm32"))
     44 }
     45 
     46 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
     47 #[cfg_attr(feature = "json_schema", derive(JsonSchema))]
     48 #[serde(untagged)]
     49 pub enum UriOrResource {
     50     ResourceRef(ResourceRef),
     51     HashedUri(HashedUri),
     52 }
     53 impl UriOrResource {
     54     pub fn to_hashed_uri(
     55         &self,
     56         resources: &ResourceStore,
     57         claim: &mut Claim,
     58     ) -> Result<UriOrResource> {
     59         match self {
     60             UriOrResource::ResourceRef(r) => {
     61                 let data = resources.get(&r.identifier)?;
     62                 let hash_uri = claim.add_databox(&r.format, data.to_vec(), None)?;
     63                 Ok(UriOrResource::HashedUri(hash_uri))
     64             }
     65             UriOrResource::HashedUri(h) => Ok(UriOrResource::HashedUri(h.clone())),
     66         }
     67     }
     68 
     69     pub fn to_resource_ref(
     70         &self,
     71         resources: &mut ResourceStore,
     72         claim: &Claim,
     73     ) -> Result<UriOrResource> {
     74         match self {
     75             UriOrResource::ResourceRef(r) => Ok(UriOrResource::ResourceRef(r.clone())),
     76             UriOrResource::HashedUri(h) => {
     77                 let uri = crate::jumbf::labels::to_absolute_uri(claim.label(), &h.url());
     78                 let data_box = claim.find_databox(&uri).ok_or(Error::MissingDataBox)?;
     79                 let resource_ref =
     80                     resources.add_with(&h.url(), &data_box.format, data_box.data.clone())?;
     81                 Ok(UriOrResource::ResourceRef(resource_ref))
     82             }
     83         }
     84     }
     85 }
     86 
     87 impl From<ResourceRef> for UriOrResource {
     88     fn from(r: ResourceRef) -> Self {
     89         Self::ResourceRef(r)
     90     }
     91 }
     92 
     93 impl From<HashedUri> for UriOrResource {
     94     fn from(h: HashedUri) -> Self {
     95         Self::HashedUri(h)
     96     }
     97 }
     98 
     99 #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
    100 #[cfg_attr(feature = "json_schema", derive(JsonSchema))]
    101 /// A reference to a resource to be used in JSON serialization.
    102 ///
    103 /// The underlying data can be read as a stream via [`Reader::resource_to_stream`][crate::Reader::resource_to_stream].
    104 pub struct ResourceRef {
    105     /// The mime type of the referenced resource.
    106     pub format: String,
    107 
    108     /// A URI that identifies the resource as referenced from the manifest.
    109     ///
    110     /// This may be a JUMBF URI, a file path, a URL or any other string.
    111     /// Relative JUMBF URIs will be resolved with the manifest label.
    112     /// Relative file paths will be resolved with the base path if provided.
    113     pub identifier: String,
    114 
    115     /// More detailed data types as defined in the C2PA spec.
    116     #[serde(skip_serializing_if = "Option::is_none")]
    117     pub data_types: Option<Vec<AssetType>>,
    118 
    119     /// The algorithm used to hash the resource (if applicable).
    120     #[serde(skip_serializing_if = "Option::is_none")]
    121     pub alg: Option<String>,
    122 
    123     /// The hash of the resource (if applicable).
    124     #[serde(skip_serializing_if = "Option::is_none")]
    125     pub hash: Option<String>,
    126 }
    127 
    128 impl ResourceRef {
    129     pub fn new<S: Into<String>, I: Into<String>>(format: S, identifier: I) -> Self {
    130         Self {
    131             format: format.into(),
    132             identifier: identifier.into(),
    133             data_types: None,
    134             alg: None,
    135             hash: None,
    136         }
    137     }
    138 }
    139 
    140 /// Resource store to contain binary objects referenced from JSON serializable structures
    141 #[derive(Debug, Serialize)]
    142 #[cfg_attr(feature = "json_schema", derive(JsonSchema))]
    143 pub struct ResourceStore {
    144     resources: HashMap<String, Vec<u8>>,
    145     #[cfg(feature = "file_io")]
    146     #[serde(skip_serializing_if = "Option::is_none")]
    147     base_path: Option<PathBuf>,
    148     #[serde(skip_serializing_if = "Option::is_none")]
    149     label: Option<String>,
    150 }
    151 
    152 impl ResourceStore {
    153     /// Create a new resource reference.
    154     pub fn new() -> Self {
    155         ResourceStore {
    156             resources: HashMap::new(),
    157             #[cfg(feature = "file_io")]
    158             base_path: None,
    159             label: None,
    160         }
    161     }
    162 
    163     /// Set a manifest label for this store used to resolve relative JUMBF URIs.
    164     pub fn set_label<S: Into<String>>(&mut self, label: S) -> &Self {
    165         self.label = Some(label.into());
    166         self
    167     }
    168 
    169     #[cfg(feature = "file_io")]
    170     // Returns the base path for relative file paths if it is set.
    171     pub fn base_path(&self) -> Option<&Path> {
    172         self.base_path.as_deref()
    173     }
    174 
    175     #[cfg(feature = "file_io")]
    176     /// Sets a base path for relative file paths.
    177     ///
    178     /// Identifiers will be interpreted as file paths and resources will be written to files if this is set.
    179     pub fn set_base_path<P: Into<PathBuf>>(&mut self, base_path: P) {
    180         self.base_path = Some(base_path.into());
    181     }
    182 
    183     #[cfg(feature = "file_io")]
    184     /// Returns and removes the base path.
    185     pub fn take_base_path(&mut self) -> Option<PathBuf> {
    186         self.base_path.take()
    187     }
    188 
    189     /// Generates a unique ID for a given content type (adds a file extension).
    190     pub fn id_from(&self, key: &str, format: &str) -> String {
    191         let ext = match format {
    192             "jpg" | "jpeg" | "image/jpeg" => ".jpg",
    193             "png" | "image/png" => ".png",
    194             //make "svg" | "image/svg+xml" => ".svg",
    195             "c2pa" | "application/x-c2pa-manifest-store" | "application/c2pa" => ".c2pa",
    196             _ => "",
    197         };
    198         // clean string for possible filesystem use
    199         let id_base = key.replace(['/', ':'], "-");
    200 
    201         // ensure it is unique in this store
    202         let mut count = 1;
    203         let mut id = format!("{id_base}{ext}");
    204         while self.exists(&id) {
    205             id = format!("{id_base}-{count}{ext}");
    206             count += 1;
    207         }
    208         id
    209     }
    210 
    211     /// Adds a resource, generating a [`ResourceRef`] from a key and format.
    212     ///
    213     /// The generated identifier may be different from the key.
    214     pub fn add_with<R>(&mut self, key: &str, format: &str, value: R) -> crate::Result<ResourceRef>
    215     where
    216         R: Into<Vec<u8>>,
    217     {
    218         let id = self.id_from(key, format);
    219         self.add(&id, value)?;
    220         Ok(ResourceRef::new(format, id))
    221     }
    222 
    223     /// Adds a resource from a URI, generating a [`ResourceRef`].
    224     ///
    225     /// The generated identifier may be different from the key.
    226     pub(crate) fn add_uri<R>(
    227         &mut self,
    228         uri: &str,
    229         format: &str,
    230         value: R,
    231     ) -> crate::Result<ResourceRef>
    232     where
    233         R: Into<Vec<u8>>,
    234     {
    235         #[cfg(feature = "file_io")]
    236         let mut id = uri.to_string();
    237         #[cfg(not(feature = "file_io"))]
    238         let id = uri.to_string();
    239 
    240         // if it isn't jumbf, assume it's an external uri and use it as is
    241         if id.starts_with("self#jumbf=") {
    242             #[cfg(feature = "file_io")]
    243             if self.base_path.is_some() {
    244                 // convert to a file path always including the manifest label
    245                 id = id.replace("self#jumbf=", "");
    246                 if id.starts_with("/c2pa/") {
    247                     id = id.replacen("/c2pa/", "", 1);
    248                 } else if let Some(label) = self.label.as_ref() {
    249                     id = format!("{}/{id}", label);
    250                 }
    251                 id = id.replace([':'], "_");
    252                 // add a file extension if it doesn't have one
    253                 if !(id.ends_with(".jpeg") || id.ends_with(".png")) {
    254                     if let Some(ext) = crate::utils::mime::format_to_extension(format) {
    255                         id = format!("{}.{}", id, ext);
    256                     }
    257                 }
    258             }
    259             if !self.exists(&id) {
    260                 self.add(&id, value)?;
    261             }
    262         }
    263         Ok(ResourceRef::new(format, id))
    264     }
    265 
    266     /// Adds a resource, using a given id value.
    267     pub fn add<S, R>(&mut self, id: S, value: R) -> crate::Result<&mut Self>
    268     where
    269         S: Into<String>,
    270         R: Into<Vec<u8>>,
    271     {
    272         #[cfg(feature = "file_io")]
    273         if let Some(base) = self.base_path.as_ref() {
    274             let path = base.join(id.into());
    275             create_dir_all(path.parent().unwrap_or(Path::new("")))?;
    276             write(path, value.into())?;
    277             return Ok(self);
    278         }
    279         self.resources.insert(id.into(), value.into());
    280         Ok(self)
    281     }
    282 
    283     /// Returns a [`HashMap`] of internal resources.
    284     pub const fn resources(&self) -> &HashMap<String, Vec<u8>> {
    285         &self.resources
    286     }
    287 
    288     /// Returns a copy on write reference to the resource if found.
    289     ///
    290     /// Returns [`Error::ResourceNotFound`] if it cannot find a resource matching that ID.
    291     pub fn get(&self, id: &str) -> Result<Cow<Vec<u8>>> {
    292         #[cfg(feature = "file_io")]
    293         if !self.resources.contains_key(id) {
    294             match self.base_path.as_ref() {
    295                 Some(base) => {
    296                     // read the file, save in Map and then return a reference
    297                     let path = base.join(id);
    298                     let value = read(path).map_err(|_| {
    299                         let path = base.join(id).to_string_lossy().into_owned();
    300                         Error::ResourceNotFound(path)
    301                     })?;
    302                     return Ok(Cow::Owned(value));
    303                 }
    304                 None => return Err(Error::ResourceNotFound(id.to_string())),
    305             }
    306         }
    307         self.resources.get(id).map_or_else(
    308             || Err(Error::ResourceNotFound(id.to_string())),
    309             |v| Ok(Cow::Borrowed(v)),
    310         )
    311     }
    312 
    313     pub fn write_stream(
    314         &self,
    315         id: &str,
    316         mut stream: impl Write + Read + Seek + Send,
    317     ) -> Result<u64> {
    318         #[cfg(feature = "file_io")]
    319         if !self.resources.contains_key(id) {
    320             match self.base_path.as_ref() {
    321                 Some(base) => {
    322                     // read from, the file to stream
    323                     let path = base.join(id);
    324                     let mut file = std::fs::File::open(path)?;
    325                     return std::io::copy(&mut file, &mut stream).map_err(Error::IoError);
    326                 }
    327                 None => return Err(Error::ResourceNotFound(id.to_string())),
    328             }
    329         }
    330         match self.resources().get(id) {
    331             Some(data) => {
    332                 stream.write_all(data).map_err(Error::IoError)?;
    333                 Ok(data.len() as u64)
    334             }
    335             None => Err(Error::ResourceNotFound(id.to_string())),
    336         }
    337     }
    338 
    339     /// Returns `true` if the resource has been added or exists as file.
    340     pub fn exists(&self, id: &str) -> bool {
    341         if !self.resources.contains_key(id) {
    342             #[cfg(feature = "file_io")]
    343             match self.base_path.as_ref() {
    344                 Some(base) => {
    345                     let path = base.join(id);
    346                     path.exists()
    347                 }
    348                 None => false,
    349             }
    350             #[cfg(not(feature = "file_io"))]
    351             false
    352         } else {
    353             true
    354         }
    355     }
    356 
    357     #[cfg(feature = "file_io")]
    358     // Returns the full path for an ID.
    359     pub fn path_for_id(&self, id: &str) -> Option<PathBuf> {
    360         self.base_path.as_ref().map(|base| base.join(id))
    361     }
    362 }
    363 
    364 impl Default for ResourceStore {
    365     fn default() -> Self {
    366         ResourceStore::new()
    367     }
    368 }
    369 
    370 #[cfg(feature = "unstable_api")]
    371 pub trait ResourceResolver {
    372     /// Read the data in a [`ResourceRef`][ResourceRef] via a stream.
    373     fn open(&self, reference: &ResourceRef) -> Result<Box<dyn CAIRead>>;
    374 }
    375 
    376 #[cfg(feature = "unstable_api")]
    377 impl ResourceResolver for ResourceStore {
    378     fn open(&self, reference: &ResourceRef) -> Result<Box<dyn CAIRead>> {
    379         let data = self.get(&reference.identifier)?.into_owned();
    380         let cursor = std::io::Cursor::new(data);
    381         Ok(Box::new(cursor))
    382     }
    383 }
    384 
    385 pub fn mime_from_uri(uri: &str) -> String {
    386     if let Some(label) = assertion_label_from_uri(uri) {
    387         if label.starts_with(labels::THUMBNAIL) {
    388             // https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_thumbnail
    389             if let Some(ext) = label.rsplit('.').next() {
    390                 return format!("image/{ext}");
    391             }
    392         }
    393     }
    394 
    395     // Unknown binary data.
    396     String::from("application/octet-stream")
    397 }
    398 
    399 #[cfg(test)]
    400 #[cfg(feature = "openssl_sign")]
    401 mod tests {
    402     #![allow(clippy::expect_used)]
    403     #![allow(clippy::unwrap_used)]
    404 
    405     use std::io::Cursor;
    406 
    407     use super::*;
    408     use crate::{utils::test::temp_signer, Builder, Reader};
    409 
    410     #[test]
    411     #[cfg(feature = "openssl_sign")]
    412     fn resource_store() {
    413         let mut c = ResourceStore::new();
    414         let value = b"my value";
    415         c.add("abc123.jpg", value.to_vec()).expect("add");
    416         let v = c.get("abc123.jpg").unwrap();
    417         assert_eq!(v.to_vec(), b"my value");
    418         c.add("cba321.jpg", value.to_vec()).expect("add");
    419         assert!(c.exists("cba321.jpg"));
    420         assert!(!c.exists("foo"));
    421 
    422         let json = r#"{
    423             "claim_generator": "test",
    424             "format" : "image/jpeg",
    425             "instance_id": "12345",
    426             "assertions": [],
    427             "thumbnail": {
    428                 "format": "image/jpeg",
    429                 "identifier": "abc123"
    430             },
    431             "ingredients": [{
    432                 "title": "A.jpg",
    433                 "format": "image/jpeg",
    434                 "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
    435                 "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
    436                 "relationship": "parentOf",
    437                 "thumbnail": {
    438                     "format": "image/jpeg",
    439                     "identifier": "cba321"
    440                 }
    441             }]
    442         }"#;
    443 
    444         let mut builder = Builder::from_json(json).expect("from json");
    445         builder
    446             .add_resource("abc123", &mut Cursor::new(value))
    447             .expect("add_resource");
    448         builder
    449             .add_resource("cba321", &mut Cursor::new(value))
    450             .expect("add_resource");
    451 
    452         let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg");
    453 
    454         let signer = temp_signer();
    455         // Embed a manifest using the signer.
    456         let mut output_image = Cursor::new(Vec::new());
    457         builder
    458             .sign(
    459                 &*signer,
    460                 "image/jpeg",
    461                 &mut Cursor::new(image),
    462                 &mut output_image,
    463             )
    464             .expect("sign");
    465 
    466         output_image.set_position(0);
    467         let reader = Reader::from_stream("jpeg", &mut output_image).expect("from_bytes");
    468         let _json = reader.json();
    469         println!("{_json}");
    470     }
    471 }