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::SettingsValidate,
ClaimGeneratorInfo, Error, ResourceRef, Result,
};
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ThumbnailFormat {
Png,
Jpeg,
Gif,
WebP,
Tiff,
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ThumbnailQuality {
Low,
Medium,
High,
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ThumbnailSettings {
pub enabled: bool,
pub ignore_errors: bool,
pub long_edge: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ThumbnailFormat>,
pub prefer_smallest_format: bool,
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(())
}
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct AutoActionSettings {
pub enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_type: Option<DigitalSourceType>,
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(untagged, rename_all = "lowercase")]
pub enum ClaimGeneratorInfoOperatingSystem {
Auto,
Other(String),
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ClaimGeneratorInfoSettings {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) icon: Option<ResourceRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operating_system: Option<ClaimGeneratorInfoOperatingSystem>,
#[serde(flatten)]
pub other: HashMap<String, serde_json::Value>,
}
impl TryFrom<ClaimGeneratorInfoSettings> for ClaimGeneratorInfo {
type Error = Error;
fn try_from(value: ClaimGeneratorInfoSettings) -> Result<Self> {
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,
})
},
other: value
.other
.into_iter()
.map(|(key, value)| {
serde_json::to_value(value)
.map(|value| (key, value))
.map_err(|err| err.into())
})
.collect::<Result<HashMap<String, serde_json::Value>>>()?,
})
}
}
impl TryFrom<&ClaimGeneratorInfoSettings> for ClaimGeneratorInfo {
type Error = Error;
fn try_from(value: &ClaimGeneratorInfoSettings) -> Result<Self> {
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(),
})
},
other: value
.other
.iter()
.map(|(key, value)| {
serde_json::to_value(value)
.map(|value| (key.clone(), value))
.map_err(|err| err.into())
})
.collect::<Result<HashMap<String, serde_json::Value>>>()?,
})
}
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub(crate) struct ActionTemplateSettings {
pub action: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub software_agent: Option<ClaimGeneratorInfoSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub software_agent_index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_type: Option<DigitalSourceType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<ResourceRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template_parameters: Option<HashMap<String, serde_json::Value>>,
}
impl TryFrom<ActionTemplateSettings> for ActionTemplate {
type Error = Error;
fn try_from(value: ActionTemplateSettings) -> Result<Self> {
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::<Result<HashMap<String, c2pa_cbor::Value>>>()
})
.transpose()?,
})
}
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub(crate) struct ActionSettings {
pub action: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub when: Option<DateT>,
#[serde(skip_serializing_if = "Option::is_none")]
pub software_agent: Option<ClaimGeneratorInfoSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub software_agent_index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub changes: Option<Vec<RegionOfInterest>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<ActionParameters>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_type: Option<DigitalSourceType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub related: Option<Vec<Action>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl TryFrom<ActionSettings> for Action {
type Error = Error;
fn try_from(value: ActionSettings) -> Result<Self> {
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()
})
}
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ActionsSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub all_actions_included: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) templates: Option<Vec<ActionTemplateSettings>>,
#[cfg_attr(feature = "json_schema", schemars(skip))]
pub(crate) actions: Option<Vec<ActionSettings>>,
pub auto_created_action: AutoActionSettings,
pub auto_opened_action: AutoActionSettings,
pub auto_placed_action: AutoActionSettings,
}
impl Default for ActionsSettings {
fn default() -> Self {
ActionsSettings {
all_actions_included: None,
templates: None,
actions: None,
auto_created_action: AutoActionSettings {
enabled: false,
source_type: None, },
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(()),
}
}
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum TimeStampFetchScope {
Parent,
All,
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct TimeStampSettings {
pub enabled: bool,
pub skip_existing: bool,
pub fetch_scope: TimeStampFetchScope,
}
impl Default for TimeStampSettings {
fn default() -> Self {
Self {
enabled: false,
skip_existing: true,
fetch_scope: TimeStampFetchScope::All,
}
}
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct BuilderSettings {
pub vendor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claim_generator_info: Option<ClaimGeneratorInfoSettings>,
pub thumbnail: ThumbnailSettings,
pub actions: ActionsSettings,
pub(crate) certificate_status_fetch: Option<OcspFetchScope>,
pub(crate) certificate_status_should_override: Option<bool>,
pub intent: Option<BuilderIntent>,
pub created_assertion_labels: Option<Vec<String>>,
pub prefer_box_hash: bool,
pub generate_c2pa_archive: Option<bool>,
pub auto_timestamp_assertion: TimeStampSettings,
}
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(),
}
}
}
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum OcspFetchScope {
All,
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() {
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()));
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));
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);
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"); }
#[test]
fn test_action_template_try_from() {
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());
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() {
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());
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()));
}
}