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 }