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 }