1#![deny(missing_docs)]
15#[cfg(feature = "file_io")]
16use std::path::{Path, PathBuf};
17use std::{borrow::Cow, io::Cursor};
18
19use async_generic::async_generic;
20use log::{debug, error};
21#[cfg(feature = "json_schema")]
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use uuid::Uuid;
25
26#[cfg(doc)]
27use crate::Manifest;
28use crate::{
29 assertion::{Assertion, AssertionBase},
30 assertions::{
31 self, labels, AssertionMetadata, AssetType, CertificateStatus, EmbeddedData, Relationship,
32 },
33 asset_io::CAIRead,
34 claim::{Claim, ClaimAssetData},
35 context::Context,
36 crypto::base64,
37 error::{Error, Result},
38 hashed_uri::HashedUri,
39 jumbf::{
40 self,
41 labels::{assertion_label_from_uri, manifest_label_from_uri},
42 },
43 log_item,
44 resource_store::{ResourceRef, ResourceStore},
45 status_tracker::StatusTracker,
46 store::Store,
47 utils::{
48 mime::{extension_to_mime, format_to_mime},
49 xmp_inmemory_utils::XmpInfo,
50 },
51 validation_results::ValidationResults,
52 validation_status::{self, ValidationStatus},
53};
54
55#[derive(Clone, Debug, Default, Deserialize, Serialize)]
56#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
57pub struct Ingredient {
59 #[serde(skip_serializing_if = "Option::is_none")]
61 title: Option<String>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 format: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 document_id: Option<String>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
74 instance_id: Option<String>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 provenance: Option<String>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
84 thumbnail: Option<ResourceRef>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
88 hash: Option<String>,
89
90 #[serde(default = "default_relationship")]
95 relationship: Relationship,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
105 active_manifest: Option<String>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 validation_status: Option<Vec<ValidationStatus>>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 validation_results: Option<ValidationResults>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 data: Option<ResourceRef>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 description: Option<String>,
122
123 #[serde(rename = "informational_URI", skip_serializing_if = "Option::is_none")]
125 informational_uri: Option<String>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
131 metadata: Option<AssertionMetadata>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 data_types: Option<Vec<AssetType>>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
141 manifest_data: Option<ResourceRef>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 label: Option<String>,
146
147 #[serde(skip)]
148 resources: ResourceStore,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
151 ocsp_responses: Option<Vec<ResourceRef>>,
152}
153
154fn default_instance_id() -> String {
155 format!("xmp:iid:{}", Uuid::new_v4())
156}
157
158fn default_relationship() -> Relationship {
159 Relationship::default()
160}
161
162impl Ingredient {
163 pub fn new<S>(title: S, format: S, instance_id: S) -> Self
178 where
179 S: Into<String>,
180 {
181 Self {
182 title: Some(title.into()),
183 format: Some(format.into()),
184 instance_id: Some(instance_id.into()),
185 ..Default::default()
186 }
187 }
188
189 pub fn new_v2<S1, S2>(title: S1, format: S2) -> Self
203 where
204 S1: Into<String>,
205 S2: Into<String>,
206 {
207 Self {
208 title: Some(title.into()),
209 format: Some(format.into()),
210 ..Default::default()
211 }
212 }
213
214 pub fn title(&self) -> Option<&str> {
226 self.title.as_deref()
227 }
228
229 pub fn label(&self) -> Option<&str> {
231 self.label.as_deref()
232 }
233
234 pub fn format(&self) -> Option<&str> {
236 self.format.as_deref()
237 }
238
239 pub fn document_id(&self) -> Option<&str> {
241 self.document_id.as_deref()
242 }
243
244 pub fn instance_id(&self) -> &str {
248 self.instance_id.as_deref().unwrap_or("None") }
250
251 pub fn provenance(&self) -> Option<&str> {
253 self.provenance.as_deref()
254 }
255
256 pub fn thumbnail_ref(&self) -> Option<&ResourceRef> {
258 self.thumbnail.as_ref()
259 }
260
261 pub fn thumbnail(&self) -> Option<(&str, Cow<'_, Vec<u8>>)> {
263 self.thumbnail
264 .as_ref()
265 .and_then(|t| Some(t.format.as_str()).zip(self.resources.get(&t.identifier).ok()))
266 }
267
268 pub fn thumbnail_bytes(&self) -> Result<Cow<'_, Vec<u8>>> {
270 match self.thumbnail.as_ref() {
271 Some(thumbnail) => self.resources.get(&thumbnail.identifier),
272 None => Err(Error::NotFound),
273 }
274 }
275
276 pub fn hash(&self) -> Option<&str> {
278 self.hash.as_deref()
279 }
280
281 pub fn is_parent(&self) -> bool {
283 self.relationship == Relationship::ParentOf
284 }
285
286 pub fn relationship(&self) -> &Relationship {
288 &self.relationship
289 }
290
291 pub fn validation_status(&self) -> Option<&[ValidationStatus]> {
293 self.validation_status.as_deref()
294 }
295
296 pub fn validation_results(&self) -> Option<&ValidationResults> {
298 self.validation_results.as_ref()
299 }
300
301 pub fn metadata(&self) -> Option<&AssertionMetadata> {
303 self.metadata.as_ref()
304 }
305
306 pub fn active_manifest(&self) -> Option<&str> {
311 self.active_manifest.as_deref()
312 }
313
314 pub fn manifest_data_ref(&self) -> Option<&ResourceRef> {
318 self.manifest_data.as_ref()
319 }
320
321 pub fn manifest_data(&self) -> Option<Cow<'_, Vec<u8>>> {
325 self.manifest_data
326 .as_ref()
327 .and_then(|r| self.resources.get(&r.identifier).ok())
328 }
329
330 pub fn data_ref(&self) -> Option<&ResourceRef> {
332 self.data.as_ref()
333 }
334
335 pub(crate) fn ocsp_responses_ref(&self) -> Option<&Vec<ResourceRef>> {
337 self.ocsp_responses.as_ref()
338 }
339
340 pub fn description(&self) -> Option<&str> {
342 self.description.as_deref()
343 }
344
345 pub fn informational_uri(&self) -> Option<&str> {
347 self.informational_uri.as_deref()
348 }
349
350 pub fn data_types(&self) -> Option<&[AssetType]> {
352 self.data_types.as_deref()
353 }
354
355 pub fn set_title<S: Into<String>>(&mut self, title: S) -> &mut Self {
357 self.title = Some(title.into());
358 self
359 }
360
361 pub fn set_instance_id<S: Into<String>>(&mut self, instance_id: S) -> &mut Self {
367 self.instance_id = Some(instance_id.into());
368 self
369 }
370
371 pub fn set_document_id<S: Into<String>>(&mut self, document_id: S) -> &mut Self {
377 self.document_id = Some(document_id.into());
378 self
379 }
380
381 pub fn set_provenance<S: Into<String>>(&mut self, provenance: S) -> &mut Self {
387 self.provenance = Some(provenance.into());
388 self
389 }
390
391 pub fn set_is_parent(&mut self) -> &mut Self {
396 self.relationship = Relationship::ParentOf;
397 self
398 }
399
400 pub fn set_relationship(&mut self, relationship: Relationship) -> &mut Self {
405 self.relationship = relationship;
406 self
407 }
408
409 pub fn set_thumbnail_ref(&mut self, thumbnail: ResourceRef) -> Result<&mut Self> {
411 self.thumbnail = Some(thumbnail);
412 Ok(self)
413 }
414
415 pub fn set_thumbnail<S: Into<String>, B: Into<Vec<u8>>>(
417 &mut self,
418 format: S,
419 bytes: B,
420 ) -> Result<&mut Self> {
421 let base_id = self.instance_id().to_string();
422 self.thumbnail = Some(self.resources.add_with(&base_id, &format.into(), bytes)?);
423 Ok(self)
424 }
425
426 #[deprecated(note = "Please use set_thumbnail instead", since = "0.28.0")]
432 pub fn set_memory_thumbnail<S: Into<String>, B: Into<Vec<u8>>>(
433 &mut self,
434 format: S,
435 bytes: B,
436 ) -> Result<&mut Self> {
437 #[cfg(feature = "file_io")]
439 let base_path = self.resources_mut().take_base_path();
440 let base_id = self.instance_id().to_string();
441 self.thumbnail = Some(self.resources.add_with(&base_id, &format.into(), bytes)?);
442 #[cfg(feature = "file_io")]
443 if let Some(path) = base_path {
444 self.resources_mut().set_base_path(path)
445 }
446 Ok(self)
447 }
448
449 pub fn set_hash<S: Into<String>>(&mut self, hash: S) -> &mut Self {
451 self.hash = Some(hash.into());
452 self
453 }
454
455 pub fn add_validation_status(&mut self, status: ValidationStatus) -> &mut Self {
457 match &mut self.validation_status {
458 None => self.validation_status = Some(vec![status]),
459 Some(validation_status) => validation_status.push(status),
460 }
461 self
462 }
463
464 pub fn set_metadata(&mut self, metadata: AssertionMetadata) -> &mut Self {
466 self.metadata = Some(metadata);
467 self
468 }
469
470 pub fn set_active_manifest<S: Into<String>>(&mut self, label: S) -> &mut Self {
472 self.active_manifest = Some(label.into());
473 self
474 }
475
476 pub fn set_manifest_data_ref(&mut self, data_ref: ResourceRef) -> Result<&mut Self> {
478 self.manifest_data = Some(data_ref);
479 Ok(self)
480 }
481
482 pub fn set_manifest_data(&mut self, data: Vec<u8>) -> Result<&mut Self> {
484 let base_id = "manifest_data".to_string();
485 self.manifest_data = Some(
486 self.resources
487 .add_with(&base_id, "application/c2pa", data)?,
488 );
489 Ok(self)
490 }
491
492 pub fn set_data_ref(&mut self, data_ref: ResourceRef) -> Result<&mut Self> {
494 if !self.resources.exists(&data_ref.identifier) {
496 return Err(Error::NotFound);
497 };
498 self.data = Some(data_ref);
499 Ok(self)
500 }
501
502 pub fn set_description<S: Into<String>>(&mut self, description: S) -> &mut Self {
504 self.description = Some(description.into());
505 self
506 }
507
508 pub fn set_informational_uri<S: Into<String>>(&mut self, uri: S) -> &mut Self {
510 self.informational_uri = Some(uri.into());
511 self
512 }
513
514 pub fn add_data_type(&mut self, data_type: AssetType) -> &mut Self {
516 if let Some(data_types) = self.data_types.as_mut() {
517 data_types.push(data_type);
518 } else {
519 self.data_types = Some([data_type].to_vec());
520 }
521
522 self
523 }
524
525 #[doc(hidden)]
527 pub fn resources(&self) -> &ResourceStore {
528 &self.resources
529 }
530
531 #[doc(hidden)]
533 pub fn resources_mut(&mut self) -> &mut ResourceStore {
534 &mut self.resources
535 }
536
537 #[cfg(feature = "file_io")]
539 fn get_path_info(path: &std::path::Path) -> (String, String, String) {
540 let title = path
541 .file_name()
542 .map(|name| name.to_string_lossy().into_owned())
543 .unwrap_or_else(|| "".into());
544
545 let extension = path
546 .extension()
547 .map(|e| e.to_string_lossy().into_owned())
548 .unwrap_or_else(|| "".into())
549 .to_lowercase();
550
551 let format = extension_to_mime(&extension)
552 .unwrap_or("application/octet-stream")
553 .to_owned();
554 (title, extension, format)
555 }
556
557 #[cfg(feature = "file_io")]
562 pub fn from_file_info<P: AsRef<Path>>(path: P) -> Self {
563 let (title, _, format) = Self::get_path_info(path.as_ref());
565
566 match std::fs::File::open(path).map_err(Error::IoError) {
568 Ok(mut file) => Self::from_stream_info(&mut file, &format, &title),
569 Err(_) => Self {
570 title: Some(title),
571 format: Some(format),
572 ..Default::default()
573 },
574 }
575 }
576
577 pub fn from_stream_info<F, S>(stream: &mut dyn CAIRead, format: F, title: S) -> Self
579 where
580 F: Into<String>,
581 S: Into<String>,
582 {
583 let format = format.into();
584
585 let xmp_info = XmpInfo::from_source(stream, &format);
587
588 let id = if let Some(id) = xmp_info.instance_id {
589 id
590 } else {
591 default_instance_id()
592 };
593
594 let mut ingredient = Self::new(title.into(), format, id);
595
596 ingredient.document_id = xmp_info.document_id; ingredient.provenance = xmp_info.provenance;
598
599 ingredient
600 }
601
602 fn update_validation_status(
605 &mut self,
606 result: Result<Store>,
607 manifest_bytes: Option<Vec<u8>>,
608 validation_log: &StatusTracker,
609 ) -> Result<()> {
610 match result {
611 Ok(store) => {
612 let validation_results = ValidationResults::from_store(&store, validation_log);
614
615 if let Some(claim) = store.provenance_claim() {
616 if validation_results
618 .active_manifest()
619 .is_some_and(|m| m.failure().is_empty())
620 {
621 if let Some(hashed_uri) = claim
622 .assertions()
623 .iter()
624 .find(|hashed_uri| hashed_uri.url().contains(labels::CLAIM_THUMBNAIL))
625 {
626 let thumb_manifest = manifest_label_from_uri(&hashed_uri.url())
628 .unwrap_or_else(|| claim.label().to_string());
629 let uri =
630 jumbf::labels::to_absolute_uri(&thumb_manifest, &hashed_uri.url());
631 let format = hashed_uri
633 .url()
634 .rsplit_once('.')
635 .and_then(|(_, ext)| extension_to_mime(ext))
636 .unwrap_or("image/jpeg"); let mut thumb = crate::resource_store::ResourceRef::new(format, &uri);
638 thumb.alg = hashed_uri.alg();
640 let hash = base64::encode(&hashed_uri.hash());
641 thumb.hash = Some(hash);
642 self.set_thumbnail_ref(thumb)?;
643
644 let claim_assertion = store.get_claim_assertion_from_uri(&uri)?;
648 let thumbnail =
649 EmbeddedData::from_assertion(claim_assertion.assertion())?;
650 self.resources.add_uri(
651 &uri,
652 &thumbnail.content_type,
653 thumbnail.data,
654 )?;
655 }
656 }
657 self.active_manifest = Some(claim.label().to_string());
658 }
659
660 if let Some(bytes) = manifest_bytes {
661 self.set_manifest_data(bytes)?;
662 }
663
664 self.validation_status = validation_results.validation_errors();
665 self.validation_results = Some(validation_results);
666
667 Ok(())
668 }
669 Err(Error::JumbfNotFound)
670 | Err(Error::ProvenanceMissing)
671 | Err(Error::UnsupportedType) => Ok(()), Err(Error::BadParam(desc)) if desc == *"unrecognized file type" => Ok(()),
673 Err(Error::RemoteManifestUrl(url)) | Err(Error::RemoteManifestFetch(url)) => {
674 let status =
675 ValidationStatus::new_failure(validation_status::MANIFEST_INACCESSIBLE)
676 .set_url(url)
677 .set_explanation("Remote manifest not fetched".to_string());
678 let mut validation_results = ValidationResults::default();
679 validation_results.add_status(status.clone());
680 self.validation_results = Some(validation_results);
681 self.validation_status = Some(vec![status]);
682 Ok(())
683 }
684 Err(e) => {
685 debug!("ingredient {e:?}");
687
688 let mut results = ValidationResults::default();
689 let statuses: Vec<ValidationStatus> = validation_log
691 .logged_items()
692 .iter()
693 .filter_map(ValidationStatus::from_log_item)
694 .collect();
695
696 for status in statuses {
697 results.add_status(status.clone());
698 }
699 self.validation_status = results.validation_errors();
700 self.validation_results = Some(results);
701 Ok(())
702 }
703 }
704 }
705
706 #[cfg(feature = "file_io")]
707 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
709 Self::from_file_with_options(path.as_ref(), &DefaultOptions { base: None })
710 }
711
712 #[cfg(feature = "file_io")]
713 pub fn from_file_with_folder<P: AsRef<Path>>(path: P, folder: P) -> Result<Self> {
715 Self::from_file_with_options(
716 path.as_ref(),
717 &DefaultOptions {
718 base: Some(PathBuf::from(folder.as_ref())),
719 },
720 )
721 }
722
723 fn thumbnail_from_assertion(assertion: &Assertion) -> (&str, &[u8]) {
725 (assertion.content_type(), assertion.data())
726 }
727
728 #[cfg(feature = "file_io")]
730 pub fn from_file_with_options<P: AsRef<Path>>(
731 path: P,
732 options: &dyn IngredientOptions,
733 ) -> Result<Self> {
734 let settings = crate::settings::get_thread_local_settings();
736 let context = Context::new().with_settings(settings)?;
737 Self::from_file_impl(path.as_ref(), options, &context)
738 }
739
740 #[cfg(feature = "file_io")]
742 fn from_file_impl(
743 path: &Path,
744 options: &dyn IngredientOptions,
745 context: &Context,
746 ) -> Result<Self> {
747 #[cfg(feature = "diagnostics")]
748 let _t = crate::utils::time_it::TimeIt::new("Ingredient:from_file_with_options");
749
750 debug!("ingredient {path:?}");
752
753 let mut ingredient = Self::from_file_info(path);
755
756 if !path.exists() {
757 return Err(Error::FileNotFound(ingredient.title.unwrap_or_default()));
758 }
759
760 if let Some(folder) = options.base_path().as_ref() {
762 ingredient.with_base_path(folder)?;
763 }
764
765 if let Some(opt_title) = options.title(path) {
767 ingredient.title = Some(opt_title);
768 }
769
770 ingredient.hash = options.hash(path);
772
773 let mut validation_log = StatusTracker::default();
774
775 let (result, manifest_bytes) = match Store::load_jumbf_from_path(path, context) {
777 Ok(manifest_bytes) => {
778 (
779 Store::from_jumbf_with_context(&manifest_bytes, &mut validation_log, context)
781 .and_then(|mut store| {
782 store
784 .verify_from_path(path, &mut validation_log, context)
785 .map(|_| store)
786 })
787 .inspect_err(|e| {
788 log_item!("asset", "error loading file", "Ingredient::from_file")
790 .failure_no_throw(&mut validation_log, e);
791 }),
792 Some(manifest_bytes),
793 )
794 }
795 Err(err) => (Err(err), None),
796 };
797
798 ingredient.update_validation_status(result, manifest_bytes, &validation_log)?;
800
801 if ingredient.thumbnail.is_none() {
803 if let Some((format, image)) = options.thumbnail(path) {
804 ingredient.set_thumbnail(format, image)?;
805 } else {
806 #[cfg(feature = "add_thumbnails")]
807 if let Some(format) = crate::format_from_path(path) {
808 ingredient.maybe_add_thumbnail(
809 &format,
810 &mut std::io::BufReader::new(std::fs::File::open(path)?),
811 context,
812 )?;
813 }
814 }
815 }
816 Ok(ingredient)
817 }
818
819 pub fn from_memory(format: &str, buffer: &[u8]) -> Result<Self> {
824 let mut stream = Cursor::new(buffer);
825 Self::from_stream(format, &mut stream)
826 }
827
828 pub fn from_stream(format: &str, stream: &mut dyn CAIRead) -> Result<Self> {
833 let settings = crate::settings::get_thread_local_settings();
835 let context = Context::new().with_settings(settings)?;
836 let ingredient = Self::from_stream_info(stream, format, "untitled");
837 stream.rewind()?;
838 ingredient.add_stream_internal(format, stream, &context)
839 }
840
841 pub fn from_json(json: &str) -> Result<Self> {
843 serde_json::from_str(json).map_err(Error::JsonError)
844 }
845
846 #[async_generic]
854 pub(crate) fn with_stream<S: Into<String>>(
855 mut self,
856 format: S,
857 stream: &mut dyn CAIRead,
858 context: &Context,
859 ) -> Result<Self> {
860 let format = format.into();
861
862 let xmp_info = XmpInfo::from_source(stream, &format);
864
865 if self.instance_id.is_none() {
866 self.instance_id = xmp_info.instance_id;
867 }
868
869 if let Some(id) = xmp_info.document_id {
870 self.document_id = Some(id);
871 };
872
873 if let Some(provenance) = xmp_info.provenance {
874 self.provenance = Some(provenance);
875 };
876
877 if self.format.is_none() {
879 self.format = Some(format.to_string());
880 };
881
882 if self.instance_id.is_none() {
884 self.instance_id = Some(default_instance_id());
885 };
886
887 stream.rewind()?;
888
889 if _sync {
890 self.add_stream_internal(&format, stream, context)
891 } else {
892 self.add_stream_internal_async(&format, stream, context)
893 .await
894 }
895 }
896
897 #[async_generic]
899 fn add_stream_internal(
900 mut self,
901 format: &str,
902 stream: &mut dyn CAIRead,
903 context: &Context,
904 ) -> Result<Self> {
905 let mut validation_log = StatusTracker::default();
906
907 let jumbf_result = match self.manifest_data() {
909 Some(data) => Ok(data.into_owned()),
910 None => if _sync {
911 Store::load_jumbf_from_stream(format, stream, context)
912 } else {
913 Store::load_jumbf_from_stream_async(format, stream, context).await
914 }
915 .map(|(manifest_bytes, _)| manifest_bytes),
916 };
917
918 let (mut result, manifest_bytes) = match jumbf_result {
920 Ok(manifest_bytes) => {
921 let result = if _sync {
922 Store::from_manifest_data_and_stream(
923 &manifest_bytes,
924 format,
925 &mut *stream,
926 &mut validation_log,
927 context,
928 )
929 } else {
930 Store::from_manifest_data_and_stream_async(
931 &manifest_bytes,
932 format,
933 &mut *stream,
934 &mut validation_log,
935 context,
936 )
937 .await
938 };
939 (result, Some(manifest_bytes))
940 }
941 Err(err) => (Err(err), None),
942 };
943
944 if let Ok(ref mut store) = result {
946 let labels = store.get_manifest_labels_for_ocsp(context.settings());
947
948 let ocsp_response_ders = if _sync {
949 store.get_ocsp_response_ders(labels, &mut validation_log, context)?
950 } else {
951 store
952 .get_ocsp_response_ders_async(labels, &mut validation_log, context)
953 .await?
954 };
955
956 let resource_refs: Vec<ResourceRef> = ocsp_response_ders
957 .into_iter()
958 .filter_map(|o| self.resources.add_with(&o.0, "ocsp", o.1).ok())
959 .collect();
960
961 self.ocsp_responses = Some(resource_refs);
962 }
963
964 self.update_validation_status(result, manifest_bytes, &validation_log)?;
966
967 #[cfg(feature = "add_thumbnails")]
969 self.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), context)?;
970
971 Ok(self)
972 }
973
974 pub async fn from_memory_async(format: &str, buffer: &[u8]) -> Result<Self> {
979 let mut stream = Cursor::new(buffer);
980 Self::from_stream_async(format, &mut stream).await
981 }
982
983 pub async fn from_stream_async(format: &str, stream: &mut dyn CAIRead) -> Result<Self> {
988 let settings = crate::settings::get_thread_local_settings();
990 let context = Context::new().with_settings(settings)?;
991 Self::from_stream_async_with_settings(format, stream, &context).await
992 }
993
994 pub(crate) async fn from_stream_async_with_settings(
995 format: &str,
996 stream: &mut dyn CAIRead,
997 context: &Context,
998 ) -> Result<Self> {
999 let mut ingredient = Self::from_stream_info(stream, format, "untitled");
1000 stream.rewind()?;
1001
1002 let mut validation_log = StatusTracker::default();
1003
1004 let (result, manifest_bytes) =
1006 match Store::load_jumbf_from_stream_async(format, stream, context).await {
1007 Ok((manifest_bytes, _)) => {
1008 (
1009 match Store::from_jumbf_with_context(
1011 &manifest_bytes,
1012 &mut validation_log,
1013 context,
1014 ) {
1015 Ok(store) => {
1016 Store::verify_store_async(
1018 &store,
1019 &mut ClaimAssetData::Stream(stream, format),
1020 &mut validation_log,
1021 context,
1022 )
1023 .await
1024 .map(|_| store)
1025 }
1026 Err(e) => {
1027 log_item!(
1028 "asset",
1029 "error loading asset",
1030 "Ingredient::from_stream_async"
1031 )
1032 .failure_no_throw(&mut validation_log, &e);
1033
1034 Err(e)
1035 }
1036 },
1037 Some(manifest_bytes),
1038 )
1039 }
1040 Err(err) => (Err(err), None),
1041 };
1042
1043 ingredient.update_validation_status(result, manifest_bytes, &validation_log)?;
1045
1046 #[cfg(feature = "add_thumbnails")]
1048 ingredient.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), context)?;
1049
1050 Ok(ingredient)
1051 }
1052
1053 pub(crate) fn from_ingredient_uri(
1056 store: &Store,
1057 claim_label: &str,
1058 ingredient_uri: &str,
1059 #[cfg(feature = "file_io")] resource_path: Option<&Path>,
1060 ) -> Result<Self> {
1061 let assertion =
1062 store
1063 .get_assertion_from_uri(ingredient_uri)
1064 .ok_or(Error::AssertionMissing {
1065 url: ingredient_uri.to_owned(),
1066 })?;
1067 let ingredient_assertion = assertions::Ingredient::from_assertion(assertion)?;
1068 let mut validation_status = match ingredient_assertion.validation_status.as_ref() {
1069 Some(status) => status.clone(),
1070 None => Vec::new(),
1071 };
1072
1073 let active_manifest = ingredient_assertion
1075 .c2pa_manifest()
1076 .and_then(|hash_url| manifest_label_from_uri(&hash_url.url()));
1077
1078 debug!(
1079 "Adding Ingredient {:?} {:?}",
1080 ingredient_assertion.title, &active_manifest
1081 );
1082
1083 let label = assertion_label_from_uri(ingredient_uri);
1085 let mut ingredient = Ingredient {
1086 title: ingredient_assertion.title,
1087 format: ingredient_assertion.format,
1088 instance_id: ingredient_assertion.instance_id,
1089 document_id: ingredient_assertion.document_id,
1090 relationship: ingredient_assertion.relationship,
1091 active_manifest,
1092 validation_results: ingredient_assertion.validation_results,
1093 metadata: ingredient_assertion.metadata,
1094 description: ingredient_assertion.description,
1095 informational_uri: ingredient_assertion.informational_uri,
1096 data_types: ingredient_assertion.data_types,
1097 label,
1098 ..Default::default()
1099 };
1100
1101 ingredient.resources.set_label(claim_label); #[cfg(feature = "file_io")]
1104 if let Some(base_path) = resource_path {
1105 ingredient.resources_mut().set_base_path(base_path)
1106 }
1107
1108 if let Some(hashed_uri) = ingredient_assertion.thumbnail.as_ref() {
1110 let target_claim_label = match manifest_label_from_uri(&hashed_uri.url()) {
1112 Some(label) => label, None => claim_label.to_owned(), };
1115 let maybe_resource_ref = match hashed_uri.url() {
1116 uri if uri.contains(jumbf::labels::ASSERTIONS) => {
1117 store
1120 .get_assertion_from_uri_and_claim(&hashed_uri.url(), &target_claim_label)
1121 .map(|assertion| {
1122 let (format, image) = Self::thumbnail_from_assertion(assertion);
1123 ingredient
1124 .resources
1125 .add_uri(&hashed_uri.url(), format, image)
1126 })
1127 }
1128 uri if uri.contains(jumbf::labels::DATABOXES) => store
1129 .get_data_box_from_uri_and_claim(hashed_uri, &target_claim_label)
1130 .map(|data_box| {
1131 ingredient.resources.add_uri(
1132 &hashed_uri.url(),
1133 &data_box.format,
1134 data_box.data.clone(),
1135 )
1136 }),
1137 _ => None,
1138 };
1139 match maybe_resource_ref {
1140 Some(data_ref) => {
1141 ingredient.thumbnail = Some(data_ref?);
1142 }
1143 None => {
1144 error!("failed to get {} from {}", hashed_uri.url(), ingredient_uri);
1145 validation_status.push(
1146 ValidationStatus::new_failure(
1147 validation_status::ASSERTION_MISSING.to_string(),
1148 )
1149 .set_url(hashed_uri.url()),
1150 );
1151 }
1152 }
1153 };
1154
1155 if let Some(data_uri) = ingredient_assertion.data.as_ref() {
1157 let maybe_data_ref = match data_uri.url() {
1158 uri if uri.contains(jumbf::labels::ASSERTIONS) => {
1159 store
1161 .get_assertion_from_uri_and_claim(&uri, claim_label)
1162 .map(|assertion| {
1163 let embedded_data = EmbeddedData::from_assertion(assertion)?;
1164 ingredient.resources.add_uri(
1165 &data_uri.url(),
1166 &embedded_data.content_type,
1167 embedded_data.data,
1168 )
1169 })
1170 }
1171 uri if uri.contains(jumbf::labels::DATABOXES) => store
1172 .get_data_box_from_uri_and_claim(data_uri, claim_label)
1173 .map(|data_box| {
1174 ingredient
1175 .resources
1176 .add_uri(&uri, &data_box.format, data_box.data.clone())
1177 }),
1178 _ => None,
1179 };
1180 match maybe_data_ref {
1181 Some(data_ref) => {
1182 ingredient.data = Some(data_ref?);
1183 }
1184 None => {
1185 error!("failed to get {} from {}", data_uri.url(), ingredient_uri);
1186 validation_status.push(
1187 ValidationStatus::new_failure(
1188 validation_status::ASSERTION_MISSING.to_string(),
1189 )
1190 .set_url(data_uri.url()),
1191 );
1192 }
1193 }
1194 };
1195
1196 if !validation_status.is_empty() {
1197 ingredient.validation_status = Some(validation_status)
1198 }
1199 Ok(ingredient)
1200 }
1201
1202 pub(crate) fn add_to_claim(
1204 &self,
1205 claim: &mut Claim,
1206 redactions: Option<Vec<String>>,
1207 resources: Option<&ResourceStore>, context: &Context,
1209 ) -> Result<HashedUri> {
1210 let mut thumbnail = None;
1211 let get_resource = |id: &str| {
1213 self.resources.get(id).or_else(|_| {
1214 resources
1215 .ok_or_else(|| Error::NotFound)
1216 .and_then(|r| r.get(id))
1217 })
1218 };
1219
1220 let (active_manifest, claim_signature) = match self.manifest_data_ref() {
1223 Some(resource_ref) => {
1224 let manifest_data = get_resource(&resource_ref.identifier)?;
1226
1227 let ingredient_store =
1229 Store::load_ingredient_to_claim(claim, &manifest_data, redactions, context)?;
1230
1231 let ingredient_active_claim = ingredient_store
1232 .provenance_claim()
1233 .ok_or(Error::JumbfNotFound)?;
1234
1235 let manifest_label = ingredient_active_claim.label();
1236 let hash = ingredient_store
1239 .get_manifest_box_hashes(ingredient_active_claim)
1240 .manifest_box_hash; let sig_hash = ingredient_store
1242 .get_manifest_box_hashes(ingredient_active_claim)
1243 .signature_box_hash; let uri = jumbf::labels::to_manifest_uri(manifest_label);
1246 let signature_uri = jumbf::labels::to_signature_uri(manifest_label);
1247
1248 if let Some(validation_results) = self.validation_results() {
1250 if validation_results.validation_state() != crate::ValidationState::Invalid {
1251 thumbnail = ingredient_active_claim
1252 .assertions()
1253 .iter()
1254 .find(|hashed_uri| hashed_uri.url().contains(labels::CLAIM_THUMBNAIL))
1255 .map(|t| {
1256 let url = jumbf::labels::to_absolute_uri(manifest_label, &t.url());
1259 HashedUri::new(url, t.alg(), &t.hash())
1260 });
1261 }
1262 }
1263 (
1265 Some(crate::hashed_uri::HashedUri::new(
1266 uri,
1267 Some(ingredient_active_claim.alg().to_owned()),
1268 hash.as_ref(),
1269 )),
1270 Some(crate::hashed_uri::HashedUri::new(
1271 signature_uri,
1272 Some(ingredient_active_claim.alg().to_owned()),
1273 sig_hash.as_ref(),
1274 )),
1275 )
1276 }
1277 None => (None, None),
1278 };
1279
1280 if let Some(thumb_ref) = self.thumbnail_ref() {
1283 let hash_url = match thumb_ref.hash.as_ref() {
1285 Some(h) => {
1286 let hash = base64::decode(h)
1287 .map_err(|_e| Error::BadParam("Invalid hash".to_string()))?;
1288 HashedUri::new(thumb_ref.identifier.clone(), thumb_ref.alg.clone(), &hash)
1289 }
1290 None => {
1291 let data = get_resource(&thumb_ref.identifier)?;
1293 if claim.version() < 2 {
1294 claim.add_databox(
1295 &thumb_ref.format,
1296 data.into_owned(),
1297 thumb_ref.data_types.clone(),
1298 )?
1299 } else {
1300 let thumbnail = EmbeddedData::new(
1302 labels::INGREDIENT_THUMBNAIL,
1303 format_to_mime(&thumb_ref.format),
1304 data.into_owned(),
1305 );
1306 claim.add_assertion(&thumbnail)?
1307 }
1308 }
1309 };
1310 thumbnail = Some(hash_url);
1311 }
1312
1313 let mut data = None;
1315 if let Some(data_ref) = self.data_ref() {
1316 let box_data = get_resource(&data_ref.identifier)?;
1317 let hash_uri = match claim.version() {
1318 1 => claim.add_databox(
1319 &data_ref.format,
1320 box_data.into_owned(),
1321 data_ref.data_types.clone(),
1322 )?,
1323 _ => {
1324 let embedded_data = EmbeddedData::new(
1325 labels::EMBEDDED_DATA,
1326 format_to_mime(&data_ref.format),
1327 box_data.into_owned(),
1328 );
1329 claim.add_assertion(&embedded_data)?
1330 }
1331 };
1332
1333 data = Some(hash_uri);
1334 };
1335
1336 if let Some(ocsp_responses_ref) = self.ocsp_responses_ref() {
1338 let ocsp_responses: Vec<Vec<u8>> = ocsp_responses_ref
1339 .iter()
1340 .filter_map(|i| get_resource(&i.identifier).ok())
1341 .map(|cow| cow.into_owned())
1342 .collect();
1343 if !ocsp_responses.is_empty() {
1344 let certificate_status =
1345 if let Some(assertion) = claim.get_assertion(CertificateStatus::LABEL, 0) {
1346 let certificate_status = CertificateStatus::from_assertion(assertion)?;
1347 certificate_status.add_ocsp_vals(ocsp_responses)
1348 } else {
1349 CertificateStatus::new(ocsp_responses)
1350 };
1351 claim.add_assertion(&certificate_status)?;
1352 }
1353 }
1354
1355 let mut ingredient_assertion = match claim.version() {
1356 1 => {
1357 assertions::Ingredient::new_v2(
1359 self.title().unwrap_or_default(),
1360 self.format().unwrap_or_default(),
1361 )
1362 }
1363 2 => {
1364 let mut assertion = assertions::Ingredient::new_v3(self.relationship.clone());
1365 assertion.title = self.title.clone();
1366 assertion.format = self.format.clone();
1367 assertion
1368 }
1369 _ => return Err(Error::ClaimVersion),
1370 };
1371 ingredient_assertion.instance_id = self.instance_id.clone();
1372 match claim.version() {
1373 1 => {
1374 ingredient_assertion.document_id = self.document_id.clone();
1375 ingredient_assertion.c2pa_manifest = active_manifest;
1376 ingredient_assertion
1377 .validation_status
1378 .clone_from(&self.validation_status);
1379 }
1380 2 => {
1381 ingredient_assertion.active_manifest = active_manifest;
1382 ingredient_assertion.claim_signature = claim_signature;
1383 ingredient_assertion.validation_results = self.validation_results.clone();
1384 }
1385 _ => {}
1386 }
1387 ingredient_assertion.relationship = self.relationship.clone();
1388 ingredient_assertion.thumbnail = thumbnail;
1389 ingredient_assertion.metadata.clone_from(&self.metadata);
1390 ingredient_assertion.data = data;
1391 ingredient_assertion
1392 .description
1393 .clone_from(&self.description);
1394 ingredient_assertion
1395 .informational_uri
1396 .clone_from(&self.informational_uri);
1397 ingredient_assertion.data_types.clone_from(&self.data_types);
1398 claim.add_assertion(&ingredient_assertion)
1399 }
1400
1401 #[cfg(feature = "file_io")]
1405 pub fn with_base_path<P: AsRef<Path>>(&mut self, base_path: P) -> Result<&Self> {
1406 std::fs::create_dir_all(&base_path)?;
1407 self.resources.set_base_path(base_path.as_ref());
1408 Ok(self)
1409 }
1410
1411 pub async fn from_manifest_and_asset_bytes_async<M: Into<Vec<u8>>>(
1433 manifest_bytes: M,
1434 format: &str,
1435 asset_bytes: &[u8],
1436 ) -> Result<Self> {
1437 let mut stream = Cursor::new(asset_bytes);
1438 Self::from_manifest_and_asset_stream_async(manifest_bytes, format, &mut stream).await
1439 }
1440
1441 pub async fn from_manifest_and_asset_stream_async<M: Into<Vec<u8>>>(
1443 manifest_bytes: M,
1444 format: &str,
1445 stream: &mut dyn CAIRead,
1446 ) -> Result<Self> {
1447 let settings = crate::settings::get_thread_local_settings();
1449 let context = Context::new().with_settings(settings)?;
1450 let mut ingredient = Self::from_stream_info(stream, format, "untitled");
1451
1452 let mut validation_log = StatusTracker::default();
1453
1454 let manifest_bytes: Vec<u8> = manifest_bytes.into();
1455 let result =
1457 match Store::from_jumbf_with_context(&manifest_bytes, &mut validation_log, &context) {
1458 Ok(store) => {
1459 stream.rewind()?;
1461
1462 Store::verify_store_async(
1463 &store,
1464 &mut ClaimAssetData::Stream(stream, format),
1465 &mut validation_log,
1466 &context,
1467 )
1468 .await
1469 .map(|_| store)
1470 }
1471 Err(e) => {
1472 log_item!("asset", "error loading file", "Ingredient::from_file")
1474 .failure_no_throw(&mut validation_log, &e);
1475
1476 Err(e)
1477 }
1478 };
1479
1480 ingredient.update_validation_status(result, Some(manifest_bytes), &validation_log)?;
1482
1483 #[cfg(feature = "add_thumbnails")]
1485 ingredient.maybe_add_thumbnail(format, &mut std::io::BufReader::new(stream), &context)?;
1486
1487 Ok(ingredient)
1488 }
1489
1490 #[cfg(feature = "add_thumbnails")]
1495 pub(crate) fn maybe_add_thumbnail<R>(
1496 &mut self,
1497 format: &str,
1498 stream: &mut R,
1499 context: &Context,
1500 ) -> Result<()>
1501 where
1502 R: std::io::BufRead + std::io::Seek,
1503 {
1504 let settings = context.settings();
1505 let auto_thumbnail = settings.builder.thumbnail.enabled;
1506
1507 if self.thumbnail.is_none() && auto_thumbnail {
1508 stream.rewind()?;
1509
1510 if let Some((output_format, image)) =
1511 crate::utils::thumbnail::make_thumbnail_bytes_from_stream(format, stream, settings)?
1512 {
1513 self.set_thumbnail(output_format.to_string(), image)?;
1514 }
1515 }
1516
1517 Ok(())
1518 }
1519
1520 pub(crate) fn merge(&mut self, other: &Ingredient) {
1522 self.relationship = other.relationship.clone();
1524
1525 if let Some(title) = &other.title {
1526 self.title = Some(title.clone());
1527 }
1528 if let Some(format) = &other.format {
1529 self.format = Some(format.clone());
1530 }
1531 if let Some(instance_id) = &other.instance_id {
1532 self.instance_id = Some(instance_id.clone());
1533 }
1534 if let Some(provenance) = &other.provenance {
1535 self.provenance = Some(provenance.clone());
1536 }
1537 if let Some(hash) = &other.hash {
1538 self.hash = Some(hash.clone());
1539 }
1540 if let Some(document_id) = &other.document_id {
1541 self.document_id = Some(document_id.clone());
1542 }
1543 if let Some(description) = &other.description {
1544 self.description = Some(description.clone());
1545 }
1546 if let Some(informational_uri) = &other.informational_uri {
1547 self.informational_uri = Some(informational_uri.clone());
1548 }
1549 if let Some(data) = &other.data {
1550 self.data = Some(data.clone());
1551 }
1552 if let Some(thumbnail) = &other.thumbnail {
1553 self.thumbnail = Some(thumbnail.clone());
1554 }
1555 if let Some(metadata) = &other.metadata {
1556 self.metadata = Some(metadata.clone());
1557 }
1558 if let Some(label) = &other.label {
1559 self.label = Some(label.clone());
1560 }
1561 }
1563}
1564
1565impl std::fmt::Display for Ingredient {
1566 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1567 let report = serde_json::to_string_pretty(self).unwrap_or_default();
1568 f.write_str(&report)
1569 }
1570}
1571
1572#[cfg(feature = "file_io")]
1574pub trait IngredientOptions {
1575 fn title(&self, _path: &Path) -> Option<String> {
1579 None
1580 }
1581
1582 fn hash(&self, _path: &Path) -> Option<String> {
1587 None
1588 }
1589
1590 fn thumbnail(&self, _path: &Path) -> Option<(String, Vec<u8>)> {
1596 None
1597 }
1598
1599 fn base_path(&self) -> Option<&Path> {
1603 None
1604 }
1605}
1606
1607#[cfg(feature = "file_io")]
1611pub struct DefaultOptions {
1612 pub base: Option<std::path::PathBuf>,
1616}
1617
1618#[cfg(feature = "file_io")]
1619impl IngredientOptions for DefaultOptions {
1620 fn base_path(&self) -> Option<&Path> {
1621 self.base.as_deref()
1622 }
1623}
1624
1625#[cfg(test)]
1626mod tests {
1627 #![allow(clippy::expect_used)]
1628 #![allow(clippy::unwrap_used)]
1629
1630 use c2pa_macros::c2pa_test_async;
1631 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1632 use wasm_bindgen_test::*;
1633
1634 use super::*;
1635 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1636 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1637
1638 #[test]
1639 #[cfg_attr(
1640 all(target_arch = "wasm32", not(target_os = "wasi")),
1641 wasm_bindgen_test
1642 )]
1643 fn test_ingredient_api() {
1644 let mut ingredient = Ingredient::new("title", "format", "instance_id");
1645 ingredient
1646 .resources_mut()
1647 .add("id", "data".as_bytes().to_vec())
1648 .expect("add");
1649 ingredient
1650 .set_document_id("document_id")
1651 .set_title("title2")
1652 .set_hash("hash")
1653 .set_provenance("provenance")
1654 .set_is_parent()
1655 .set_relationship(Relationship::ParentOf)
1656 .set_metadata(AssertionMetadata::new())
1657 .set_thumbnail("format", "thumbnail".as_bytes().to_vec())
1658 .unwrap()
1659 .set_active_manifest("active_manifest")
1660 .set_manifest_data("data".as_bytes().to_vec())
1661 .expect("set_manifest")
1662 .set_description("description")
1663 .set_informational_uri("uri")
1664 .set_data_ref(ResourceRef::new("format", "id"))
1665 .expect("set_data_ref")
1666 .add_validation_status(ValidationStatus::new("status_code"));
1667 assert_eq!(ingredient.title(), Some("title2"));
1668 assert_eq!(ingredient.format(), Some("format"));
1669 assert_eq!(ingredient.instance_id(), "instance_id");
1670 assert_eq!(ingredient.document_id(), Some("document_id"));
1671 assert_eq!(ingredient.provenance(), Some("provenance"));
1672 assert_eq!(ingredient.hash(), Some("hash"));
1673 assert!(ingredient.is_parent());
1674 assert_eq!(ingredient.relationship(), &Relationship::ParentOf);
1675 assert_eq!(ingredient.description(), Some("description"));
1676 assert_eq!(ingredient.informational_uri(), Some("uri"));
1677 assert_eq!(ingredient.data_ref().unwrap().format, "format");
1678 assert_eq!(ingredient.data_ref().unwrap().identifier, "id");
1679 assert!(ingredient.metadata().is_some());
1680 assert_eq!(ingredient.thumbnail().unwrap().0, "format");
1681 assert_eq!(
1682 *ingredient.thumbnail().unwrap().1,
1683 "thumbnail".as_bytes().to_vec()
1684 );
1685 assert_eq!(
1686 *ingredient.thumbnail_bytes().unwrap(),
1687 "thumbnail".as_bytes().to_vec()
1688 );
1689 assert_eq!(ingredient.active_manifest(), Some("active_manifest"));
1690
1691 assert_eq!(
1692 ingredient.validation_status().unwrap()[0].code(),
1693 "status_code"
1694 );
1695 }
1696
1697 #[c2pa_test_async]
1698 async fn test_stream_async_jpg() {
1699 let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
1700 let title = "Test Image";
1701 let format = "image/jpeg";
1702 let mut ingredient = Ingredient::from_memory_async(format, image_bytes)
1703 .await
1704 .expect("from_memory");
1705 ingredient.set_title(title);
1706
1707 println!("ingredient = {ingredient}");
1708 assert_eq!(ingredient.title(), Some(title));
1709 assert_eq!(ingredient.format(), Some(format));
1710 assert!(ingredient.manifest_data().is_some());
1711 assert_eq!(ingredient.metadata(), None);
1712 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1713 web_sys::console::debug_2(
1714 &"ingredient_from_memory_async:".into(),
1715 &ingredient.to_string().into(),
1716 );
1717 assert_eq!(ingredient.validation_status(), None);
1718 }
1719
1720 #[test]
1721 fn test_stream_jpg() {
1722 let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
1723 let title = "Test Image";
1724 let format = "image/jpeg";
1725 let mut ingredient = Ingredient::from_memory(format, image_bytes).expect("from_memory");
1726 ingredient.set_title(title);
1727
1728 println!("ingredient = {ingredient}");
1729 assert_eq!(ingredient.title(), Some(title));
1730 assert_eq!(ingredient.format(), Some(format));
1731 assert!(ingredient.manifest_data().is_some());
1732 assert_eq!(ingredient.metadata(), None);
1733 assert_eq!(ingredient.validation_status(), None);
1734 }
1735
1736 #[cfg(feature = "add_thumbnails")]
1737 #[test]
1738 fn test_stream_thumbnail() {
1739 use crate::settings::Settings;
1740
1741 #[cfg(target_os = "wasi")]
1742 Settings::reset().unwrap();
1743
1744 Settings::from_toml(
1745 &toml::toml! {
1746 [builder.thumbnail]
1747 enabled = true
1748 }
1749 .to_string(),
1750 )
1751 .unwrap();
1752
1753 let image_bytes = include_bytes!("../tests/fixtures/sample1.png");
1754 let ingredient = Ingredient::from_memory("image/png", image_bytes).unwrap();
1755 assert!(ingredient.thumbnail().is_some());
1756
1757 Settings::from_toml(
1758 &toml::toml! {
1759 [builder.thumbnail]
1760 enabled = false
1761 }
1762 .to_string(),
1763 )
1764 .unwrap();
1765
1766 let ingredient = Ingredient::from_memory("image/png", image_bytes).unwrap();
1767 assert!(ingredient.thumbnail().is_none());
1768 #[cfg(target_os = "wasi")]
1769 Settings::reset().unwrap();
1770 }
1771
1772 #[c2pa_test_async]
1773 async fn test_stream_ogp() {
1774 let image_bytes = include_bytes!("../tests/fixtures/XCA.jpg");
1775 let title = "XCA.jpg";
1776 let format = "image/jpeg";
1777 let mut ingredient = Ingredient::from_memory_async(format, image_bytes)
1778 .await
1779 .expect("from_memory");
1780 ingredient.set_title(title);
1781
1782 println!("ingredient = {ingredient}");
1783 assert_eq!(ingredient.title(), Some(title));
1784 assert_eq!(ingredient.format(), Some(format));
1785 #[cfg(feature = "add_thumbnails")]
1786 assert!(ingredient.thumbnail().is_some());
1787 assert!(ingredient.manifest_data().is_some());
1788 assert_eq!(ingredient.metadata(), None);
1789 assert!(ingredient.validation_status().is_some());
1790 assert_eq!(
1791 ingredient.validation_status().unwrap()[0].code(),
1792 validation_status::ASSERTION_DATAHASH_MISMATCH
1793 );
1794 }
1795
1796 #[cfg(feature = "fetch_remote_manifests")]
1797 #[c2pa_test_async]
1798 async fn test_jpg_cloud_from_memory() {
1799 crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
1800 crate::settings::set_settings_value("verify.remote_manifest_fetch", true).unwrap();
1801
1802 let image_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
1803 let format = "image/jpeg";
1804
1805 let ingredient = Ingredient::from_memory_async(format, image_bytes)
1806 .await
1807 .expect("from_memory_async");
1808
1809 assert_eq!(ingredient.title(), Some("untitled"));
1811 assert_eq!(ingredient.format(), Some(format));
1812 assert!(ingredient.provenance().is_some());
1813 assert!(ingredient.provenance().unwrap().starts_with("https:"));
1814 assert!(ingredient.manifest_data().is_some());
1815 assert_eq!(ingredient.validation_status(), None);
1816 }
1817
1818 #[cfg(not(any(feature = "fetch_remote_manifests", feature = "file_io")))]
1819 #[c2pa_test_async]
1820 async fn test_jpg_cloud_from_memory_no_file_io() {
1821 crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
1822 crate::settings::set_settings_value("verify.remote_manifest_fetch", true).unwrap();
1823
1824 let image_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
1825 let format = "image/jpeg";
1826
1827 let ingredient = Ingredient::from_memory_async(format, image_bytes)
1828 .await
1829 .expect("from_memory_async");
1830
1831 assert!(ingredient.validation_status().is_some());
1832 assert_eq!(
1833 ingredient.validation_status().unwrap()[0].code(),
1834 validation_status::MANIFEST_INACCESSIBLE
1835 );
1836 assert!(ingredient.validation_status().unwrap()[0]
1837 .url()
1838 .unwrap()
1839 .starts_with("http"));
1840 assert_eq!(ingredient.manifest_data(), None);
1841 }
1842
1843 #[c2pa_test_async]
1844 async fn test_jpg_cloud_from_memory_and_manifest() {
1845 crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
1846
1847 let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
1848 let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa");
1849 let format = "image/jpeg";
1850 let ingredient = Ingredient::from_manifest_and_asset_bytes_async(
1851 manifest_bytes.to_vec(),
1852 format,
1853 asset_bytes,
1854 )
1855 .await
1856 .unwrap();
1857 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1858 web_sys::console::debug_2(
1859 &"ingredient_from_memory_async:".into(),
1860 &ingredient.to_string().into(),
1861 );
1862 assert_eq!(ingredient.validation_status(), None);
1863 assert!(ingredient.manifest_data().is_some());
1864 assert!(ingredient.provenance().is_some());
1865 }
1866}
1867
1868#[cfg(test)]
1869#[cfg(feature = "file_io")]
1870mod tests_file_io {
1871 #![allow(clippy::expect_used)]
1872 #![allow(clippy::unwrap_used)]
1873
1874 use super::*;
1875 use crate::{assertion::AssertionData, utils::test::fixture_path};
1876
1877 const NO_MANIFEST_JPEG: &str = "earth_apollo17.jpg";
1878 const MANIFEST_JPEG: &str = "C.jpg";
1879 const BAD_SIGNATURE_JPEG: &str = "E-sig-CA.jpg";
1880
1881 fn stats(ingredient: &Ingredient) -> usize {
1882 let thumb_size = ingredient.thumbnail_bytes().map_or(0, |i| i.len());
1883 let manifest_data_size = ingredient.manifest_data().map_or(0, |r| r.len());
1884
1885 println!(
1886 " {} instance_id: {}, thumb size: {}, manifest_data size: {}",
1887 ingredient.title().unwrap_or_default(),
1888 ingredient.instance_id(),
1889 thumb_size,
1890 manifest_data_size,
1891 );
1892 ingredient.title().unwrap_or_default().len()
1893 + ingredient.instance_id().len()
1894 + thumb_size
1895 + manifest_data_size
1896 }
1897
1898 fn test_thumbnail(ingredient: &Ingredient, format: &str) {
1900 if cfg!(feature = "add_thumbnails") {
1901 assert!(ingredient.thumbnail().is_some());
1902 assert_eq!(ingredient.thumbnail().unwrap().0, format);
1903 } else {
1904 assert_eq!(ingredient.thumbnail(), None);
1905 }
1906 }
1907
1908 #[test]
1909 #[cfg(feature = "file_io")]
1910 fn test_psd() {
1911 let ap = fixture_path("Purple Square.psd");
1914 let ingredient = Ingredient::from_file(ap).expect("from_file");
1915 stats(&ingredient);
1916
1917 println!("ingredient = {ingredient}");
1918 assert_eq!(ingredient.title(), Some("Purple Square.psd"));
1919 assert_eq!(ingredient.format(), Some("image/vnd.adobe.photoshop"));
1920 assert!(ingredient.thumbnail().is_none()); assert!(ingredient.manifest_data().is_none());
1922 }
1923
1924 #[test]
1925 #[cfg(feature = "file_io")]
1926 fn test_manifest_jpg() {
1927 let ap = fixture_path(MANIFEST_JPEG);
1928 let ingredient = Ingredient::from_file(ap).expect("from_file");
1929 stats(&ingredient);
1930
1931 println!("ingredient = {ingredient}");
1932 assert_eq!(ingredient.title(), Some(MANIFEST_JPEG));
1933 assert_eq!(ingredient.format(), Some("image/jpeg"));
1934 assert!(ingredient.thumbnail_ref().is_some()); assert!(ingredient
1936 .thumbnail_ref()
1937 .unwrap()
1938 .identifier
1939 .starts_with("self#jumbf="));
1940 assert!(ingredient.manifest_data().is_some());
1941 assert_eq!(ingredient.metadata(), None);
1942 }
1943
1944 #[test]
1945 #[cfg(feature = "file_io")]
1946 fn test_no_manifest_jpg() {
1947 let ap = fixture_path(NO_MANIFEST_JPEG);
1948 let ingredient = Ingredient::from_file(ap).expect("from_file");
1949 stats(&ingredient);
1950
1951 println!("ingredient = {ingredient}");
1952 assert_eq!(ingredient.title(), Some(NO_MANIFEST_JPEG));
1953 assert_eq!(ingredient.format(), Some("image/jpeg"));
1954 test_thumbnail(&ingredient, "image/jpeg");
1955 assert_eq!(ingredient.provenance(), None);
1956 assert_eq!(ingredient.manifest_data(), None);
1957 assert_eq!(ingredient.metadata(), None);
1958 assert!(ingredient.instance_id().starts_with("xmp.iid:"));
1959 #[cfg(feature = "add_thumbnails")]
1960 assert!(ingredient
1961 .thumbnail_ref()
1962 .unwrap()
1963 .identifier
1964 .starts_with("xmp.iid"));
1965 }
1966
1967 #[test]
1968 #[cfg(feature = "file_io")]
1969 fn test_jpg_options() {
1970 struct MyOptions {}
1971 impl IngredientOptions for MyOptions {
1972 fn title(&self, _path: &Path) -> Option<String> {
1973 Some("MyTitle".to_string())
1974 }
1975
1976 fn hash(&self, _path: &Path) -> Option<String> {
1977 Some("1234568abcdef".to_string())
1978 }
1979
1980 fn thumbnail(&self, _path: &Path) -> Option<(String, Vec<u8>)> {
1981 Some(("image/foo".to_string(), "bits".as_bytes().to_owned()))
1982 }
1983 }
1984
1985 let ap = fixture_path(NO_MANIFEST_JPEG);
1986 let ingredient = Ingredient::from_file_with_options(ap, &MyOptions {}).expect("from_file");
1987 stats(&ingredient);
1988
1989 assert_eq!(ingredient.title(), Some("MyTitle"));
1990 assert_eq!(ingredient.format(), Some("image/jpeg"));
1991 assert_eq!(ingredient.hash(), Some("1234568abcdef"));
1992 assert_eq!(ingredient.thumbnail_ref().unwrap().format, "image/foo"); assert_eq!(ingredient.manifest_data(), None);
1994 assert_eq!(ingredient.metadata(), None);
1995 }
1996
1997 #[test]
1998 #[cfg(feature = "file_io")]
1999 fn test_png_no_claim() {
2000 let ap = fixture_path("libpng-test.png");
2001 let ingredient = Ingredient::from_file(ap).expect("from_file");
2002 stats(&ingredient);
2003
2004 println!("ingredient = {ingredient}");
2005 assert_eq!(ingredient.title(), Some("libpng-test.png"));
2006 test_thumbnail(&ingredient, "image/png");
2007 assert_eq!(ingredient.provenance(), None);
2008 assert_eq!(ingredient.manifest_data, None);
2009 }
2010
2011 #[test]
2012 #[cfg(feature = "file_io")]
2013 fn test_jpg_bad_signature() {
2014 let ap = fixture_path(BAD_SIGNATURE_JPEG);
2015 let ingredient = Ingredient::from_file(ap).expect("from_file");
2016 stats(&ingredient);
2017
2018 println!("ingredient = {ingredient}");
2019 assert_eq!(ingredient.title(), Some(BAD_SIGNATURE_JPEG));
2020 assert_eq!(ingredient.format(), Some("image/jpeg"));
2021 test_thumbnail(&ingredient, "image/jpeg");
2022 assert!(ingredient.manifest_data().is_some());
2023 assert!(
2024 ingredient
2025 .validation_results()
2026 .unwrap()
2027 .active_manifest()
2028 .unwrap()
2029 .informational
2030 .iter()
2031 .any(|info| info.code() == validation_status::TIMESTAMP_MISMATCH),
2032 "No informational item with TIMESTAMP_MISMATCH found"
2033 );
2034 }
2035
2036 #[test]
2037 #[cfg(all(feature = "file_io", feature = "add_thumbnails"))]
2038 fn test_jpg_prerelease() {
2039 const PRERELEASE_JPEG: &str = "prerelease.jpg";
2040 let ap = fixture_path(PRERELEASE_JPEG);
2041 let ingredient = Ingredient::from_file(ap).expect("from_file");
2042 stats(&ingredient);
2043
2044 println!("ingredient = {ingredient}");
2045 assert_eq!(ingredient.title(), Some(PRERELEASE_JPEG));
2046 assert_eq!(ingredient.format(), Some("image/jpeg"));
2047 test_thumbnail(&ingredient, "image/jpeg");
2048 assert!(ingredient.provenance().is_some());
2049 assert_eq!(ingredient.manifest_data(), None);
2050 assert!(ingredient.validation_status().is_some());
2051 assert_eq!(
2052 ingredient.validation_status().unwrap()[0].code(),
2053 validation_status::STATUS_PRERELEASE
2054 );
2055 }
2056
2057 #[test]
2058 #[cfg(feature = "file_io")]
2059 fn test_jpg_nested_err() {
2060 let ap = fixture_path("CIE-sig-CA.jpg");
2061 let ingredient = Ingredient::from_file(ap).expect("from_file");
2062 assert_eq!(ingredient.validation_status(), None);
2064 assert!(ingredient.manifest_data().is_some());
2065 }
2066
2067 #[test]
2068 #[cfg(feature = "fetch_remote_manifests")]
2069 fn test_jpg_cloud_failure() {
2070 let ap = fixture_path("cloudx.jpg");
2071 let ingredient = Ingredient::from_file(ap).expect("from_file");
2072 println!("ingredient = {ingredient}");
2073 assert!(ingredient.validation_status().is_some());
2074 assert_eq!(
2075 ingredient.validation_status().unwrap()[0].code(),
2076 validation_status::MANIFEST_INACCESSIBLE
2077 );
2078 }
2079
2080 #[test]
2081 #[cfg(feature = "file_io")]
2082 fn test_jpg_with_path() {
2083 use crate::utils::io_utils::tempdirectory;
2084
2085 let ap = fixture_path("CA.jpg");
2086 let temp_dir = tempdirectory().expect("Failed to create temp directory");
2087 let folder = temp_dir.path().join("ingredient");
2088 std::fs::create_dir_all(&folder).expect("Failed to create subdirectory");
2089
2090 let ingredient = Ingredient::from_file_with_folder(ap, folder).expect("from_file");
2091 println!("ingredient = {ingredient}");
2092 assert_eq!(ingredient.validation_status(), None);
2093
2094 assert!(ingredient
2096 .thumbnail_ref()
2097 .unwrap()
2098 .identifier
2099 .contains(labels::JPEG_CLAIM_THUMBNAIL));
2100
2101 assert!(ingredient.manifest_data_ref().is_some());
2103 assert_eq!(ingredient.thumbnail_ref().unwrap().format, "image/jpeg");
2104 assert!(ingredient
2105 .thumbnail_ref()
2106 .unwrap()
2107 .identifier
2108 .starts_with("self#jumbf="));
2109 }
2110
2111 #[test]
2112 #[cfg(feature = "file_io")]
2113 fn test_file_based_ingredient() {
2114 let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2115 folder.push("tests/fixtures");
2116 let mut ingredient = Ingredient::new("title", "format", "instance_id");
2117 ingredient.resources.set_base_path(folder);
2118
2119 assert_eq!(ingredient.thumbnail_ref(), None);
2120 assert_eq!(ingredient.manifest_data_ref(), None);
2124 assert!(ingredient
2126 .set_thumbnail_ref(ResourceRef::new("image/jpeg", "C.jpg"))
2127 .is_ok());
2128 assert!(ingredient.thumbnail_ref().is_some());
2129 assert!(ingredient
2130 .set_manifest_data_ref(ResourceRef::new("application/c2pa", "cloud_manifest.c2pa"))
2131 .is_ok());
2132 assert!(ingredient.manifest_data_ref().is_some());
2133 }
2134
2135 #[test]
2136 fn test_input_to_ingredient() {
2137 let mut ingredient = Ingredient::new_v2("prompt", "text/plain");
2139 ingredient.relationship = Relationship::InputTo;
2140
2141 ingredient
2143 .resources_mut()
2144 .add("prompt_id", "pirate with bird on shoulder")
2145 .expect("add");
2146
2147 let mut data_ref = ResourceRef::new("text/plain", "prompt_id");
2149 let data_type = crate::assertions::AssetType {
2150 asset_type: "c2pa.types.generator.prompt".to_string(),
2151 version: None,
2152 };
2153 data_ref.data_types = Some([data_type].to_vec());
2154
2155 ingredient.set_data_ref(data_ref).expect("set_data_ref");
2157
2158 println!("ingredient = {ingredient}");
2159
2160 assert_eq!(ingredient.title(), Some("prompt"));
2161 assert_eq!(ingredient.format(), Some("text/plain"));
2162 assert_eq!(ingredient.instance_id(), "None");
2163 assert_eq!(ingredient.data_ref().unwrap().identifier, "prompt_id");
2164 assert_eq!(ingredient.data_ref().unwrap().format, "text/plain");
2165 assert_eq!(ingredient.relationship(), &Relationship::InputTo);
2166 assert_eq!(
2167 ingredient.data_ref().unwrap().data_types.as_ref().unwrap()[0].asset_type,
2168 "c2pa.types.generator.prompt"
2169 );
2170 }
2171
2172 #[test]
2173 #[cfg(feature = "file_io")]
2174 fn test_input_to_file_based_ingredient() {
2175 let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2176 folder.push("tests/fixtures");
2177 let mut ingredient = Ingredient::new_v2("title", "format");
2178 ingredient.resources.set_base_path(folder);
2179 }
2182
2183 #[test]
2184 fn test_thumbnail_from_assertion_for_svg() {
2185 let assertion = Assertion::new(
2186 "c2pa.thumbnail.ingredient",
2187 None,
2188 AssertionData::Binary(include_bytes!("../tests/fixtures/sample1.svg").to_vec()),
2189 )
2190 .set_content_type("image/svg+xml");
2191 let (format, image) = Ingredient::thumbnail_from_assertion(&assertion);
2192 assert_eq!(format, "image/svg+xml");
2193 assert_eq!(
2194 image,
2195 include_bytes!("../tests/fixtures/sample1.svg").to_vec()
2196 );
2197 }
2198}