// 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());
}
}