// Copyright 2024 Adobe. All rights reserved. // This file is licensed to you under the Apache License, // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) // or the MIT license (http://opensource.org/licenses/MIT), // at your option. // Unless required by applicable law or agreed to in writing, // this software is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or // implied. See the LICENSE-MIT and LICENSE-APACHE files for the // specific language governing permissions and limitations under // each license. use std::{collections::HashMap, env::consts}; #[cfg(feature = "json_schema")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ assertions::{ region_of_interest::RegionOfInterest, Action, ActionParameters, ActionTemplate, DigitalSourceType, SoftwareAgent, }, builder::BuilderIntent, cbor_types::DateT, resource_store::UriOrResource, settings::{deserialize_case_insensitive, deserialize_case_insensitive_opt, SettingsValidate}, ClaimGeneratorInfo, Error, ResourceRef, Result, }; // TODO: thumbnails/previews for audio? /// Possible output types for automatic thumbnail generation. /// /// These formats are a combination of types supported in [image-rs](https://docs.rs/image/latest/image/enum.ImageFormat.html) /// and types defined by the [IANA registry media type](https://www.iana.org/assignments/media-types/media-types.xhtml) (as defined in the spec). #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum ThumbnailFormat { /// An image in PNG format. Png, /// An image in JPEG format. Jpeg, /// An image in GIF format. Gif, /// An image in WEBP format. WebP, /// An image in TIFF format. Tiff, } /// Quality of the thumbnail. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum ThumbnailQuality { /// Low quality. Low, /// Medium quality. Medium, /// High quality. High, } /// Settings for controlling automatic thumbnail generation. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ThumbnailSettings { /// Whether or not to automatically generate thumbnails. /// /// The default value is true. /// ///
/// This setting is only applicable if the crate is compiled with the `add_thumbnails` feature. ///
pub enabled: bool, /// Whether to ignore thumbnail generation errors. /// /// This may occur, for instance, if the thumbnail media type or color layout isn't /// supported. /// /// The default value is true. pub ignore_errors: bool, /// The size of the longest edge of the thumbnail. /// /// This function will resize the input to preserve aspect ratio. /// /// The default value is 1024. pub long_edge: u32, /// Format of the thumbnail. /// /// If this field isn't specified, the thumbnail format will correspond to the /// input format. /// /// The default value is None. #[serde( skip_serializing_if = "Option::is_none", default, deserialize_with = "deserialize_case_insensitive_opt" )] pub format: Option, /// Whether or not to prefer a smaller sized media format for the thumbnail. /// /// Note that [ThumbnailSettings::format] takes precedence over this field. In addition, /// if the output format is unsupported, it will default to the smallest format regardless /// of the value of this field. /// /// For instance, if the source input type is a PNG, but it doesn't have an alpha channel, /// the image will be converted to a JPEG of smaller size. /// /// The default value is true. pub prefer_smallest_format: bool, /// The output quality of the thumbnail. /// /// This setting contains sensible defaults for things like quality, compression, and /// algorithms for various formats. /// /// The default value is [`ThumbnailQuality::Medium`]. #[serde(deserialize_with = "deserialize_case_insensitive")] pub quality: ThumbnailQuality, } impl Default for ThumbnailSettings { fn default() -> Self { ThumbnailSettings { enabled: true, ignore_errors: true, long_edge: 1024, format: None, prefer_smallest_format: true, quality: ThumbnailQuality::Medium, } } } impl SettingsValidate for ThumbnailSettings { fn validate(&self) -> Result<()> { #[cfg(not(feature = "add_thumbnails"))] if self.enabled { log::warn!("c2pa-rs feature `add_thumbnails` must be enabled to generate thumbnails!"); } Ok(()) } } /// Settings for the auto actions (e.g. created, opened, placed). #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct AutoActionSettings { /// Whether to enable this auto action or not. pub enabled: bool, /// The default source type for the auto action. #[serde(skip_serializing_if = "Option::is_none")] pub source_type: Option, } #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(untagged, rename_all = "lowercase")] pub enum ClaimGeneratorInfoOperatingSystem { /// Whether or not to automatically infer the operating system. /// /// This option will attempt to following the [LLVM "triples"] conventions. For more information, /// see [`ClaimGeneratorInfoOperatingSystem::Other`]. /// /// [LLVM "triples"]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple Auto, /// The name of the operating system. /// /// It is recommended to follow the [LLVM "triples"] conventions to define the operating system, /// with the format `---`. For instance: /// - `x86_64-unknown-linux-gnu` /// - `x86_64-pc-windows-msvc` /// - `arm64-apple-darwin` /// /// [LLVM "triples"]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple Other(String), } /// Settings for the claim generator info. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ClaimGeneratorInfoSettings { /// A human readable string naming the claim_generator. pub name: String, /// A human readable string of the product's version. #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, /// Reference to an icon. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) icon: Option, /// Settings for the claim generator info's operating system field. #[serde(skip_serializing_if = "Option::is_none")] pub operating_system: Option, /// Any other values that are not part of the standard. #[serde(flatten)] pub other: HashMap, } impl TryFrom for ClaimGeneratorInfo { type Error = Error; fn try_from(value: ClaimGeneratorInfoSettings) -> Result { Ok(ClaimGeneratorInfo { name: value.name, version: value.version, icon: value.icon.map(UriOrResource::ResourceRef), operating_system: { value.operating_system.map(|os| match os { ClaimGeneratorInfoOperatingSystem::Auto => { format!("{}-unknown-{}", consts::ARCH, consts::OS) } ClaimGeneratorInfoOperatingSystem::Other(name) => name, }) }, spec_version: None, other: value .other .into_iter() .map(|(key, value)| { serde_json::to_value(value) .map(|value| (key, value)) .map_err(|err| err.into()) }) .collect::>>()?, }) } } impl TryFrom<&ClaimGeneratorInfoSettings> for ClaimGeneratorInfo { type Error = Error; fn try_from(value: &ClaimGeneratorInfoSettings) -> Result { Ok(ClaimGeneratorInfo { name: value.name.clone(), version: value.version.clone(), icon: value .icon .as_ref() .map(|icon| UriOrResource::ResourceRef(icon.clone())), operating_system: { value.operating_system.as_ref().map(|os| match os { ClaimGeneratorInfoOperatingSystem::Auto => { format!("{}-unknown-{}", consts::ARCH, consts::OS) } ClaimGeneratorInfoOperatingSystem::Other(name) => name.clone(), }) }, spec_version: None, other: value .other .iter() .map(|(key, value)| { serde_json::to_value(value) .map(|value| (key.clone(), value)) .map_err(|err| err.into()) }) .collect::>>()?, }) } } /// Settings for an action template. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub(crate) struct ActionTemplateSettings { /// The label associated with this action. See ([c2pa_action][crate::assertions::actions::c2pa_action]). pub action: String, /// The software agent that performed the action. #[serde(skip_serializing_if = "Option::is_none")] pub software_agent: Option, // TODO: change this to use string names and document in c2pa.toml /// 0-based index into the softwareAgents array #[serde(skip_serializing_if = "Option::is_none")] pub software_agent_index: Option, /// One of the defined URI values at `` #[serde(skip_serializing_if = "Option::is_none")] pub source_type: Option, // TODO: handle paths/urls and document in the sample c2pa.toml /// Reference to an icon. #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, /// Description of the template. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// Additional parameters for the template #[serde(skip_serializing_if = "Option::is_none")] pub template_parameters: Option>, } impl TryFrom for ActionTemplate { type Error = Error; fn try_from(value: ActionTemplateSettings) -> Result { Ok(ActionTemplate { action: value.action, software_agent: value .software_agent .map(|software_agent| software_agent.try_into()) .transpose()?, software_agent_index: value.software_agent_index, source_type: value.source_type, icon: value.icon.map(UriOrResource::ResourceRef), description: value.description, template_parameters: value .template_parameters .map(|template_parameters| { template_parameters .into_iter() .map(|(key, value)| { c2pa_cbor::value::to_value(value) .map(|value| (key, value)) .map_err(|err| err.into()) }) .collect::>>() }) .transpose()?, }) } } /// Settings for an action. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub(crate) struct ActionSettings { /// The label associated with this action. See ([`c2pa_action`]). pub action: String, /// Timestamp of when the action occurred. #[serde(skip_serializing_if = "Option::is_none")] pub when: Option, /// The software agent that performed the action. #[serde(skip_serializing_if = "Option::is_none")] pub software_agent: Option, /// 0-based index into the softwareAgents array. #[serde(skip_serializing_if = "Option::is_none")] pub software_agent_index: Option, /// A list of the regions of interest of the resource that were changed. /// /// If not present, presumed to be undefined. /// When tracking changes and the scope of the changed components is unknown, /// it should be assumed that anything might have changed. #[serde(skip_serializing_if = "Option::is_none")] pub changes: Option>, /// Additional parameters of the action. These vary by the type of action. #[serde(skip_serializing_if = "Option::is_none")] pub parameters: Option, /// One of the defined URI values at ``. #[serde(skip_serializing_if = "Option::is_none")] pub source_type: Option, /// List of related actions. #[serde(skip_serializing_if = "Option::is_none")] pub related: Option>, // The reason why this action was performed, required when the action is `c2pa.redacted`. #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, /// Description of the action. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, } impl TryFrom for Action { type Error = Error; fn try_from(value: ActionSettings) -> Result { Ok(Action { action: value.action, when: value.when, software_agent: value .software_agent .map(|software_agent| software_agent.try_into()) .transpose()? .map(SoftwareAgent::ClaimGeneratorInfo), software_agent_index: value.software_agent_index, changes: value.changes, parameters: value.parameters, source_type: value.source_type, related: value.related, reason: value.reason, description: value.description, ..Default::default() }) } } /// Settings for configuring the "base" [Actions][crate::assertions::Actions] assertion. /// /// The reason this setting exists only for an [Actions][crate::assertions::Actions] assertion /// is because of its mandations and reusable fields. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ActionsSettings { /// Whether or not to set the [Actions::all_actions_included][crate::assertions::Actions::all_actions_included] /// field. #[serde(skip_serializing_if = "Option::is_none")] pub all_actions_included: Option, /// Whether to automatically set [Actions::all_actions_included][crate::assertions::Actions::all_actions_included] /// to `true` when the manifest's sole recorded action is `c2pa.opened` — i.e. the asset was /// opened only to record that action and immediately re-saved without any other changes, /// as required by the /// [C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_all_actions_included). /// /// Disabled by default: the builder can only see changes that were recorded as an /// [`Action`], so enabling this is an assertion by the caller that every change made to /// the asset in this workflow is in fact tracked as an action. /// Takes priority over `all_actions_included` when it applies, but never overrides a value /// the caller explicitly set on the actions assertion data itself. pub auto_all_actions_included: bool, /// Templates to be added to the [Actions::templates][crate::assertions::Actions::templates] field. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) templates: Option>, /// Actions to be added to the [Actions::actions][crate::assertions::Actions::actions] field. // TODO: ActionSettings indirectly depends on ActionParameters which contains a c2pa_cbor::Value and // schemars can't generate a schema for cbor values. It also doesn't feel right to change our API for // the sake of json schemas. #[cfg_attr(feature = "json_schema", schemars(skip))] pub(crate) actions: Option>, /// Whether to automatically generate a c2pa.created [Action] assertion or error that it doesn't already exist. /// /// For more information about the mandatory conditions for a c2pa.created action assertion, see the /// [C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_mandatory_presence_of_at_least_one_actions_assertion). pub auto_created_action: AutoActionSettings, /// Whether to automatically generate a c2pa.opened [Action] assertion or error that it doesn't already exist. /// /// For more information about the mandatory conditions for a c2pa.opened action assertion, see the /// [C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_mandatory_presence_of_at_least_one_actions_assertion). pub auto_opened_action: AutoActionSettings, /// Whether to automatically generate a c2pa.placed [Action] assertion or error that it doesn't already exist. /// /// For more information about the mandatory conditions for a c2pa.placed action assertion, see /// [Relationship - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_relationship) pub auto_placed_action: AutoActionSettings, } impl Default for ActionsSettings { fn default() -> Self { ActionsSettings { all_actions_included: None, auto_all_actions_included: false, templates: None, actions: None, auto_created_action: AutoActionSettings { enabled: false, source_type: None, // Some(DigitalSourceType::Empty), }, auto_opened_action: AutoActionSettings { enabled: false, source_type: None, }, auto_placed_action: AutoActionSettings { enabled: false, source_type: None, }, } } } impl SettingsValidate for ActionsSettings { fn validate(&self) -> Result<()> { match self.auto_created_action.enabled && self.auto_created_action.source_type.is_none() { true => Err(Error::MissingAutoCreatedActionSourceType), false => Ok(()), } } } /// The scope of manifests to fetch timestamps for. /// /// See [`TimeStampSettings`] for more information. #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)] #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[serde(rename_all = "lowercase")] pub enum TimeStampFetchScope { /// Fetch timestamps for only the parent manifest. Parent, /// Fetch timestamps for all manifests in the manifest store. All, } /// Settings for configuring auto-generation of the [`TimeStamp`] assertion. /// /// Useful when a manifest was signed offline and you want to attach a trusted timestamp to it later. /// /// [`TimeStamp`]: crate::assertions::TimeStamp #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TimeStampSettings { /// Whether to auto-generate a [`TimeStamp`] assertion for the [`TimeStampSettings::fetch_scope`]. /// /// Note that for this setting to take effect, a timestamping authority URL must be set in the /// [`Signer::time_authority_url`]. If the signer is acquired from settings via [`Settings::signer`], /// the URL can be set in [`SignerSettings`]. /// /// The default value is false. /// /// [`TimeStamp`]: crate::assertions::TimeStamp /// [`Signer::time_authority_url`]: crate::Signer::time_authority_url /// [`Settings::signer`]: crate::settings::signer /// [`SignerSettings`]: crate::settings::signer::SignerSettings pub enabled: bool, /// Whether to skip fetching timestamps for manifests that already have one. /// /// This setting will account for both existing [`TimeStamp`] assertions and timestamps embedded /// in the claim. /// /// The default value is true. /// /// [`TimeStamp`]: crate::assertions::TimeStamp pub skip_existing: bool, /// Which manifests to fetch timestamps for. /// /// The default value is [`TimeStampFetchScope::All`]. #[serde(deserialize_with = "deserialize_case_insensitive")] pub fetch_scope: TimeStampFetchScope, } impl Default for TimeStampSettings { fn default() -> Self { Self { enabled: false, skip_existing: true, fetch_scope: TimeStampFetchScope::All, } } } // TODO: do more validation on URL fields, cert fields, etc. /// Settings for the [Builder][crate::Builder]. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct BuilderSettings { /// The name of the vendor creating the content credential. pub vendor: Option, /// When set, used as [`ClaimGeneratorInfo`] when /// [`ManifestDefinition::claim_generator_info`](crate::builder::ManifestDefinition) is empty /// (e.g. key omitted in JSON or an empty array). If `None` or when the definition lists at /// least one generator, that path does not use this value. #[serde(skip_serializing_if = "Option::is_none")] pub claim_generator_info: Option, /// Various settings for configuring automatic thumbnail generation. pub thumbnail: ThumbnailSettings, /// Settings for configuring fields in an [Actions][crate::assertions::Actions] assertion. /// /// For more information on the reasoning behind this field see [ActionsSettings]. pub actions: ActionsSettings, // TODO: this setting affects fetching and generation of the assertion; needs clarification /// Whether to create [`CertificateStatus`] assertions for manifests to store certificate revocation /// status. The assertion can be fetched for the active manifest or for all manifests (including /// ingredients). /// /// The default is to not fetch them at all. /// /// For more information, see [Certificate status assertion - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#certificate_status_assertion). /// /// [`CertificateStatus`]: crate::assertions::CertificateStatus #[serde(default, deserialize_with = "deserialize_case_insensitive_opt")] pub(crate) certificate_status_fetch: Option, // TODO: this setting affects fetching and generation of the assertion; needs clarification /// Whether to only use [`CertificateStatus`] assertions to check certificate revocation status. If there /// is a stapled OCSP in the COSE claim of the manifest, it will be ignored. If [`Verify::ocsp_fetch`] is /// enabled, it will also be ignored. /// /// The default value is false. /// /// [`CertificateStatus`]: crate::assertions::CertificateStatus /// [`Verify::ocsp_fetch`]: crate::settings::Verify::ocsp_fetch pub(crate) certificate_status_should_override: Option, /// The default [`BuilderIntent`] for the [`Builder`]. /// /// See [`BuilderIntent`] for more information. /// /// [`BuilderIntent`]: crate::BuilderIntent /// [`Builder`]: crate::Builder #[serde(default, deserialize_with = "deserialize_case_insensitive_opt")] pub intent: Option, /// Assertions with a base label included in this list will be automatically marked as a created assertion. /// Assertions not in this list will be automatically marked as gathered. /// /// Note that the label should be a **base label**, not including the assertion version nor instance. /// /// See more information on the difference between created vs gathered assertions in the spec here: /// [fields - C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_fields) pub created_assertion_labels: Option>, /// When `true`, use [`BoxHash`] instead of [`crate::assertions::DataHash`] for formats /// that support it (JPEG, PNG, GIF, etc.) when no explicit hard binding assertion has /// been set. /// /// Formats that support `BoxHash` can embed the C2PA manifest as a new chunk/segment /// without shifting existing byte offsets, so a placeholder is never required. /// Setting this to `true` enables the direct workflow (`Builder::sign_embeddable` /// Mode 2) for those formats and makes `Builder::needs_placeholder` return `false`. /// /// Defaults to `false` to preserve existing behaviour until `BoxHash` support is /// more widely tested. Set to `true` (or configure it per-[`Context`]) whenever /// you are ready to prefer box-based hashing for supported formats. /// /// [`BoxHash`]: crate::assertions::BoxHash /// [`Context`]: crate::Context pub prefer_box_hash: bool, /// Whether to generate a C2PA archive (instead of zip) when writing the manifest builder. /// Now always defaults to true - the ability to disable it will be removed in the future. pub generate_c2pa_archive: Option, /// Settings for configuring auto-generation of the [`TimeStamp`] assertion. /// /// [`TimeStamp`]: crate::assertions::TimeStamp pub auto_timestamp_assertion: TimeStampSettings, /// Whether `/free` and `/skip` boxes are excluded from the BMFF/MP4 hard-binding hash. /// /// `/free` and `/skip` are reserved/padding space that apps commonly rewrite after /// signing (e.g. to reclaim or repurpose it), so the C2PA spec permits excluding /// them. Set to `false` to fold their content into the hash instead, so any later /// edit to either box invalidates the hard binding like any other content change. /// /// The default value is `true`. pub bmff_hash_exclude_free_and_skip_boxes: bool, /// Whether to ignore errors encountered while loading or validating an [`Ingredient`]'s /// manifest (e.g. invalid file format). /// /// When enabled, a hard error is reported as a `general.error` in the ingredient's /// [`validation_results`] instead of being returned, so the [`Ingredient`] still loads /// and callers can inspect what went wrong. /// /// The default value is false. /// /// [`Ingredient`]: crate::Ingredient /// [`validation_results`]: crate::Ingredient::validation_results pub ignore_ingredient_errors: bool, } impl Default for BuilderSettings { fn default() -> Self { BuilderSettings { vendor: None, claim_generator_info: None, thumbnail: ThumbnailSettings::default(), actions: ActionsSettings::default(), certificate_status_fetch: None, certificate_status_should_override: None, intent: None, created_assertion_labels: None, prefer_box_hash: false, generate_c2pa_archive: Some(true), auto_timestamp_assertion: TimeStampSettings::default(), bmff_hash_exclude_free_and_skip_boxes: true, ignore_ingredient_errors: false, } } } /// The scope of which manifests to fetch for OCSP. #[cfg_attr(feature = "json_schema", derive(JsonSchema))] #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub(crate) enum OcspFetchScope { /// Fetch OCSP for all manifests. All, /// Fetch OCSP for the active manifest only. Active, } impl SettingsValidate for BuilderSettings { fn validate(&self) -> Result<()> { self.actions.validate()?; self.thumbnail.validate() } } #[cfg(test)] pub mod tests { #![allow(clippy::unwrap_used)] use super::*; use crate::assertions::DigitalSourceType; #[test] fn test_auto_created_action_without_source_type() { let actions_settings = ActionsSettings { auto_created_action: AutoActionSettings { enabled: true, source_type: None, }, ..Default::default() }; assert!(actions_settings.validate().is_err()); } #[test] fn test_auto_created_action_with_source_type() { let actions_settings = ActionsSettings { auto_created_action: AutoActionSettings { enabled: true, source_type: Some(DigitalSourceType::Empty), }, ..Default::default() }; assert!(actions_settings.validate().is_ok()); } #[test] fn test_claim_generator_info_try_from() { // Test basic conversion let settings = ClaimGeneratorInfoSettings { name: "Test Generator".to_string(), version: Some("1.0.0".to_string()), icon: None, operating_system: None, other: HashMap::new(), }; let info = ClaimGeneratorInfo::try_from(settings).unwrap(); assert_eq!(info.name, "Test Generator"); assert_eq!(info.version, Some("1.0.0".to_string())); // Test with auto OS detection let settings = ClaimGeneratorInfoSettings { name: "Test Generator".to_string(), version: None, icon: None, operating_system: Some(ClaimGeneratorInfoOperatingSystem::Auto), other: HashMap::new(), }; let info = ClaimGeneratorInfo::try_from(settings).unwrap(); let os = info.operating_system.unwrap(); assert!(os.contains(consts::ARCH) && os.contains(consts::OS)); // Test with custom OS, icon, and other fields let icon_ref = ResourceRef::new("image/png".to_string(), "icon.png".to_string()); let mut other = HashMap::new(); other.insert("custom".to_string(), serde_json::json!("value")); let settings = ClaimGeneratorInfoSettings { name: "Test Generator".to_string(), version: Some("2.0.0".to_string()), icon: Some(icon_ref.clone()), operating_system: Some(ClaimGeneratorInfoOperatingSystem::Other( "x86_64-pc-windows-msvc".to_string(), )), other, }; let info = ClaimGeneratorInfo::try_from(settings).unwrap(); assert_eq!( info.operating_system, Some("x86_64-pc-windows-msvc".to_string()) ); assert!(matches!(info.icon, Some(UriOrResource::ResourceRef(_)))); assert_eq!(info.other.len(), 1); // Test reference conversion let settings = ClaimGeneratorInfoSettings { name: "Test Generator".to_string(), version: Some("1.5.0".to_string()), icon: None, operating_system: None, other: HashMap::new(), }; let info = ClaimGeneratorInfo::try_from(&settings).unwrap(); assert_eq!(info.name, "Test Generator"); assert_eq!(settings.name, "Test Generator"); // Original still valid } #[test] fn test_action_template_try_from() { // Test basic conversion let settings = ActionTemplateSettings { action: "c2pa.created".to_string(), software_agent: None, software_agent_index: None, source_type: None, icon: None, description: None, template_parameters: None, }; let template = ActionTemplate::try_from(settings).unwrap(); assert_eq!(template.action, "c2pa.created"); assert!(template.software_agent.is_none()); // Test with software agent and parameters let mut params = HashMap::new(); params.insert("param1".to_string(), serde_json::json!("value1")); let software_agent = ClaimGeneratorInfoSettings { name: "Test Agent".to_string(), version: Some("1.0.0".to_string()), icon: None, operating_system: None, other: HashMap::new(), }; let settings = ActionTemplateSettings { action: "c2pa.edited".to_string(), software_agent: Some(software_agent), software_agent_index: Some(0), source_type: Some(DigitalSourceType::TrainedAlgorithmicMedia), icon: None, description: Some("Test template".to_string()), template_parameters: Some(params), }; let template = ActionTemplate::try_from(settings).unwrap(); assert_eq!(template.action, "c2pa.edited"); assert!(template.software_agent.is_some()); assert!(template.template_parameters.is_some()); } #[test] fn test_action_try_from() { // Test basic conversion let settings = ActionSettings { action: "c2pa.opened".to_string(), when: None, software_agent: None, software_agent_index: None, changes: None, parameters: None, source_type: None, related: None, reason: None, description: None, }; let action = Action::try_from(settings).unwrap(); assert_eq!(action.action, "c2pa.opened"); assert!(action.software_agent.is_none()); // Test with software agent and other fields let software_agent = ClaimGeneratorInfoSettings { name: "Editor Pro".to_string(), version: Some("2.0.0".to_string()), icon: None, operating_system: Some(ClaimGeneratorInfoOperatingSystem::Auto), other: HashMap::new(), }; let settings = ActionSettings { action: "c2pa.edited".to_string(), when: None, software_agent: Some(software_agent), software_agent_index: None, changes: None, parameters: None, source_type: Some(DigitalSourceType::CompositeWithTrainedAlgorithmicMedia), related: None, reason: Some("Privacy concerns".to_string()), description: Some("Edited with filters".to_string()), }; let action = Action::try_from(settings).unwrap(); assert_eq!(action.action, "c2pa.edited"); assert!(matches!( action.software_agent, Some(SoftwareAgent::ClaimGeneratorInfo(_)) )); assert_eq!(action.reason, Some("Privacy concerns".to_string())); } // For backwards compatibility with the `config` crate. #[test] fn test_case_insensitive_enum_deserialization() { use crate::settings::Settings; let settings = Settings::new() .with_json(r#"{"builder": {"thumbnail": {"format": "PNG", "quality": "HIGH"}, "intent": "EDIT"}}"#) .unwrap(); assert_eq!( settings.builder.thumbnail.format, Some(ThumbnailFormat::Png) ); assert_eq!(settings.builder.thumbnail.quality, ThumbnailQuality::High); assert_eq!(settings.builder.intent, Some(BuilderIntent::Edit)); let settings = Settings::new() .with_json(r#"{"builder": {"intent": {"create": "empty"}}}"#) .unwrap(); assert_eq!( settings.builder.intent, Some(BuilderIntent::Create(DigitalSourceType::Empty)) ); let result = Settings::new().with_json(r#"{"builder": {"intent": {"Create": "empty"}}}"#); assert!(result.is_err()); } }