use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum DmiValue {
#[default]
Unspecified,
Allowed,
ProhibitedAiMlTraining,
ProhibitedGenAiMlTraining,
ProhibitedExceptSearchEngineIndexing,
Prohibited,
ProhibitedSeeConstraints,
}
impl DmiValue {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
DmiValue::Unspecified => "Unspecified",
DmiValue::Allowed => "Allowed",
DmiValue::ProhibitedAiMlTraining => "ProhibitedAiMlTraining",
DmiValue::ProhibitedGenAiMlTraining => "ProhibitedGenAiMlTraining",
DmiValue::ProhibitedExceptSearchEngineIndexing => {
"ProhibitedExceptSearchEngineIndexing"
}
DmiValue::Prohibited => "Prohibited",
DmiValue::ProhibitedSeeConstraints => "ProhibitedSeeConstraints",
}
}
pub fn to_iptc_property(&self) -> &'static str {
match self {
DmiValue::Unspecified => "Iptc4xmpExt:DMI",
DmiValue::Allowed => "Iptc4xmpExt:DMI-Allowed",
DmiValue::ProhibitedAiMlTraining => "Iptc4xmpExt:DMI-Prohibited",
DmiValue::ProhibitedGenAiMlTraining => "Iptc4xmpExt:DMI-Prohibited",
DmiValue::ProhibitedExceptSearchEngineIndexing => "Iptc4xmpExt:DMI-Prohibited",
DmiValue::Prohibited => "Iptc4xmpExt:DMI-Prohibited",
DmiValue::ProhibitedSeeConstraints => "Iptc4xmpExt:DMI-Prohibited",
}
}
#[must_use]
pub fn plus_vocab_key(self) -> &'static str {
match self {
DmiValue::Unspecified => "DMI-UNSPECIFIED",
DmiValue::Allowed => "DMI-ALLOWED",
DmiValue::ProhibitedAiMlTraining => "DMI-PROHIBITED-AIMLTRAINING",
DmiValue::ProhibitedGenAiMlTraining => "DMI-PROHIBITED-GENAIMLTRAINING",
DmiValue::ProhibitedExceptSearchEngineIndexing => {
"DMI-PROHIBITED-EXCEPTSEARCHENGINEINDEXING"
}
DmiValue::Prohibited => "DMI-PROHIBITED",
DmiValue::ProhibitedSeeConstraints => "DMI-PROHIBITED-SEECONSTRAINT",
}
}
#[must_use]
pub fn from_plus_vocab_key(key: &str) -> Option<Self> {
let bare = key.rsplit('/').next().unwrap_or(key);
match bare {
"DMI-UNSPECIFIED" => Some(DmiValue::Unspecified),
"DMI-ALLOWED" => Some(DmiValue::Allowed),
"DMI-PROHIBITED-AIMLTRAINING" => Some(DmiValue::ProhibitedAiMlTraining),
"DMI-PROHIBITED-GENAIMLTRAINING" => Some(DmiValue::ProhibitedGenAiMlTraining),
"DMI-PROHIBITED-EXCEPTSEARCHENGINEINDEXING" => {
Some(DmiValue::ProhibitedExceptSearchEngineIndexing)
}
"DMI-PROHIBITED" => Some(DmiValue::Prohibited),
"DMI-PROHIBITED-SEECONSTRAINT" => Some(DmiValue::ProhibitedSeeConstraints),
_ => None,
}
}
}
pub const PLUS_NAMESPACE: &str = "http://ns.useplus.org/ldf/xmp/1.0/";
pub const PLUS_DATA_MINING_PROPERTY: &str = "plus:DataMining";
#[deprecated(
since = "0.4.0",
note = "Use ProtectionPreset instead. EvidenceProfile only changes warning interpretation; ProtectionPreset controls actual processing behavior."
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum EvidenceProfile {
#[default]
LegalNotice,
LegalNoticeWithStego,
AuthenticatedProvenance,
Maximal,
}
#[allow(deprecated)]
impl EvidenceProfile {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
EvidenceProfile::LegalNotice => "legal-notice",
EvidenceProfile::LegalNoticeWithStego => "legal-notice-stego",
EvidenceProfile::AuthenticatedProvenance => "authenticated-provenance",
EvidenceProfile::Maximal => "maximal",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MetadataUpdatePolicy {
#[default]
ReplaceStegoOwned,
FailOnConflict,
PreserveExisting,
}
impl MetadataUpdatePolicy {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
MetadataUpdatePolicy::ReplaceStegoOwned => "replace-stego-owned",
MetadataUpdatePolicy::FailOnConflict => "fail-on-conflict",
MetadataUpdatePolicy::PreserveExisting => "preserve-existing",
}
}
}
impl std::fmt::Display for MetadataUpdatePolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MetadataUpdatePolicy::ReplaceStegoOwned => write!(f, "ReplaceStegoOwned"),
MetadataUpdatePolicy::FailOnConflict => write!(f, "FailOnConflict"),
MetadataUpdatePolicy::PreserveExisting => write!(f, "PreserveExisting"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum ProtectionLevel {
Disabled,
Light,
#[default]
Standard,
}
impl ProtectionLevel {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
ProtectionLevel::Disabled => "disabled",
ProtectionLevel::Light => "light",
ProtectionLevel::Standard => "standard",
}
}
#[must_use]
pub fn to_byte(&self) -> u8 {
match self {
ProtectionLevel::Disabled => 0,
ProtectionLevel::Light => 1,
ProtectionLevel::Standard => 2,
}
}
#[must_use]
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(ProtectionLevel::Disabled),
1 => Some(ProtectionLevel::Light),
2 => Some(ProtectionLevel::Standard),
_ => None,
}
}
#[must_use]
pub fn to_request(&self, notice: RightsNotice, policy: RightsPolicy) -> ProtectionRequest {
match self {
ProtectionLevel::Disabled => {
ProtectionRequest::new(
notice,
policy,
ProtectionChannels {
rights_metadata: false,
hidden_marker: HiddenMarkerMode::Disabled,
authentication: AuthenticationMode::None,
},
)
}
ProtectionLevel::Light => {
ProtectionRequest::new(notice, policy, ProtectionChannels::with_hidden_marker())
}
ProtectionLevel::Standard => {
ProtectionRequest::new(notice, policy, ProtectionChannels::with_hidden_marker())
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum ImageOutputFormat {
#[default]
Png,
Jpeg,
WebP,
}
pub const DEFAULT_OUTPUT_FORMAT: ImageOutputFormat = ImageOutputFormat::Png;
impl ImageOutputFormat {
#[must_use]
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"png" => Some(ImageOutputFormat::Png),
"jpg" | "jpeg" => Some(ImageOutputFormat::Jpeg),
"webp" => Some(ImageOutputFormat::WebP),
_ => None,
}
}
#[must_use]
pub fn from_magic_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 4 {
return None;
}
if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
return Some(ImageOutputFormat::Png);
}
if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
return Some(ImageOutputFormat::Jpeg);
}
if bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
return Some(ImageOutputFormat::WebP);
}
None
}
#[must_use]
pub fn is_png(bytes: &[u8]) -> bool {
bytes.len() >= 4 && bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47])
}
#[must_use]
pub fn is_jpeg(bytes: &[u8]) -> bool {
bytes.len() >= 3 && bytes.starts_with(&[0xFF, 0xD8, 0xFF])
}
#[must_use]
pub fn is_webp(bytes: &[u8]) -> bool {
bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP"
}
pub fn extension(&self) -> &'static str {
match self {
ImageOutputFormat::Png => "png",
ImageOutputFormat::Jpeg => "jpg",
ImageOutputFormat::WebP => "webp",
}
}
#[must_use]
pub fn to_image_format(self) -> image::ImageFormat {
match self {
ImageOutputFormat::Png => image::ImageFormat::Png,
ImageOutputFormat::Jpeg => image::ImageFormat::Jpeg,
ImageOutputFormat::WebP => image::ImageFormat::WebP,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalizedText {
text: String,
#[serde(default = "default_lang")]
lang: String,
}
fn default_lang() -> String {
"x-default".to_string()
}
impl LocalizedText {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
lang: default_lang(),
}
}
#[must_use]
pub fn with_lang(text: impl Into<String>, lang: impl Into<String>) -> Self {
Self {
text: text.into(),
lang: lang.into(),
}
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn lang(&self) -> &str {
&self.lang
}
}
impl From<String> for LocalizedText {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for LocalizedText {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl std::fmt::Display for LocalizedText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text)
}
}
#[derive(Debug, Clone, Default)]
pub struct RightsNotice {
copyright_holder: Option<String>,
contact_email: Option<String>,
license_url: Option<String>,
usage_terms: Option<String>,
usage_terms_lang: Option<String>,
creation_date: Option<String>,
ai_constraints: Option<String>,
web_statement_of_rights: Option<String>,
creator: Option<String>,
credit_line: Option<String>,
copyright_owner: Option<String>,
licensor_name: Option<String>,
licensor_email: Option<String>,
licensor_url: Option<String>,
metadata_date: Option<String>,
notice_applied_at: Option<String>,
dmi: Option<DmiValue>,
seed: Option<u64>,
}
impl RightsNotice {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn copyright_holder(&self) -> Option<&str> {
self.copyright_holder.as_deref()
}
#[must_use]
pub fn contact_email(&self) -> Option<&str> {
self.contact_email.as_deref()
}
#[must_use]
pub fn license_url(&self) -> Option<&str> {
self.license_url.as_deref()
}
#[must_use]
pub fn usage_terms(&self) -> Option<&str> {
self.usage_terms.as_deref()
}
#[must_use]
pub fn usage_terms_lang(&self) -> Option<&str> {
self.usage_terms_lang.as_deref()
}
#[must_use]
pub fn creation_date(&self) -> Option<&str> {
self.creation_date.as_deref()
}
#[must_use]
pub fn ai_constraints(&self) -> Option<&str> {
self.ai_constraints.as_deref()
}
#[must_use]
pub fn web_statement_of_rights(&self) -> Option<&str> {
self.web_statement_of_rights.as_deref()
}
#[must_use]
pub fn creator(&self) -> Option<&str> {
self.creator.as_deref()
}
#[must_use]
pub fn credit_line(&self) -> Option<&str> {
self.credit_line.as_deref()
}
#[must_use]
pub fn copyright_owner(&self) -> Option<&str> {
self.copyright_owner.as_deref()
}
#[must_use]
pub fn licensor_name(&self) -> Option<&str> {
self.licensor_name.as_deref()
}
#[must_use]
pub fn licensor_email(&self) -> Option<&str> {
self.licensor_email.as_deref()
}
#[must_use]
pub fn licensor_url(&self) -> Option<&str> {
self.licensor_url.as_deref()
}
#[must_use]
pub fn metadata_date(&self) -> Option<&str> {
self.metadata_date.as_deref()
}
#[must_use]
pub fn notice_applied_at(&self) -> Option<&str> {
self.notice_applied_at.as_deref()
}
#[must_use]
pub fn dmi(&self) -> Option<DmiValue> {
self.dmi
}
#[must_use]
pub fn seed(&self) -> Option<u64> {
self.seed
}
#[must_use]
pub fn has_legal_content(&self) -> bool {
self.copyright_holder.is_some()
|| self.contact_email.is_some()
|| self.license_url.is_some()
|| self.usage_terms.is_some()
|| self.creation_date.is_some()
|| self.ai_constraints.is_some()
|| self.web_statement_of_rights.is_some()
|| self.creator.is_some()
|| self.credit_line.is_some()
|| self.copyright_owner.is_some()
|| self.licensor_name.is_some()
|| self.licensor_email.is_some()
|| self.licensor_url.is_some()
|| self.metadata_date.is_some()
|| self.notice_applied_at.is_some()
}
#[must_use]
pub fn with_copyright_holder(mut self, holder: impl Into<String>) -> Self {
self.copyright_holder = Some(holder.into());
self
}
#[must_use]
pub fn with_contact_email(mut self, email: impl Into<String>) -> Self {
self.contact_email = Some(email.into());
self
}
#[must_use]
pub fn with_license_url(mut self, url: impl Into<String>) -> Self {
self.license_url = Some(url.into());
self
}
#[must_use]
pub fn with_usage_terms(mut self, terms: impl Into<String>) -> Self {
self.usage_terms = Some(terms.into());
self
}
#[must_use]
pub fn with_creation_date(mut self, date: impl Into<String>) -> Self {
self.creation_date = Some(date.into());
self
}
#[must_use]
pub fn with_ai_constraints(mut self, constraints: impl Into<String>) -> Self {
self.ai_constraints = Some(constraints.into());
self
}
#[must_use]
pub fn with_web_statement_of_rights(mut self, statement: impl Into<String>) -> Self {
self.web_statement_of_rights = Some(statement.into());
self
}
#[must_use]
pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
self.creator = Some(creator.into());
self
}
#[must_use]
pub fn with_credit_line(mut self, line: impl Into<String>) -> Self {
self.credit_line = Some(line.into());
self
}
#[must_use]
pub fn with_copyright_owner(mut self, owner: impl Into<String>) -> Self {
self.copyright_owner = Some(owner.into());
self
}
#[must_use]
pub fn with_licensor_name(mut self, name: impl Into<String>) -> Self {
self.licensor_name = Some(name.into());
self
}
#[must_use]
pub fn with_licensor_email(mut self, email: impl Into<String>) -> Self {
self.licensor_email = Some(email.into());
self
}
#[must_use]
pub fn with_licensor_url(mut self, url: impl Into<String>) -> Self {
self.licensor_url = Some(url.into());
self
}
#[must_use]
pub fn with_metadata_date(mut self, date: impl Into<String>) -> Self {
self.metadata_date = Some(date.into());
self
}
#[must_use]
pub fn with_notice_applied_at(mut self, timestamp: impl Into<String>) -> Self {
self.notice_applied_at = Some(timestamp.into());
self
}
#[must_use]
pub fn with_dmi(mut self, dmi: DmiValue) -> Self {
self.dmi = Some(dmi);
self
}
#[must_use]
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LegalMetadata {
copyright_holder: Option<String>,
contact_email: Option<String>,
license_url: Option<String>,
usage_terms: Option<String>,
usage_terms_lang: Option<String>,
creation_date: Option<String>,
ai_constraints: Option<String>,
web_statement_of_rights: Option<String>,
creator: Option<String>,
credit_line: Option<String>,
copyright_owner: Option<String>,
licensor_name: Option<String>,
licensor_email: Option<String>,
licensor_url: Option<String>,
metadata_date: Option<String>,
notice_applied_at: Option<String>,
}
impl LegalMetadata {
pub const MAX_FIELD_LEN: usize = 8192;
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> crate::Result<()> {
let check = |name: &str, val: &Option<String>| -> crate::Result<()> {
if let Some(v) = val {
if v.len() > Self::MAX_FIELD_LEN {
return Err(crate::Error::Config(format!(
"Legal metadata field '{}' exceeds maximum length of {} bytes (got {})",
name,
Self::MAX_FIELD_LEN,
v.len()
)));
}
}
Ok(())
};
let check_url = |name: &str, val: &Option<String>| -> crate::Result<()> {
if let Some(v) = val {
Self::validate_url_syntax(name, v)?;
}
Ok(())
};
check("copyright_holder", &self.copyright_holder)?;
check("contact_email", &self.contact_email)?;
check_url("license_url", &self.license_url)?;
check("usage_terms", &self.usage_terms)?;
check("creation_date", &self.creation_date)?;
check("ai_constraints", &self.ai_constraints)?;
check_url("web_statement_of_rights", &self.web_statement_of_rights)?;
check("creator", &self.creator)?;
check("credit_line", &self.credit_line)?;
check("copyright_owner", &self.copyright_owner)?;
check("licensor_name", &self.licensor_name)?;
check("licensor_email", &self.licensor_email)?;
check_url("licensor_url", &self.licensor_url)?;
check("metadata_date", &self.metadata_date)?;
check("notice_applied_at", &self.notice_applied_at)?;
let check_date = |name: &str, val: &Option<String>| -> crate::Result<()> {
if let Some(v) = val {
Self::validate_date(name, v)?;
}
Ok(())
};
check_date("creation_date", &self.creation_date)?;
check_date("metadata_date", &self.metadata_date)?;
check_date("notice_applied_at", &self.notice_applied_at)?;
Ok(())
}
fn validate_url_syntax(field_name: &str, url: &str) -> crate::Result<()> {
if url.is_empty() {
return Err(crate::Error::Config(format!(
"URL field '{}' must not be empty",
field_name
)));
}
let has_scheme = url.contains("://");
if !has_scheme {
return Err(crate::Error::Config(format!(
"URL field '{}' must include a scheme (e.g., https://): {}",
field_name, url
)));
}
let after_scheme = &url[url.find("://").unwrap() + 3..];
if after_scheme.is_empty() {
return Err(crate::Error::Config(format!(
"URL field '{}' must include an authority after the scheme: {}",
field_name, url
)));
}
Ok(())
}
fn validate_date(field_name: &str, value: &str) -> crate::Result<()> {
if value.is_empty() {
return Err(crate::Error::Config(format!(
"Date field '{}' must not be empty",
field_name
)));
}
let valid = match value.len() {
10 => {
value.as_bytes()[4] == b'-'
&& value.as_bytes()[7] == b'-'
&& value.bytes().enumerate().all(|(i, b)| match i {
4 | 7 => b == b'-',
_ => b.is_ascii_digit(),
})
}
20 => {
value.as_bytes()[4] == b'-'
&& value.as_bytes()[7] == b'-'
&& value.as_bytes()[10] == b'T'
&& value.as_bytes()[13] == b':'
&& value.as_bytes()[16] == b':'
&& value.as_bytes()[19] == b'Z'
&& value.bytes().enumerate().all(|(i, b)| match i {
4 | 7 => b == b'-',
10 => b == b'T',
13 | 16 => b == b':',
19 => b == b'Z',
_ => b.is_ascii_digit(),
})
}
25 => {
value.as_bytes()[4] == b'-'
&& value.as_bytes()[7] == b'-'
&& value.as_bytes()[10] == b'T'
&& value.as_bytes()[13] == b':'
&& value.as_bytes()[16] == b':'
&& (value.as_bytes()[19] == b'+' || value.as_bytes()[19] == b'-')
&& value.as_bytes()[22] == b':'
&& value.bytes().enumerate().all(|(i, b)| match i {
4 | 7 => b == b'-',
10 => b == b'T',
13 | 16 | 22 => b == b':',
19 => b == b'+' || b == b'-',
_ => b.is_ascii_digit(),
})
}
_ => false,
};
if !valid {
return Err(crate::Error::Config(format!(
"Date field '{}' must be ISO 8601 format (YYYY-MM-DD, YYYY-MM-DDTHH:MM:SSZ, \
or YYYY-MM-DDTHH:MM:SS+HH:MM): {}",
field_name, value
)));
}
Ok(())
}
#[must_use]
pub fn has_content(&self) -> bool {
self.copyright_holder.is_some()
|| self.contact_email.is_some()
|| self.license_url.is_some()
|| self.usage_terms.is_some()
|| self.creation_date.is_some()
|| self.ai_constraints.is_some()
|| self.web_statement_of_rights.is_some()
|| self.creator.is_some()
|| self.credit_line.is_some()
|| self.copyright_owner.is_some()
|| self.licensor_name.is_some()
|| self.licensor_email.is_some()
|| self.licensor_url.is_some()
|| self.metadata_date.is_some()
|| self.notice_applied_at.is_some()
}
#[must_use]
pub fn copyright_holder(&self) -> Option<&str> {
self.copyright_holder.as_deref()
}
#[must_use]
pub fn contact_email(&self) -> Option<&str> {
self.contact_email.as_deref()
}
#[must_use]
pub fn license_url(&self) -> Option<&str> {
self.license_url.as_deref()
}
#[must_use]
pub fn usage_terms(&self) -> Option<&str> {
self.usage_terms.as_deref()
}
#[must_use]
pub fn usage_terms_lang(&self) -> Option<&str> {
self.usage_terms_lang.as_deref()
}
#[must_use]
pub fn creation_date(&self) -> Option<&str> {
self.creation_date.as_deref()
}
#[must_use]
pub fn ai_constraints(&self) -> Option<&str> {
self.ai_constraints.as_deref()
}
#[must_use]
pub fn web_statement_of_rights(&self) -> Option<&str> {
self.web_statement_of_rights.as_deref()
}
#[must_use]
pub fn creator(&self) -> Option<&str> {
self.creator.as_deref()
}
#[must_use]
pub fn credit_line(&self) -> Option<&str> {
self.credit_line.as_deref()
}
#[must_use]
pub fn copyright_owner(&self) -> Option<&str> {
self.copyright_owner.as_deref()
}
#[must_use]
pub fn licensor_name(&self) -> Option<&str> {
self.licensor_name.as_deref()
}
#[must_use]
pub fn licensor_email(&self) -> Option<&str> {
self.licensor_email.as_deref()
}
#[must_use]
pub fn licensor_url(&self) -> Option<&str> {
self.licensor_url.as_deref()
}
#[must_use]
pub fn metadata_date(&self) -> Option<&str> {
self.metadata_date.as_deref()
}
#[must_use]
pub fn notice_applied_at(&self) -> Option<&str> {
self.notice_applied_at.as_deref()
}
#[must_use]
pub fn with_copyright_holder(mut self, holder: impl Into<String>) -> Self {
self.copyright_holder = Some(holder.into());
self
}
#[must_use]
pub fn with_contact_email(mut self, email: impl Into<String>) -> Self {
self.contact_email = Some(email.into());
self
}
#[must_use]
pub fn with_license_url(mut self, url: impl Into<String>) -> Self {
self.license_url = Some(url.into());
self
}
#[must_use]
pub fn with_usage_terms(mut self, terms: impl Into<String>) -> Self {
self.usage_terms = Some(terms.into());
self
}
#[must_use]
pub fn with_usage_terms_localized(mut self, terms: impl Into<LocalizedText>) -> Self {
let lt = terms.into();
self.usage_terms = Some(lt.text().to_string());
self.usage_terms_lang = Some(lt.lang().to_string());
self
}
#[must_use]
pub fn with_creation_date(mut self, date: impl Into<String>) -> Self {
self.creation_date = Some(date.into());
self
}
#[must_use]
pub fn with_ai_constraints(mut self, constraints: impl Into<String>) -> Self {
self.ai_constraints = Some(constraints.into());
self
}
#[must_use]
pub fn with_web_statement_of_rights(mut self, statement: impl Into<String>) -> Self {
self.web_statement_of_rights = Some(statement.into());
self
}
#[must_use]
pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
self.creator = Some(creator.into());
self
}
#[must_use]
pub fn with_credit_line(mut self, line: impl Into<String>) -> Self {
self.credit_line = Some(line.into());
self
}
#[must_use]
pub fn with_copyright_owner(mut self, owner: impl Into<String>) -> Self {
self.copyright_owner = Some(owner.into());
self
}
#[must_use]
pub fn with_licensor_name(mut self, name: impl Into<String>) -> Self {
self.licensor_name = Some(name.into());
self
}
#[must_use]
pub fn with_licensor_email(mut self, email: impl Into<String>) -> Self {
self.licensor_email = Some(email.into());
self
}
#[must_use]
pub fn with_licensor_url(mut self, url: impl Into<String>) -> Self {
self.licensor_url = Some(url.into());
self
}
#[must_use]
pub fn with_metadata_date(mut self, date: impl Into<String>) -> Self {
self.metadata_date = Some(date.into());
self
}
#[must_use]
pub fn with_notice_applied_at(mut self, ts: impl Into<String>) -> Self {
self.notice_applied_at = Some(ts.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProtectionConfig {
mac_key: Option<Vec<u8>>,
legal_metadata: Option<LegalMetadata>,
}
impl ProtectionConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_mac_key(mut self, key: Vec<u8>) -> Self {
self.mac_key = Some(key);
self
}
#[must_use]
pub fn with_legal_metadata(mut self, metadata: LegalMetadata) -> Self {
self.legal_metadata = Some(metadata);
self
}
#[must_use]
pub fn mac_key(&self) -> Option<&[u8]> {
self.mac_key.as_deref()
}
#[must_use]
pub fn legal_metadata(&self) -> Option<&LegalMetadata> {
self.legal_metadata.as_ref()
}
}
#[allow(deprecated)]
#[derive(Debug, Clone, Deserialize)]
pub struct ProtectionContext {
intensity: f32,
seed: u64,
input_format: Option<ImageOutputFormat>,
output_format: Option<ImageOutputFormat>,
protection_level: Option<ProtectionLevel>,
evidence_profile: Option<EvidenceProfile>,
dmi_value: Option<DmiValue>,
max_dimension: Option<u32>,
inject_metadata: Option<bool>,
inject_legal_claims: Option<bool>,
stego_redundancy: Option<usize>,
jpeg_quality: u8,
progressive_jpeg: bool,
tile_size: Option<u32>,
tile_extraction_max_origins: u32,
content_hash: Option<[u8; 4]>,
metadata_update_policy: Option<MetadataUpdatePolicy>,
#[serde(skip)]
timestamp_override: Option<String>,
#[serde(skip)]
config: Option<Arc<ProtectionConfig>>,
#[serde(skip)]
resource_limits: Option<crate::resource_limits::ResourceLimits>,
}
impl Serialize for ProtectionContext {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut fields = 17;
if self.config.is_some() {
fields += 1;
}
let mut s = serializer.serialize_struct("ProtectionContext", fields)?;
s.serialize_field("intensity", &self.intensity)?;
s.serialize_field("seed", &self.seed)?;
s.serialize_field("input_format", &self.input_format)?;
s.serialize_field("output_format", &self.output_format)?;
s.serialize_field("protection_level", &self.protection_level)?;
s.serialize_field("evidence_profile", &self.evidence_profile)?;
s.serialize_field("dmi_value", &self.dmi_value)?;
s.serialize_field("max_dimension", &self.max_dimension)?;
s.serialize_field("inject_metadata", &self.inject_metadata)?;
s.serialize_field("inject_legal_claims", &self.inject_legal_claims)?;
s.serialize_field("stego_redundancy", &self.stego_redundancy)?;
s.serialize_field("jpeg_quality", &self.jpeg_quality)?;
s.serialize_field("progressive_jpeg", &self.progressive_jpeg)?;
s.serialize_field("tile_size", &self.tile_size)?;
s.serialize_field(
"tile_extraction_max_origins",
&self.tile_extraction_max_origins,
)?;
s.serialize_field("content_hash", &self.content_hash)?;
s.serialize_field("metadata_update_policy", &self.metadata_update_policy)?;
if self.config.is_some() {
s.serialize_field(
"_config_dropped_warning",
"ProtectionContext.config is not serialized; MAC key and legal metadata will be lost on roundtrip. Set them again after deserialization.",
)?;
}
s.end()
}
}
impl Default for ProtectionContext {
fn default() -> Self {
let seed = crate::util::seed::generate_random_seed();
Self {
intensity: 0.5,
seed,
input_format: None,
output_format: None,
protection_level: None,
evidence_profile: None,
dmi_value: None,
max_dimension: None,
inject_metadata: None,
inject_legal_claims: None,
stego_redundancy: None,
jpeg_quality: 90,
progressive_jpeg: false,
tile_size: None,
tile_extraction_max_origins: 64,
content_hash: None,
metadata_update_policy: None,
timestamp_override: None,
config: None,
resource_limits: None,
}
}
}
impl ProtectionContext {
pub fn new(intensity: f32, seed: u64) -> Self {
Self {
intensity: intensity.clamp(0.0, 1.0),
seed,
input_format: None,
output_format: None,
protection_level: None,
evidence_profile: None,
dmi_value: None,
max_dimension: None,
inject_metadata: None,
inject_legal_claims: None,
stego_redundancy: None,
jpeg_quality: 90,
progressive_jpeg: false,
tile_size: None,
tile_extraction_max_origins: 64,
content_hash: None,
metadata_update_policy: None,
timestamp_override: None,
config: None,
resource_limits: None,
}
}
#[must_use]
pub fn with_config(mut self, config: Arc<ProtectionConfig>) -> Self {
self.config = Some(config);
self
}
#[must_use]
pub fn with_mac_key(mut self, key: Vec<u8>) -> Self {
let config = self
.config
.get_or_insert_with(|| Arc::new(ProtectionConfig::new()));
let mut builder = (**config).clone();
builder.mac_key = Some(key);
self.config = Some(Arc::new(builder));
self
}
#[must_use]
pub fn with_legal_metadata(mut self, metadata: LegalMetadata) -> Self {
let config = self
.config
.get_or_insert_with(|| Arc::new(ProtectionConfig::new()));
let mut builder = (**config).clone();
builder.legal_metadata = Some(metadata);
self.config = Some(Arc::new(builder));
self
}
#[must_use]
pub fn mac_key(&self) -> Option<&[u8]> {
self.config.as_ref().and_then(|c| c.mac_key.as_deref())
}
#[must_use]
pub fn legal_metadata(&self) -> Option<&LegalMetadata> {
self.config.as_ref().and_then(|c| c.legal_metadata.as_ref())
}
#[must_use]
pub fn with_max_dimension(mut self, max: u32) -> Self {
self.max_dimension = Some(max);
self
}
#[must_use]
pub fn with_format(mut self, format: ImageOutputFormat) -> Self {
self.output_format = Some(format);
self
}
#[must_use]
pub fn with_input_format(mut self, format: ImageOutputFormat) -> Self {
self.input_format = Some(format);
self
}
#[deprecated(
since = "0.4.0",
note = "Use RightsPolicy in ProtectionRequest instead. See ProtectionRequest::new()."
)]
#[must_use]
pub fn with_dmi(mut self, dmi: DmiValue) -> Self {
self.dmi_value = Some(dmi);
self
}
#[allow(deprecated)]
#[must_use]
pub fn with_evidence_profile(mut self, profile: EvidenceProfile) -> Self {
self.evidence_profile = Some(profile);
self
}
#[allow(deprecated)]
#[must_use]
pub fn evidence_profile(&self) -> EvidenceProfile {
self.evidence_profile
.unwrap_or(EvidenceProfile::LegalNotice)
}
#[allow(deprecated)]
#[must_use]
pub fn legal_notice() -> Self {
Self::default().with_evidence_profile(EvidenceProfile::LegalNotice)
}
#[allow(deprecated)]
#[must_use]
pub fn legal_notice_with_stego() -> Self {
Self::default().with_evidence_profile(EvidenceProfile::LegalNoticeWithStego)
}
#[allow(deprecated)]
#[must_use]
pub fn authenticated_provenance() -> Self {
Self::default().with_evidence_profile(EvidenceProfile::AuthenticatedProvenance)
}
#[allow(deprecated)]
#[must_use]
pub fn maximal() -> Self {
Self::default().with_evidence_profile(EvidenceProfile::Maximal)
}
#[deprecated(
since = "0.4.0",
note = "Use ProtectionChannels::metadata_only() or ProtectionRequest builder instead."
)]
#[must_use]
pub fn with_metadata_injection(mut self, enable: bool) -> Self {
self.inject_metadata = Some(enable);
self
}
#[must_use]
#[deprecated(
since = "0.2.2",
note = "Legal claims are auto-enabled when LegalMetadata is present. \
This method is redundant for the normal case and produces a \
ContradictoryLegalClaims warning when used with `false` \
while legal metadata is set."
)]
pub fn with_legal_claims(mut self, enable: bool) -> Self {
self.inject_legal_claims = Some(enable);
self
}
#[must_use]
pub fn with_intensity(mut self, intensity: f32) -> Self {
self.intensity = intensity.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
#[must_use]
pub fn with_stego_redundancy(mut self, redundancy: usize) -> Self {
self.stego_redundancy = Some(redundancy.clamp(1, 10));
self
}
#[must_use]
pub fn with_jpeg_quality(mut self, quality: u8) -> Self {
self.jpeg_quality = quality.clamp(1, 100);
self
}
#[must_use]
pub fn with_progressive_jpeg(mut self, progressive: bool) -> Self {
self.progressive_jpeg = progressive;
self
}
#[must_use]
pub fn with_tile_size(mut self, size: u32) -> Self {
if size == 0 {
self.tile_size = Some(0);
} else {
self.tile_size = Some(size.clamp(32, 1024));
}
self
}
#[must_use]
pub fn with_tile_extraction_max_origins(mut self, n: u32) -> Self {
self.tile_extraction_max_origins = n.clamp(1, 4096);
self
}
#[must_use]
pub fn with_content_hash(mut self, hash: [u8; 4]) -> Self {
self.content_hash = Some(hash);
self
}
#[must_use]
pub fn with_metadata_update_policy(mut self, policy: MetadataUpdatePolicy) -> Self {
self.metadata_update_policy = Some(policy);
self
}
#[must_use]
pub fn metadata_update_policy(&self) -> MetadataUpdatePolicy {
self.metadata_update_policy
.unwrap_or(MetadataUpdatePolicy::ReplaceStegoOwned)
}
#[must_use]
pub fn with_timestamp_override(mut self, ts: impl Into<String>) -> Self {
self.timestamp_override = Some(ts.into());
self
}
#[must_use]
pub fn with_resource_limits(mut self, limits: crate::resource_limits::ResourceLimits) -> Self {
self.resource_limits = Some(limits);
self
}
#[must_use]
pub fn resource_limits(&self) -> crate::resource_limits::ResourceLimits {
self.resource_limits.clone().unwrap_or_default()
}
#[must_use]
pub fn intensity(&self) -> f32 {
self.intensity
}
#[must_use]
pub fn seed(&self) -> u64 {
self.seed
}
#[must_use]
pub fn input_format(&self) -> Option<ImageOutputFormat> {
self.input_format
}
#[must_use]
pub fn output_format(&self) -> Option<ImageOutputFormat> {
self.output_format
}
#[must_use]
pub fn protection_level(&self) -> Option<ProtectionLevel> {
self.protection_level
}
#[must_use]
pub fn dmi_value(&self) -> Option<DmiValue> {
self.dmi_value
}
#[must_use]
pub fn max_dimension(&self) -> Option<u32> {
self.max_dimension
}
#[must_use]
pub fn inject_metadata(&self) -> Option<bool> {
self.inject_metadata
}
#[must_use]
pub fn inject_legal_claims(&self) -> Option<bool> {
self.inject_legal_claims
}
#[must_use]
pub fn stego_redundancy(&self) -> usize {
self.effective_redundancy()
}
pub(crate) fn effective_redundancy(&self) -> usize {
if let Some(r) = self.stego_redundancy {
return r;
}
let i = self.intensity;
if i < 0.3 {
1
} else if i < 0.7 {
2
} else {
3
}
}
#[must_use]
pub fn jpeg_quality(&self) -> u8 {
self.jpeg_quality
}
#[must_use]
pub fn progressive_jpeg(&self) -> bool {
self.progressive_jpeg
}
#[must_use]
pub fn tile_size(&self) -> Option<u32> {
self.tile_size
}
#[must_use]
pub fn is_tile_mode_enabled(&self) -> bool {
matches!(self.tile_size, Some(n) if n > 0)
}
#[must_use]
pub fn tile_extraction_max_origins(&self) -> u32 {
self.tile_extraction_max_origins.max(1)
}
#[must_use]
pub fn content_hash(&self) -> Option<[u8; 4]> {
self.content_hash
}
pub fn set_input_format(&mut self, format: ImageOutputFormat) {
self.input_format = Some(format);
}
pub(crate) fn set_tile_size(&mut self, size: u32) {
if size == 0 {
self.tile_size = Some(0);
} else {
self.tile_size = Some(size.clamp(32, 1024));
}
}
pub(crate) fn set_protection_level(&mut self, level: ProtectionLevel) {
self.protection_level = Some(level);
}
#[must_use]
pub fn normalize_rights_notice(&self) -> RightsNotice {
let legal = self.legal_metadata();
let dmi = self
.dmi_value()
.or_else(|| {
self.protection_level().and_then(|level| match level {
ProtectionLevel::Light => Some(DmiValue::Prohibited),
ProtectionLevel::Standard => Some(DmiValue::ProhibitedAiMlTraining),
_ => None,
})
})
.filter(|v| *v != DmiValue::Unspecified);
let notice_applied_at =
legal
.and_then(|l| l.notice_applied_at().map(String::from))
.or_else(|| {
if legal.is_some() {
Some(self.timestamp_override.clone().unwrap_or_else(
crate::protected::metadata_trap::current_timestamp_iso8601,
))
} else {
None
}
});
RightsNotice {
copyright_holder: legal.and_then(|l| l.copyright_holder().map(String::from)),
contact_email: legal.and_then(|l| l.contact_email().map(String::from)),
license_url: legal.and_then(|l| l.license_url().map(String::from)),
usage_terms: legal.and_then(|l| l.usage_terms().map(String::from)),
usage_terms_lang: legal.and_then(|l| l.usage_terms_lang().map(String::from)),
creation_date: legal.and_then(|l| l.creation_date().map(String::from)),
ai_constraints: legal.and_then(|l| l.ai_constraints().map(String::from)),
web_statement_of_rights: legal
.and_then(|l| l.web_statement_of_rights().map(String::from)),
creator: legal.and_then(|l| l.creator().map(String::from)),
credit_line: legal.and_then(|l| l.credit_line().map(String::from)),
copyright_owner: legal.and_then(|l| l.copyright_owner().map(String::from)),
licensor_name: legal.and_then(|l| l.licensor_name().map(String::from)),
licensor_email: legal.and_then(|l| l.licensor_email().map(String::from)),
licensor_url: legal.and_then(|l| l.licensor_url().map(String::from)),
metadata_date: legal.and_then(|l| l.metadata_date().map(String::from)),
notice_applied_at,
dmi,
seed: Some(self.seed()),
}
}
}
#[derive(Debug, Clone)]
pub enum VerificationResult {
Verified {
payload: crate::StegoPayload,
},
Corrupted {
payload: crate::StegoPayload,
},
MetadataOnly {
seed: u64,
},
NotFound,
}
impl VerificationResult {
#[must_use]
pub fn is_verified(&self) -> bool {
matches!(self, VerificationResult::Verified { .. })
}
#[must_use]
pub fn is_found(&self) -> bool {
!matches!(self, VerificationResult::NotFound)
}
#[must_use]
pub fn payload(&self) -> Option<&crate::StegoPayload> {
match self {
VerificationResult::Verified { payload } => Some(payload),
_ => None,
}
}
#[must_use]
pub fn metadata_seed(&self) -> Option<u64> {
match self {
VerificationResult::MetadataOnly { seed } => Some(*seed),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum VerificationStatus {
Verified,
Invalid,
NotFound,
}
impl std::fmt::Display for VerificationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VerificationStatus::Verified => write!(f, "Verified"),
VerificationStatus::Invalid => write!(f, "Invalid"),
VerificationStatus::NotFound => write!(f, "NotFound"),
}
}
}
impl From<Option<bool>> for VerificationStatus {
fn from(val: Option<bool>) -> Self {
match val {
Some(true) => VerificationStatus::Verified,
Some(false) => VerificationStatus::Invalid,
None => VerificationStatus::NotFound,
}
}
}
impl From<VerificationStatus> for Option<bool> {
fn from(val: VerificationStatus) -> Self {
match val {
VerificationStatus::Verified => Some(true),
VerificationStatus::Invalid => Some(false),
VerificationStatus::NotFound => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum EvidenceStrength {
NoNoticeFound,
MetadataNoticeOnly,
MetadataNoticeAndBestEffortStego,
MetadataNoticeAndAuthenticatedProvenance,
}
impl std::fmt::Display for EvidenceStrength {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EvidenceStrength::NoNoticeFound => write!(f, "NoNoticeFound"),
EvidenceStrength::MetadataNoticeOnly => write!(f, "MetadataNoticeOnly"),
EvidenceStrength::MetadataNoticeAndBestEffortStego => {
write!(f, "MetadataNoticeAndBestEffortStego")
}
EvidenceStrength::MetadataNoticeAndAuthenticatedProvenance => {
write!(f, "MetadataNoticeAndAuthenticatedProvenance")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum EvidenceChannel {
PngText,
PngXmp,
JpegComment,
JpegXmp,
JpegIptc,
WebPXmp,
WebPExif,
LsbPayload,
DctPayload,
QTableSeed,
}
impl std::fmt::Display for EvidenceChannel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EvidenceChannel::PngText => write!(f, "PngText"),
EvidenceChannel::PngXmp => write!(f, "PngXmp"),
EvidenceChannel::JpegComment => write!(f, "JpegComment"),
EvidenceChannel::JpegXmp => write!(f, "JpegXmp"),
EvidenceChannel::JpegIptc => write!(f, "JpegIptc"),
EvidenceChannel::WebPXmp => write!(f, "WebPXmp"),
EvidenceChannel::WebPExif => write!(f, "WebPExif"),
EvidenceChannel::LsbPayload => write!(f, "LsbPayload"),
EvidenceChannel::DctPayload => write!(f, "DctPayload"),
EvidenceChannel::QTableSeed => write!(f, "QTableSeed"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RightsSignalKind {
CanonicalPlusDataMining,
LegacyStegoEggoDmi,
LegacyTdmReservation,
Unknown,
}
impl std::fmt::Display for RightsSignalKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RightsSignalKind::CanonicalPlusDataMining => write!(f, "CanonicalPlusDataMining"),
RightsSignalKind::LegacyStegoEggoDmi => write!(f, "LegacyStegoEggoDmi"),
RightsSignalKind::LegacyTdmReservation => write!(f, "LegacyTdmReservation"),
RightsSignalKind::Unknown => write!(f, "Unknown"),
}
}
}
#[derive(Debug, Clone)]
pub struct NoticeVerification {
copyright_holder: Option<String>,
creator: Option<String>,
contact: Option<String>,
rights_url: Option<String>,
usage_terms: Option<String>,
ai_constraints: Option<String>,
dmi: Option<DmiValue>,
tdm_reserved: Option<bool>,
rights_signal_kind: RightsSignalKind,
canonical_dmi: Option<DmiValue>,
legacy_dmi: Option<DmiValue>,
protection_seed: Option<u64>,
stego_status: VerificationStatus,
stego_payload: Option<crate::StegoPayload>,
authenticated: bool,
evidence_strength: EvidenceStrength,
channels: Vec<EvidenceChannel>,
license_url: Option<String>,
web_statement_of_rights: Option<String>,
credit_line: Option<String>,
copyright_owner: Option<String>,
licensor_name: Option<String>,
licensor_email: Option<String>,
licensor_url: Option<String>,
metadata_date: Option<String>,
notice_applied_at: Option<String>,
}
impl NoticeVerification {
#[must_use]
pub fn copyright_holder(&self) -> Option<&str> {
self.copyright_holder.as_deref()
}
#[must_use]
pub fn creator(&self) -> Option<&str> {
self.creator.as_deref()
}
#[must_use]
pub fn contact(&self) -> Option<&str> {
self.contact.as_deref()
}
#[must_use]
pub fn rights_url(&self) -> Option<&str> {
self.rights_url
.as_deref()
.or(self.web_statement_of_rights.as_deref())
.or(self.license_url.as_deref())
}
#[must_use]
pub fn usage_terms(&self) -> Option<&str> {
self.usage_terms.as_deref()
}
#[must_use]
pub fn ai_constraints(&self) -> Option<&str> {
self.ai_constraints.as_deref()
}
#[must_use]
pub fn dmi(&self) -> Option<DmiValue> {
self.dmi
}
#[must_use]
pub fn tdm_reserved(&self) -> Option<bool> {
self.tdm_reserved
}
#[must_use]
pub fn rights_signal_kind(&self) -> RightsSignalKind {
self.rights_signal_kind
}
#[must_use]
pub fn canonical_dmi(&self) -> Option<DmiValue> {
self.canonical_dmi
}
#[must_use]
pub fn legacy_dmi(&self) -> Option<DmiValue> {
self.legacy_dmi
}
#[must_use]
pub fn has_dmi_conflict(&self) -> bool {
if let (Some(canonical), Some(legacy)) = (self.canonical_dmi, self.legacy_dmi) {
canonical != legacy
} else {
false
}
}
#[must_use]
pub fn protection_seed(&self) -> Option<u64> {
self.protection_seed
}
#[must_use]
pub fn stego_status(&self) -> VerificationStatus {
self.stego_status
}
#[must_use]
pub fn stego_payload(&self) -> Option<&crate::StegoPayload> {
self.stego_payload.as_ref()
}
#[must_use]
pub fn authenticated(&self) -> bool {
self.authenticated
}
#[must_use]
pub fn evidence_strength(&self) -> EvidenceStrength {
self.evidence_strength
}
#[must_use]
pub fn channels(&self) -> &[EvidenceChannel] {
&self.channels
}
#[must_use]
pub fn license_url(&self) -> Option<&str> {
self.license_url.as_deref()
}
#[must_use]
pub fn web_statement_of_rights(&self) -> Option<&str> {
self.web_statement_of_rights.as_deref()
}
#[must_use]
pub fn credit_line(&self) -> Option<&str> {
self.credit_line.as_deref()
}
#[must_use]
pub fn copyright_owner(&self) -> Option<&str> {
self.copyright_owner.as_deref()
}
#[must_use]
pub fn licensor_name(&self) -> Option<&str> {
self.licensor_name.as_deref()
}
#[must_use]
pub fn licensor_email(&self) -> Option<&str> {
self.licensor_email.as_deref()
}
#[must_use]
pub fn licensor_url(&self) -> Option<&str> {
self.licensor_url.as_deref()
}
#[must_use]
pub fn metadata_date(&self) -> Option<&str> {
self.metadata_date.as_deref()
}
#[must_use]
pub fn notice_applied_at(&self) -> Option<&str> {
self.notice_applied_at.as_deref()
}
#[must_use]
pub fn has_notice(&self) -> bool {
self.copyright_holder.is_some()
|| self.creator.is_some()
|| self.contact.is_some()
|| self.rights_url.is_some()
|| self.usage_terms.is_some()
|| self.ai_constraints.is_some()
|| self.dmi.is_some()
|| self.license_url.is_some()
|| self.web_statement_of_rights.is_some()
|| self.credit_line.is_some()
|| self.copyright_owner.is_some()
|| self.licensor_name.is_some()
|| self.licensor_email.is_some()
|| self.licensor_url.is_some()
|| self.metadata_date.is_some()
|| self.notice_applied_at.is_some()
}
#[deprecated(since = "0.2.2", note = "use NoticeVerificationBuilder instead")]
#[allow(clippy::too_many_arguments, dead_code)]
pub(crate) fn new(
copyright_holder: Option<String>,
creator: Option<String>,
contact: Option<String>,
rights_url: Option<String>,
usage_terms: Option<String>,
ai_constraints: Option<String>,
dmi: Option<DmiValue>,
tdm_reserved: Option<bool>,
rights_signal_kind: RightsSignalKind,
canonical_dmi: Option<DmiValue>,
legacy_dmi: Option<DmiValue>,
protection_seed: Option<u64>,
stego_status: VerificationStatus,
stego_payload: Option<crate::StegoPayload>,
authenticated: bool,
evidence_strength: EvidenceStrength,
channels: Vec<EvidenceChannel>,
license_url: Option<String>,
web_statement_of_rights: Option<String>,
credit_line: Option<String>,
copyright_owner: Option<String>,
licensor_name: Option<String>,
licensor_email: Option<String>,
licensor_url: Option<String>,
metadata_date: Option<String>,
notice_applied_at: Option<String>,
) -> Self {
Self {
copyright_holder,
creator,
contact,
rights_url,
usage_terms,
ai_constraints,
dmi,
tdm_reserved,
rights_signal_kind,
canonical_dmi,
legacy_dmi,
protection_seed,
stego_status,
stego_payload,
authenticated,
evidence_strength,
channels,
license_url,
web_statement_of_rights,
credit_line,
copyright_owner,
licensor_name,
licensor_email,
licensor_url,
metadata_date,
notice_applied_at,
}
}
#[must_use]
pub fn builder() -> NoticeVerificationBuilder {
NoticeVerificationBuilder::default()
}
}
#[derive(Debug, Clone)]
pub struct NoticeVerificationBuilder {
copyright_holder: Option<String>,
creator: Option<String>,
contact: Option<String>,
rights_url: Option<String>,
usage_terms: Option<String>,
ai_constraints: Option<String>,
dmi: Option<DmiValue>,
tdm_reserved: Option<bool>,
rights_signal_kind: RightsSignalKind,
canonical_dmi: Option<DmiValue>,
legacy_dmi: Option<DmiValue>,
protection_seed: Option<u64>,
stego_status: VerificationStatus,
stego_payload: Option<crate::StegoPayload>,
authenticated: bool,
evidence_strength: EvidenceStrength,
channels: Vec<EvidenceChannel>,
license_url: Option<String>,
web_statement_of_rights: Option<String>,
credit_line: Option<String>,
copyright_owner: Option<String>,
licensor_name: Option<String>,
licensor_email: Option<String>,
licensor_url: Option<String>,
metadata_date: Option<String>,
notice_applied_at: Option<String>,
}
impl Default for NoticeVerificationBuilder {
fn default() -> Self {
Self {
copyright_holder: None,
creator: None,
contact: None,
rights_url: None,
usage_terms: None,
ai_constraints: None,
dmi: None,
tdm_reserved: None,
rights_signal_kind: RightsSignalKind::Unknown,
canonical_dmi: None,
legacy_dmi: None,
protection_seed: None,
stego_status: VerificationStatus::NotFound,
stego_payload: None,
authenticated: false,
evidence_strength: EvidenceStrength::NoNoticeFound,
channels: Vec::new(),
license_url: None,
web_statement_of_rights: None,
credit_line: None,
copyright_owner: None,
licensor_name: None,
licensor_email: None,
licensor_url: None,
metadata_date: None,
notice_applied_at: None,
}
}
}
impl NoticeVerificationBuilder {
#[must_use]
pub fn copyright_holder(mut self, v: Option<String>) -> Self {
self.copyright_holder = v;
self
}
#[must_use]
pub fn creator(mut self, v: Option<String>) -> Self {
self.creator = v;
self
}
#[must_use]
pub fn contact(mut self, v: Option<String>) -> Self {
self.contact = v;
self
}
#[must_use]
pub fn rights_url(mut self, v: Option<String>) -> Self {
self.rights_url = v;
self
}
#[must_use]
pub fn usage_terms(mut self, v: Option<String>) -> Self {
self.usage_terms = v;
self
}
#[must_use]
pub fn ai_constraints(mut self, v: Option<String>) -> Self {
self.ai_constraints = v;
self
}
#[must_use]
pub fn dmi(mut self, v: Option<DmiValue>) -> Self {
self.dmi = v;
self
}
#[must_use]
pub fn tdm_reserved(mut self, v: Option<bool>) -> Self {
self.tdm_reserved = v;
self
}
#[must_use]
pub fn rights_signal_kind(mut self, v: RightsSignalKind) -> Self {
self.rights_signal_kind = v;
self
}
#[must_use]
pub fn canonical_dmi(mut self, v: Option<DmiValue>) -> Self {
self.canonical_dmi = v;
self
}
#[must_use]
pub fn legacy_dmi(mut self, v: Option<DmiValue>) -> Self {
self.legacy_dmi = v;
self
}
#[must_use]
pub fn protection_seed(mut self, v: Option<u64>) -> Self {
self.protection_seed = v;
self
}
#[must_use]
pub fn stego_status(mut self, v: VerificationStatus) -> Self {
self.stego_status = v;
self
}
#[must_use]
pub fn stego_payload(mut self, v: Option<crate::StegoPayload>) -> Self {
self.stego_payload = v;
self
}
#[must_use]
pub fn authenticated(mut self, v: bool) -> Self {
self.authenticated = v;
self
}
#[must_use]
pub fn evidence_strength(mut self, v: EvidenceStrength) -> Self {
self.evidence_strength = v;
self
}
#[must_use]
pub fn channels(mut self, v: Vec<EvidenceChannel>) -> Self {
self.channels = v;
self
}
#[must_use]
pub fn license_url(mut self, v: Option<String>) -> Self {
self.license_url = v;
self
}
#[must_use]
pub fn web_statement_of_rights(mut self, v: Option<String>) -> Self {
self.web_statement_of_rights = v;
self
}
#[must_use]
pub fn credit_line(mut self, v: Option<String>) -> Self {
self.credit_line = v;
self
}
#[must_use]
pub fn copyright_owner(mut self, v: Option<String>) -> Self {
self.copyright_owner = v;
self
}
#[must_use]
pub fn licensor_name(mut self, v: Option<String>) -> Self {
self.licensor_name = v;
self
}
#[must_use]
pub fn licensor_email(mut self, v: Option<String>) -> Self {
self.licensor_email = v;
self
}
#[must_use]
pub fn licensor_url(mut self, v: Option<String>) -> Self {
self.licensor_url = v;
self
}
#[must_use]
pub fn metadata_date(mut self, v: Option<String>) -> Self {
self.metadata_date = v;
self
}
#[must_use]
pub fn notice_applied_at(mut self, v: Option<String>) -> Self {
self.notice_applied_at = v;
self
}
#[must_use]
pub fn build(self) -> NoticeVerification {
NoticeVerification {
copyright_holder: self.copyright_holder,
creator: self.creator,
contact: self.contact,
rights_url: self.rights_url,
usage_terms: self.usage_terms,
ai_constraints: self.ai_constraints,
dmi: self.dmi,
tdm_reserved: self.tdm_reserved,
rights_signal_kind: self.rights_signal_kind,
canonical_dmi: self.canonical_dmi,
legacy_dmi: self.legacy_dmi,
protection_seed: self.protection_seed,
stego_status: self.stego_status,
stego_payload: self.stego_payload,
authenticated: self.authenticated,
evidence_strength: self.evidence_strength,
channels: self.channels,
license_url: self.license_url,
web_statement_of_rights: self.web_statement_of_rights,
credit_line: self.credit_line,
copyright_owner: self.copyright_owner,
licensor_name: self.licensor_name,
licensor_email: self.licensor_email,
licensor_url: self.licensor_url,
metadata_date: self.metadata_date,
notice_applied_at: self.notice_applied_at,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmbedPath {
Lsb,
LsbTiled,
DctF5,
DctF5Tiled,
QTableSeedOnly,
}
#[derive(Debug, Clone)]
pub enum EmbedOutcome<T> {
Embedded {
output: T,
payload_bytes: usize,
required_capacity: usize,
available_capacity: usize,
path: EmbedPath,
},
SkippedCapacity {
output: T,
payload_bytes: usize,
required_capacity: usize,
available_capacity: usize,
path: EmbedPath,
},
UnsupportedProgressive {
output: T,
},
}
impl<T> EmbedOutcome<T> {
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> EmbedOutcome<U> {
match self {
EmbedOutcome::Embedded {
output,
payload_bytes,
required_capacity,
available_capacity,
path,
} => EmbedOutcome::Embedded {
output: f(output),
payload_bytes,
required_capacity,
available_capacity,
path,
},
EmbedOutcome::SkippedCapacity {
output,
payload_bytes,
required_capacity,
available_capacity,
path,
} => EmbedOutcome::SkippedCapacity {
output: f(output),
payload_bytes,
required_capacity,
available_capacity,
path,
},
EmbedOutcome::UnsupportedProgressive { output } => {
EmbedOutcome::UnsupportedProgressive { output: f(output) }
}
}
}
pub fn into_inner(self) -> T {
match self {
EmbedOutcome::Embedded { output, .. }
| EmbedOutcome::SkippedCapacity { output, .. }
| EmbedOutcome::UnsupportedProgressive { output } => output,
}
}
#[must_use]
pub fn output(&self) -> &T {
match self {
EmbedOutcome::Embedded { output, .. }
| EmbedOutcome::SkippedCapacity { output, .. }
| EmbedOutcome::UnsupportedProgressive { output } => output,
}
}
#[must_use]
pub fn is_embedded(&self) -> bool {
matches!(self, EmbedOutcome::Embedded { .. })
}
#[must_use]
pub fn is_skipped(&self) -> bool {
matches!(self, EmbedOutcome::SkippedCapacity { .. })
}
#[must_use]
pub fn required_capacity(&self) -> usize {
match self {
EmbedOutcome::Embedded {
required_capacity, ..
}
| EmbedOutcome::SkippedCapacity {
required_capacity, ..
} => *required_capacity,
EmbedOutcome::UnsupportedProgressive { .. } => 0,
}
}
#[must_use]
pub fn available_capacity(&self) -> usize {
match self {
EmbedOutcome::Embedded {
available_capacity, ..
}
| EmbedOutcome::SkippedCapacity {
available_capacity, ..
} => *available_capacity,
EmbedOutcome::UnsupportedProgressive { .. } => 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProtectionWarning {
MissingMacKey,
MetadataInjectionDisabled,
ProgressiveJpegFallback,
JpegReencodeFragile,
LsbCapacitySkipped,
DctCapacityInsufficient,
ContradictoryLegalClaims,
}
impl std::fmt::Display for ProtectionWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProtectionWarning::MissingMacKey => write!(
f,
"No MAC key configured: payload integrity is CRC32-only and forgeable."
),
ProtectionWarning::MetadataInjectionDisabled => write!(
f,
"Metadata injection disabled: visible DMI/legal evidence will not be emitted."
),
ProtectionWarning::ProgressiveJpegFallback => write!(
f,
"Progressive JPEG detected: fell back to Q-table seed only. \
Full F5 DCT steganography was not applied."
),
ProtectionWarning::JpegReencodeFragile => write!(
f,
"JPEG output is fragile under downstream re-encoding; serve byte-identical \
output or expect metadata/Q-table/DCT evidence loss."
),
ProtectionWarning::LsbCapacitySkipped => write!(
f,
"Image too small for LSB steganographic embedding: no payload embedded. \
Only metadata markers were applied."
),
ProtectionWarning::DctCapacityInsufficient => write!(
f,
"JPEG DCT coefficients insufficient for full F5 embedding: \
fell back to Q-table seed only. Weaker protection applied."
),
ProtectionWarning::ContradictoryLegalClaims => write!(
f,
"Legal claims explicitly disabled but legal metadata is present: \
the legal metadata will be ignored. Remove the legal metadata or \
stop disabling legal claims."
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WarningCategory {
LegalNotice,
BestEffortStego,
AuthenticatedProvenance,
FormatFragility,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WarningSeverity {
Info,
Warning,
Error,
}
impl std::fmt::Display for WarningSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WarningSeverity::Info => write!(f, "info"),
WarningSeverity::Warning => write!(f, "warning"),
WarningSeverity::Error => write!(f, "error"),
}
}
}
impl ProtectionWarning {
#[must_use]
pub fn category(&self) -> WarningCategory {
match self {
ProtectionWarning::MissingMacKey => WarningCategory::AuthenticatedProvenance,
ProtectionWarning::MetadataInjectionDisabled => WarningCategory::LegalNotice,
ProtectionWarning::ProgressiveJpegFallback => WarningCategory::FormatFragility,
ProtectionWarning::JpegReencodeFragile => WarningCategory::FormatFragility,
ProtectionWarning::LsbCapacitySkipped => WarningCategory::BestEffortStego,
ProtectionWarning::DctCapacityInsufficient => WarningCategory::BestEffortStego,
ProtectionWarning::ContradictoryLegalClaims => WarningCategory::LegalNotice,
}
}
#[allow(deprecated)]
#[must_use]
pub fn severity_for_profile(&self, profile: EvidenceProfile) -> WarningSeverity {
match self {
ProtectionWarning::MissingMacKey => match profile {
EvidenceProfile::AuthenticatedProvenance | EvidenceProfile::Maximal => {
WarningSeverity::Warning
}
_ => WarningSeverity::Info,
},
ProtectionWarning::MetadataInjectionDisabled => match profile {
EvidenceProfile::LegalNotice | EvidenceProfile::LegalNoticeWithStego => {
WarningSeverity::Error
}
_ => WarningSeverity::Warning,
},
ProtectionWarning::ProgressiveJpegFallback | ProtectionWarning::JpegReencodeFragile => {
WarningSeverity::Warning
}
ProtectionWarning::LsbCapacitySkipped | ProtectionWarning::DctCapacityInsufficient => {
match profile {
EvidenceProfile::LegalNotice => WarningSeverity::Info,
_ => WarningSeverity::Warning,
}
}
ProtectionWarning::ContradictoryLegalClaims => WarningSeverity::Warning,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum RightsPolicy {
#[default]
Unspecified,
Allowed,
ProhibitedAiMlTraining,
ProhibitedGenerativeAiTraining,
ProhibitedExceptSearchIndexing,
ProhibitedAllDataMining,
ProhibitedSeeConstraints,
}
impl RightsPolicy {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
RightsPolicy::Unspecified => "Unspecified",
RightsPolicy::Allowed => "Allowed",
RightsPolicy::ProhibitedAiMlTraining => "ProhibitedAiMlTraining",
RightsPolicy::ProhibitedGenerativeAiTraining => "ProhibitedGenerativeAiTraining",
RightsPolicy::ProhibitedExceptSearchIndexing => "ProhibitedExceptSearchIndexing",
RightsPolicy::ProhibitedAllDataMining => "ProhibitedAllDataMining",
RightsPolicy::ProhibitedSeeConstraints => "ProhibitedSeeConstraints",
}
}
#[must_use]
pub fn to_dmi_value(&self) -> Option<DmiValue> {
match self {
RightsPolicy::Unspecified => None,
RightsPolicy::Allowed => Some(DmiValue::Allowed),
RightsPolicy::ProhibitedAiMlTraining => Some(DmiValue::ProhibitedAiMlTraining),
RightsPolicy::ProhibitedGenerativeAiTraining => {
Some(DmiValue::ProhibitedGenAiMlTraining)
}
RightsPolicy::ProhibitedExceptSearchIndexing => {
Some(DmiValue::ProhibitedExceptSearchEngineIndexing)
}
RightsPolicy::ProhibitedAllDataMining => Some(DmiValue::Prohibited),
RightsPolicy::ProhibitedSeeConstraints => Some(DmiValue::ProhibitedSeeConstraints),
}
}
#[must_use]
pub fn from_dmi_value(dmi: DmiValue) -> Self {
match dmi {
DmiValue::Unspecified => RightsPolicy::Unspecified,
DmiValue::Allowed => RightsPolicy::Allowed,
DmiValue::ProhibitedAiMlTraining => RightsPolicy::ProhibitedAiMlTraining,
DmiValue::ProhibitedGenAiMlTraining => RightsPolicy::ProhibitedGenerativeAiTraining,
DmiValue::ProhibitedExceptSearchEngineIndexing => {
RightsPolicy::ProhibitedExceptSearchIndexing
}
DmiValue::Prohibited => RightsPolicy::ProhibitedAllDataMining,
DmiValue::ProhibitedSeeConstraints => RightsPolicy::ProhibitedSeeConstraints,
}
}
#[must_use]
pub fn requires_constraints(&self) -> bool {
matches!(self, RightsPolicy::ProhibitedSeeConstraints)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HiddenMarkerMode {
Disabled,
BestEffort,
Tiled {
tile_size: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AuthenticationMode {
None,
Hmac,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtectionChannels {
pub rights_metadata: bool,
pub hidden_marker: HiddenMarkerMode,
pub authentication: AuthenticationMode,
}
impl ProtectionChannels {
#[must_use]
pub fn metadata_only() -> Self {
Self {
rights_metadata: true,
hidden_marker: HiddenMarkerMode::Disabled,
authentication: AuthenticationMode::None,
}
}
#[must_use]
pub fn with_hidden_marker() -> Self {
Self {
rights_metadata: true,
hidden_marker: HiddenMarkerMode::BestEffort,
authentication: AuthenticationMode::None,
}
}
#[must_use]
pub fn authenticated() -> Self {
Self {
rights_metadata: true,
hidden_marker: HiddenMarkerMode::BestEffort,
authentication: AuthenticationMode::Hmac,
}
}
#[must_use]
pub fn has_stego(&self) -> bool {
!matches!(self.hidden_marker, HiddenMarkerMode::Disabled)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessingOptions {
pub output_format: Option<ImageOutputFormat>,
pub jpeg_quality: u8,
pub progressive_jpeg: bool,
pub max_dimension: Option<u32>,
pub metadata_update_policy: MetadataUpdatePolicy,
}
impl Default for ProcessingOptions {
fn default() -> Self {
Self {
output_format: None,
jpeg_quality: 90,
progressive_jpeg: false,
max_dimension: None,
metadata_update_policy: MetadataUpdatePolicy::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct ProtectionRequest {
notice: RightsNotice,
policy: RightsPolicy,
channels: ProtectionChannels,
processing: ProcessingOptions,
seed: Option<u64>,
intensity: f32,
legal_metadata: Option<LegalMetadata>,
mac_key: Option<Vec<u8>>,
resource_limits: Option<crate::resource_limits::ResourceLimits>,
}
impl ProtectionRequest {
#[must_use]
pub fn new(notice: RightsNotice, policy: RightsPolicy, channels: ProtectionChannels) -> Self {
Self {
notice,
policy,
channels,
processing: ProcessingOptions::default(),
seed: None,
intensity: 0.5,
legal_metadata: None,
mac_key: None,
resource_limits: None,
}
}
#[must_use]
pub fn metadata_only(notice: RightsNotice, policy: RightsPolicy) -> Self {
Self::new(notice, policy, ProtectionChannels::metadata_only())
}
#[must_use]
pub fn with_hidden_marker(notice: RightsNotice, policy: RightsPolicy) -> Self {
Self::new(notice, policy, ProtectionChannels::with_hidden_marker())
}
#[must_use]
pub fn with_processing(mut self, processing: ProcessingOptions) -> Self {
self.processing = processing;
self
}
#[must_use]
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
#[must_use]
pub fn with_intensity(mut self, intensity: f32) -> Self {
self.intensity = intensity.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_legal_metadata(mut self, metadata: LegalMetadata) -> Self {
self.legal_metadata = Some(metadata);
self
}
#[must_use]
pub fn with_mac_key(mut self, key: Vec<u8>) -> Self {
self.mac_key = Some(key);
self
}
#[must_use]
pub fn with_resource_limits(mut self, limits: crate::resource_limits::ResourceLimits) -> Self {
self.resource_limits = Some(limits);
self
}
#[must_use]
pub fn with_output_format(mut self, format: ImageOutputFormat) -> Self {
self.processing.output_format = Some(format);
self
}
#[must_use]
pub fn with_jpeg_quality(mut self, quality: u8) -> Self {
self.processing.jpeg_quality = quality.clamp(1, 100);
self
}
#[must_use]
pub fn with_progressive_jpeg(mut self) -> Self {
self.processing.progressive_jpeg = true;
self
}
#[must_use]
pub fn with_max_dimension(mut self, max: u32) -> Self {
self.processing.max_dimension = Some(max);
self
}
#[must_use]
pub fn with_metadata_update_policy(mut self, policy: MetadataUpdatePolicy) -> Self {
self.processing.metadata_update_policy = policy;
self
}
#[must_use]
pub fn notice(&self) -> &RightsNotice {
&self.notice
}
#[must_use]
pub fn policy(&self) -> RightsPolicy {
self.policy
}
#[must_use]
pub fn channels(&self) -> &ProtectionChannels {
&self.channels
}
#[must_use]
pub fn processing(&self) -> &ProcessingOptions {
&self.processing
}
#[must_use]
pub fn seed(&self) -> Option<u64> {
self.seed
}
#[must_use]
pub fn intensity(&self) -> f32 {
self.intensity
}
#[must_use]
pub fn legal_metadata(&self) -> Option<&LegalMetadata> {
self.legal_metadata.as_ref()
}
#[must_use]
pub fn mac_key(&self) -> Option<&[u8]> {
self.mac_key.as_deref()
}
#[must_use]
pub fn resource_limits(&self) -> Option<&crate::resource_limits::ResourceLimits> {
self.resource_limits.as_ref()
}
#[must_use]
pub fn from_preset(
preset: ProtectionPreset,
notice: RightsNotice,
policy: RightsPolicy,
) -> Self {
Self::new(notice, policy, preset.to_channels())
}
}
#[derive(Debug, Clone)]
pub struct ResolvedProtectionPlan {
effective_policy: RightsPolicy,
effective_dmi: Option<DmiValue>,
effective_notice: RightsNotice,
channels: ProtectionChannels,
processing: ProcessingOptions,
seed: u64,
intensity: f32,
input_format: ImageOutputFormat,
output_format: ImageOutputFormat,
legal_metadata: Option<LegalMetadata>,
mac_key: Option<Vec<u8>>,
warnings: Vec<ProtectionWarning>,
resource_limits: crate::resource_limits::ResourceLimits,
}
impl ResolvedProtectionPlan {
#[must_use]
pub fn effective_policy(&self) -> RightsPolicy {
self.effective_policy
}
#[must_use]
pub fn effective_dmi(&self) -> Option<DmiValue> {
self.effective_dmi
}
#[must_use]
pub fn effective_notice(&self) -> &RightsNotice {
&self.effective_notice
}
#[must_use]
pub fn channels(&self) -> &ProtectionChannels {
&self.channels
}
#[must_use]
pub fn processing(&self) -> &ProcessingOptions {
&self.processing
}
#[must_use]
pub fn seed(&self) -> u64 {
self.seed
}
#[must_use]
pub fn intensity(&self) -> f32 {
self.intensity
}
#[must_use]
pub fn input_format(&self) -> ImageOutputFormat {
self.input_format
}
#[must_use]
pub fn output_format(&self) -> ImageOutputFormat {
self.output_format
}
#[must_use]
pub fn legal_metadata(&self) -> Option<&LegalMetadata> {
self.legal_metadata.as_ref()
}
#[must_use]
pub fn mac_key(&self) -> Option<&[u8]> {
self.mac_key.as_deref()
}
#[must_use]
pub fn warnings(&self) -> &[ProtectionWarning] {
&self.warnings
}
#[must_use]
pub fn resource_limits(&self) -> &crate::resource_limits::ResourceLimits {
&self.resource_limits
}
#[must_use]
pub fn modifies_pixels(&self) -> bool {
self.channels.has_stego()
}
#[must_use]
pub fn is_metadata_only(&self) -> bool {
!self.channels.has_stego() && self.channels.rights_metadata
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
effective_policy: RightsPolicy,
effective_dmi: Option<DmiValue>,
effective_notice: RightsNotice,
channels: ProtectionChannels,
processing: ProcessingOptions,
seed: u64,
intensity: f32,
input_format: ImageOutputFormat,
output_format: ImageOutputFormat,
legal_metadata: Option<LegalMetadata>,
mac_key: Option<Vec<u8>>,
warnings: Vec<ProtectionWarning>,
resource_limits: crate::resource_limits::ResourceLimits,
) -> Self {
Self {
effective_policy,
effective_dmi,
effective_notice,
channels,
processing,
seed,
intensity,
input_format,
output_format,
legal_metadata,
mac_key,
warnings,
resource_limits,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ProtectionPreset {
LegalNotice,
LegalNoticeWithStego,
AuthenticatedProvenance,
Maximal,
}
impl ProtectionPreset {
#[must_use]
pub fn to_channels(&self) -> ProtectionChannels {
match self {
ProtectionPreset::LegalNotice => ProtectionChannels::metadata_only(),
ProtectionPreset::LegalNoticeWithStego => ProtectionChannels::with_hidden_marker(),
ProtectionPreset::AuthenticatedProvenance => ProtectionChannels::authenticated(),
ProtectionPreset::Maximal => ProtectionChannels {
rights_metadata: true,
hidden_marker: HiddenMarkerMode::BestEffort,
authentication: AuthenticationMode::Hmac,
},
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
ProtectionPreset::LegalNotice => "legal-notice",
ProtectionPreset::LegalNoticeWithStego => "legal-notice-stego",
ProtectionPreset::AuthenticatedProvenance => "authenticated-provenance",
ProtectionPreset::Maximal => "maximal",
}
}
#[must_use]
pub fn requires_mac_key(&self) -> bool {
matches!(
self,
ProtectionPreset::AuthenticatedProvenance | ProtectionPreset::Maximal
)
}
}
#[derive(Debug, Clone, Default)]
pub struct ExecutionReport {
pub effective_policy: RightsPolicy,
pub effective_dmi: Option<DmiValue>,
pub metadata_injected: bool,
pub stego_attempted: bool,
pub stego_succeeded: bool,
pub format_transcoded: bool,
pub warnings: Vec<ProtectionWarning>,
pub resource_usage: Option<crate::resource_limits::ResourceUsage>,
}
impl ExecutionReport {
#[must_use]
pub fn effective_policy(&self) -> RightsPolicy {
self.effective_policy
}
#[must_use]
pub fn effective_dmi(&self) -> Option<DmiValue> {
self.effective_dmi
}
#[must_use]
pub fn metadata_injected(&self) -> bool {
self.metadata_injected
}
#[must_use]
pub fn stego_attempted(&self) -> bool {
self.stego_attempted
}
#[must_use]
pub fn stego_succeeded(&self) -> bool {
self.stego_succeeded
}
#[must_use]
pub fn format_transcoded(&self) -> bool {
self.format_transcoded
}
#[must_use]
pub fn warnings(&self) -> &[ProtectionWarning] {
&self.warnings
}
#[must_use]
pub fn resource_usage(&self) -> Option<&crate::resource_limits::ResourceUsage> {
self.resource_usage.as_ref()
}
#[must_use]
pub fn any_succeeded(&self) -> bool {
self.metadata_injected || self.stego_succeeded
}
#[allow(deprecated)]
#[must_use]
pub fn has_degradation(&self) -> bool {
self.warnings.iter().any(|w| {
matches!(
w.severity_for_profile(EvidenceProfile::LegalNotice),
WarningSeverity::Warning | WarningSeverity::Error
)
})
}
}
impl From<DmiValue> for RightsPolicy {
fn from(dmi: DmiValue) -> Self {
RightsPolicy::from_dmi_value(dmi)
}
}
impl From<RightsPolicy> for DmiValue {
fn from(policy: RightsPolicy) -> Self {
match policy {
RightsPolicy::Unspecified => DmiValue::Unspecified,
RightsPolicy::Allowed => DmiValue::Allowed,
RightsPolicy::ProhibitedAiMlTraining => DmiValue::ProhibitedAiMlTraining,
RightsPolicy::ProhibitedGenerativeAiTraining => DmiValue::ProhibitedGenAiMlTraining,
RightsPolicy::ProhibitedExceptSearchIndexing => {
DmiValue::ProhibitedExceptSearchEngineIndexing
}
RightsPolicy::ProhibitedAllDataMining => DmiValue::Prohibited,
RightsPolicy::ProhibitedSeeConstraints => DmiValue::ProhibitedSeeConstraints,
}
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
#[test]
fn builder_chain() {
let ctx = ProtectionContext::new(0.5, 42)
.with_format(ImageOutputFormat::Png)
.with_stego_redundancy(3);
assert_eq!(ctx.intensity(), 0.5);
assert_eq!(ctx.seed(), 42);
assert_eq!(ctx.stego_redundancy(), 3);
}
#[test]
fn intensity_clamped() {
let ctx = ProtectionContext::new(2.0, 42);
assert_eq!(ctx.intensity(), 1.0);
let ctx = ProtectionContext::new(-1.0, 42);
assert_eq!(ctx.intensity(), 0.0);
}
#[test]
fn seed_roundtrip_through_serde() {
let ctx = ProtectionContext::new(0.7, 12345);
let json = serde_json::to_string(&ctx).unwrap();
let restored: ProtectionContext = serde_json::from_str(&json).unwrap();
assert_eq!(restored.seed(), 12345);
assert_eq!(restored.intensity(), 0.7);
}
#[test]
fn serialize_emits_warning_when_config_set() {
let ctx = ProtectionContext::new(0.5, 99).with_mac_key(b"key".to_vec());
let json = serde_json::to_string(&ctx).unwrap();
assert!(
json.contains("_config_dropped_warning"),
"Serialized JSON should contain a warning field when config is set: {json}"
);
assert!(
json.contains("MAC key"),
"Warning should mention the MAC key: {json}"
);
let restored: ProtectionContext = serde_json::from_str(&json).unwrap();
assert_eq!(restored.seed(), 99);
assert_eq!(restored.intensity(), 0.5);
assert!(
restored.mac_key().is_none(),
"MAC key should be lost after serde roundtrip even when warning is emitted"
);
}
#[test]
fn serialize_no_warning_when_config_none() {
let ctx = ProtectionContext::new(0.5, 99);
let json = serde_json::to_string(&ctx).unwrap();
assert!(
!json.contains("_config_dropped_warning"),
"No warning should be emitted when config is None: {json}"
);
}
#[test]
fn tile_size_default_is_none() {
let ctx = ProtectionContext::new(0.5, 42);
assert_eq!(ctx.tile_size(), None);
assert!(!ctx.is_tile_mode_enabled());
}
#[test]
fn with_tile_size_zero_disables_tiling() {
let ctx = ProtectionContext::new(0.5, 42).with_tile_size(0);
assert_eq!(ctx.tile_size(), Some(0));
assert!(!ctx.is_tile_mode_enabled());
}
#[test]
fn with_tile_size_enables_tiling() {
let ctx = ProtectionContext::new(0.5, 42).with_tile_size(64);
assert_eq!(ctx.tile_size(), Some(64));
assert!(ctx.is_tile_mode_enabled());
}
#[test]
fn with_tile_size_clamps_below_minimum() {
let ctx = ProtectionContext::new(0.5, 42).with_tile_size(8);
assert_eq!(ctx.tile_size(), Some(32), "values below 32 clamp up to 32");
}
#[test]
fn with_tile_size_clamps_above_maximum() {
let ctx = ProtectionContext::new(0.5, 42).with_tile_size(4096);
assert_eq!(
ctx.tile_size(),
Some(1024),
"values above 1024 clamp down to 1024"
);
}
#[test]
fn with_tile_extraction_max_origins_defaults_to_64() {
let ctx = ProtectionContext::new(0.5, 42);
assert_eq!(ctx.tile_extraction_max_origins(), 64);
}
#[test]
fn with_tile_extraction_max_origins_zero_clamps_to_one() {
let ctx = ProtectionContext::new(0.5, 42).with_tile_extraction_max_origins(0);
assert_eq!(ctx.tile_extraction_max_origins(), 1);
}
#[test]
fn tile_settings_survive_serde_roundtrip() {
let ctx = ProtectionContext::new(0.5, 42)
.with_tile_size(64)
.with_tile_extraction_max_origins(128);
let json = serde_json::to_string(&ctx).unwrap();
let restored: ProtectionContext = serde_json::from_str(&json).unwrap();
assert_eq!(restored.tile_size(), Some(64));
assert_eq!(restored.tile_extraction_max_origins(), 128);
}
#[test]
fn protection_level_byte_roundtrip() {
let levels = [
ProtectionLevel::Disabled,
ProtectionLevel::Light,
ProtectionLevel::Standard,
];
for level in &levels {
let byte = level.to_byte();
let restored = ProtectionLevel::from_byte(byte);
assert_eq!(restored.as_ref(), Some(level));
}
}
#[test]
fn protection_level_from_invalid_byte() {
assert!(ProtectionLevel::from_byte(3).is_none());
assert!(ProtectionLevel::from_byte(255).is_none());
}
#[test]
fn dmi_value_iptc_property_mapping() {
use crate::types::DmiValue;
let allowed = DmiValue::Allowed;
assert!(allowed.to_iptc_property().contains("DMI-Allowed"));
let prohibited_training = DmiValue::ProhibitedAiMlTraining;
assert!(prohibited_training
.to_iptc_property()
.contains("DMI-Prohibited"));
let prohibited_gen = DmiValue::ProhibitedGenAiMlTraining;
assert!(prohibited_gen.to_iptc_property().contains("DMI-Prohibited"));
let prohibited_all = DmiValue::Prohibited;
assert!(prohibited_all.to_iptc_property().contains("DMI-Prohibited"));
let prohibited_se = DmiValue::ProhibitedExceptSearchEngineIndexing;
assert!(prohibited_se.to_iptc_property().contains("DMI-Prohibited"));
let prohibited_see = DmiValue::ProhibitedSeeConstraints;
assert!(prohibited_see.to_iptc_property().contains("DMI-Prohibited"));
let unspecified = DmiValue::Unspecified;
assert!(unspecified.to_iptc_property().contains("DMI"));
}
#[test]
fn evidence_profile_default_is_legal_notice() {
let ctx = ProtectionContext::new(0.5, 42);
assert_eq!(ctx.evidence_profile(), EvidenceProfile::LegalNotice);
}
#[test]
fn with_evidence_profile_sets_and_retrieves() {
let ctx = ProtectionContext::new(0.5, 42)
.with_evidence_profile(EvidenceProfile::AuthenticatedProvenance);
assert_eq!(
ctx.evidence_profile(),
EvidenceProfile::AuthenticatedProvenance
);
}
#[test]
fn evidence_profile_serialization_roundtrip() {
let profiles = [
EvidenceProfile::LegalNotice,
EvidenceProfile::LegalNoticeWithStego,
EvidenceProfile::AuthenticatedProvenance,
EvidenceProfile::Maximal,
];
for profile in &profiles {
let json = serde_json::to_string(profile).unwrap();
let restored: EvidenceProfile = serde_json::from_str(&json).unwrap();
assert_eq!(&restored, profile);
}
}
#[test]
fn evidence_profile_as_str() {
assert_eq!(EvidenceProfile::LegalNotice.as_str(), "legal-notice");
assert_eq!(
EvidenceProfile::LegalNoticeWithStego.as_str(),
"legal-notice-stego"
);
assert_eq!(
EvidenceProfile::AuthenticatedProvenance.as_str(),
"authenticated-provenance"
);
assert_eq!(EvidenceProfile::Maximal.as_str(), "maximal");
}
#[test]
fn evidence_profile_serde_roundtrip_in_context() {
let ctx = ProtectionContext::new(0.5, 42)
.with_evidence_profile(EvidenceProfile::AuthenticatedProvenance);
let json = serde_json::to_string(&ctx).unwrap();
let restored: ProtectionContext = serde_json::from_str(&json).unwrap();
assert_eq!(
restored.evidence_profile(),
EvidenceProfile::AuthenticatedProvenance
);
}
#[test]
fn evidence_profile_default_context_backward_compatible() {
let ctx = ProtectionContext::new(0.5, 42);
let json = serde_json::to_string(&ctx).unwrap();
let restored: ProtectionContext = serde_json::from_str(&json).unwrap();
assert_eq!(restored.evidence_profile(), EvidenceProfile::LegalNotice);
assert_eq!(restored.intensity(), 0.5);
assert_eq!(restored.seed(), 42);
}
#[test]
fn helper_constructors_set_correct_profile() {
assert_eq!(
ProtectionContext::legal_notice().evidence_profile(),
EvidenceProfile::LegalNotice
);
assert_eq!(
ProtectionContext::legal_notice_with_stego().evidence_profile(),
EvidenceProfile::LegalNoticeWithStego
);
assert_eq!(
ProtectionContext::authenticated_provenance().evidence_profile(),
EvidenceProfile::AuthenticatedProvenance
);
assert_eq!(
ProtectionContext::maximal().evidence_profile(),
EvidenceProfile::Maximal
);
}
#[test]
fn warning_category_mapping() {
assert_eq!(
ProtectionWarning::MissingMacKey.category(),
WarningCategory::AuthenticatedProvenance
);
assert_eq!(
ProtectionWarning::MetadataInjectionDisabled.category(),
WarningCategory::LegalNotice
);
assert_eq!(
ProtectionWarning::ProgressiveJpegFallback.category(),
WarningCategory::FormatFragility
);
assert_eq!(
ProtectionWarning::JpegReencodeFragile.category(),
WarningCategory::FormatFragility
);
assert_eq!(
ProtectionWarning::LsbCapacitySkipped.category(),
WarningCategory::BestEffortStego
);
assert_eq!(
ProtectionWarning::DctCapacityInsufficient.category(),
WarningCategory::BestEffortStego
);
}
#[test]
fn missing_mac_key_severity_by_profile() {
let w = ProtectionWarning::MissingMacKey;
assert_eq!(
w.severity_for_profile(EvidenceProfile::AuthenticatedProvenance),
WarningSeverity::Warning
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::Maximal),
WarningSeverity::Warning
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::LegalNotice),
WarningSeverity::Info
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::LegalNoticeWithStego),
WarningSeverity::Info
);
}
#[test]
fn metadata_injection_disabled_severity_by_profile() {
let w = ProtectionWarning::MetadataInjectionDisabled;
assert_eq!(
w.severity_for_profile(EvidenceProfile::LegalNotice),
WarningSeverity::Error
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::LegalNoticeWithStego),
WarningSeverity::Error
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::AuthenticatedProvenance),
WarningSeverity::Warning
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::Maximal),
WarningSeverity::Warning
);
}
#[test]
fn format_fragility_severity_is_always_warning() {
for w in [
ProtectionWarning::ProgressiveJpegFallback,
ProtectionWarning::JpegReencodeFragile,
] {
for profile in [
EvidenceProfile::LegalNotice,
EvidenceProfile::LegalNoticeWithStego,
EvidenceProfile::AuthenticatedProvenance,
EvidenceProfile::Maximal,
] {
assert_eq!(
w.severity_for_profile(profile),
WarningSeverity::Warning,
"{:?} should be Warning for {:?}",
w,
profile
);
}
}
}
#[test]
fn stego_capacity_severity_by_profile() {
for w in [
ProtectionWarning::LsbCapacitySkipped,
ProtectionWarning::DctCapacityInsufficient,
] {
assert_eq!(
w.severity_for_profile(EvidenceProfile::LegalNotice),
WarningSeverity::Info,
"{:?} should be Info for LegalNotice",
w
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::LegalNoticeWithStego),
WarningSeverity::Warning,
"{:?} should be Warning for LegalNoticeWithStego",
w
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::AuthenticatedProvenance),
WarningSeverity::Warning,
"{:?} should be Warning for AuthenticatedProvenance",
w
);
assert_eq!(
w.severity_for_profile(EvidenceProfile::Maximal),
WarningSeverity::Warning,
"{:?} should be Warning for Maximal",
w
);
}
}
}
#[cfg(test)]
#[allow(deprecated)]
mod plus_mapping_tests {
use super::*;
#[test]
fn all_variants_have_plus_vocab_key() {
let variants = [
DmiValue::Unspecified,
DmiValue::Allowed,
DmiValue::ProhibitedAiMlTraining,
DmiValue::ProhibitedGenAiMlTraining,
DmiValue::ProhibitedExceptSearchEngineIndexing,
DmiValue::Prohibited,
DmiValue::ProhibitedSeeConstraints,
];
for v in variants {
let key = v.plus_vocab_key();
assert!(key.starts_with("DMI-"), "key must start with DMI-: {key}");
assert_eq!(DmiValue::from_plus_vocab_key(key), Some(v));
}
}
#[test]
fn from_plus_vocab_key_rejects_unknown() {
assert_eq!(DmiValue::from_plus_vocab_key("DMI-UNKNOWN"), None);
assert_eq!(DmiValue::from_plus_vocab_key(""), None);
assert_eq!(DmiValue::from_plus_vocab_key("Prohibited"), None);
}
#[test]
fn plus_vocab_keys_match_exiftool() {
assert_eq!(
DmiValue::ProhibitedSeeConstraints.plus_vocab_key(),
"DMI-PROHIBITED-SEECONSTRAINT"
);
assert_eq!(
DmiValue::ProhibitedAiMlTraining.plus_vocab_key(),
"DMI-PROHIBITED-AIMLTRAINING"
);
}
}
#[cfg(test)]
#[allow(deprecated)]
mod rights_policy_tests {
use super::*;
#[test]
fn unspecified_to_dmi_returns_none() {
assert_eq!(RightsPolicy::Unspecified.to_dmi_value(), None);
}
#[test]
fn allowed_to_dmi() {
assert_eq!(
RightsPolicy::Allowed.to_dmi_value(),
Some(DmiValue::Allowed)
);
}
#[test]
fn prohibited_ai_ml_training_to_dmi() {
assert_eq!(
RightsPolicy::ProhibitedAiMlTraining.to_dmi_value(),
Some(DmiValue::ProhibitedAiMlTraining)
);
}
#[test]
fn prohibited_generative_ai_training_to_dmi() {
assert_eq!(
RightsPolicy::ProhibitedGenerativeAiTraining.to_dmi_value(),
Some(DmiValue::ProhibitedGenAiMlTraining)
);
}
#[test]
fn prohibited_except_search_indexing_to_dmi() {
assert_eq!(
RightsPolicy::ProhibitedExceptSearchIndexing.to_dmi_value(),
Some(DmiValue::ProhibitedExceptSearchEngineIndexing)
);
}
#[test]
fn prohibited_all_data_mining_to_dmi() {
assert_eq!(
RightsPolicy::ProhibitedAllDataMining.to_dmi_value(),
Some(DmiValue::Prohibited)
);
}
#[test]
fn prohibited_see_constraints_to_dmi() {
assert_eq!(
RightsPolicy::ProhibitedSeeConstraints.to_dmi_value(),
Some(DmiValue::ProhibitedSeeConstraints)
);
}
#[test]
fn from_dmi_unspecified() {
assert_eq!(
RightsPolicy::from_dmi_value(DmiValue::Unspecified),
RightsPolicy::Unspecified
);
}
#[test]
fn from_dmi_allowed() {
assert_eq!(
RightsPolicy::from_dmi_value(DmiValue::Allowed),
RightsPolicy::Allowed
);
}
#[test]
fn from_dmi_prohibited_ai_ml_training() {
assert_eq!(
RightsPolicy::from_dmi_value(DmiValue::ProhibitedAiMlTraining),
RightsPolicy::ProhibitedAiMlTraining
);
}
#[test]
fn from_dmi_prohibited_gen_ai_ml_training() {
assert_eq!(
RightsPolicy::from_dmi_value(DmiValue::ProhibitedGenAiMlTraining),
RightsPolicy::ProhibitedGenerativeAiTraining
);
}
#[test]
fn from_dmi_prohibited_except_search_engine_indexing() {
assert_eq!(
RightsPolicy::from_dmi_value(DmiValue::ProhibitedExceptSearchEngineIndexing),
RightsPolicy::ProhibitedExceptSearchIndexing
);
}
#[test]
fn from_dmi_prohibited() {
assert_eq!(
RightsPolicy::from_dmi_value(DmiValue::Prohibited),
RightsPolicy::ProhibitedAllDataMining
);
}
#[test]
fn from_dmi_prohibited_see_constraints() {
assert_eq!(
RightsPolicy::from_dmi_value(DmiValue::ProhibitedSeeConstraints),
RightsPolicy::ProhibitedSeeConstraints
);
}
#[test]
fn roundtrip_to_dmi_from_dmi() {
let policies = [
RightsPolicy::Allowed,
RightsPolicy::ProhibitedAiMlTraining,
RightsPolicy::ProhibitedGenerativeAiTraining,
RightsPolicy::ProhibitedExceptSearchIndexing,
RightsPolicy::ProhibitedAllDataMining,
RightsPolicy::ProhibitedSeeConstraints,
];
for policy in policies {
let dmi = policy.to_dmi_value().unwrap();
let roundtripped = RightsPolicy::from_dmi_value(dmi);
assert_eq!(roundtripped, policy);
}
}
#[test]
fn roundtrip_from_dmi_to_dmi() {
let dmi_values = [
DmiValue::Allowed,
DmiValue::ProhibitedAiMlTraining,
DmiValue::ProhibitedGenAiMlTraining,
DmiValue::ProhibitedExceptSearchEngineIndexing,
DmiValue::Prohibited,
DmiValue::ProhibitedSeeConstraints,
];
for dmi in dmi_values {
let policy = RightsPolicy::from_dmi_value(dmi);
let roundtripped = policy.to_dmi_value().unwrap();
assert_eq!(roundtripped, dmi);
}
}
#[test]
fn from_trait_matches_function() {
for dmi in [
DmiValue::Allowed,
DmiValue::ProhibitedAiMlTraining,
DmiValue::ProhibitedGenAiMlTraining,
] {
let via_from: RightsPolicy = dmi.into();
let via_fn = RightsPolicy::from_dmi_value(dmi);
assert_eq!(via_from, via_fn);
}
}
#[test]
fn into_trait_matches_to_dmi_value() {
for policy in [
RightsPolicy::Allowed,
RightsPolicy::ProhibitedAiMlTraining,
RightsPolicy::ProhibitedGenerativeAiTraining,
] {
let via_into: DmiValue = policy.into();
let via_fn = policy.to_dmi_value().unwrap();
assert_eq!(via_into, via_fn);
}
}
#[test]
fn requires_constraints_only_for_see_constraints() {
assert!(!RightsPolicy::Allowed.requires_constraints());
assert!(!RightsPolicy::ProhibitedAiMlTraining.requires_constraints());
assert!(RightsPolicy::ProhibitedSeeConstraints.requires_constraints());
}
#[test]
fn as_str_matches_variant_name() {
assert_eq!(RightsPolicy::Unspecified.as_str(), "Unspecified");
assert_eq!(RightsPolicy::Allowed.as_str(), "Allowed");
assert_eq!(
RightsPolicy::ProhibitedAllDataMining.as_str(),
"ProhibitedAllDataMining"
);
}
}
#[cfg(test)]
#[allow(deprecated)]
mod protection_preset_tests {
use super::*;
#[test]
fn legal_notice_expands_to_metadata_only() {
let channels = ProtectionPreset::LegalNotice.to_channels();
assert!(channels.rights_metadata);
assert_eq!(channels.hidden_marker, HiddenMarkerMode::Disabled);
assert_eq!(channels.authentication, AuthenticationMode::None);
assert!(!channels.has_stego());
}
#[test]
fn legal_notice_with_stego_expands_correctly() {
let channels = ProtectionPreset::LegalNoticeWithStego.to_channels();
assert!(channels.rights_metadata);
assert_eq!(channels.hidden_marker, HiddenMarkerMode::BestEffort);
assert_eq!(channels.authentication, AuthenticationMode::None);
assert!(channels.has_stego());
}
#[test]
fn authenticated_provenance_expands_correctly() {
let channels = ProtectionPreset::AuthenticatedProvenance.to_channels();
assert!(channels.rights_metadata);
assert_eq!(channels.hidden_marker, HiddenMarkerMode::BestEffort);
assert_eq!(channels.authentication, AuthenticationMode::Hmac);
assert!(channels.has_stego());
}
#[test]
fn maximal_expands_correctly() {
let channels = ProtectionPreset::Maximal.to_channels();
assert!(channels.rights_metadata);
assert_eq!(channels.hidden_marker, HiddenMarkerMode::BestEffort);
assert_eq!(channels.authentication, AuthenticationMode::Hmac);
assert!(channels.has_stego());
}
#[test]
fn requires_mac_key_only_for_authenticated_presets() {
assert!(!ProtectionPreset::LegalNotice.requires_mac_key());
assert!(!ProtectionPreset::LegalNoticeWithStego.requires_mac_key());
assert!(ProtectionPreset::AuthenticatedProvenance.requires_mac_key());
assert!(ProtectionPreset::Maximal.requires_mac_key());
}
#[test]
fn as_str_returns_lowercase() {
assert_eq!(ProtectionPreset::LegalNotice.as_str(), "legal-notice");
assert_eq!(
ProtectionPreset::LegalNoticeWithStego.as_str(),
"legal-notice-stego"
);
assert_eq!(
ProtectionPreset::AuthenticatedProvenance.as_str(),
"authenticated-provenance"
);
assert_eq!(ProtectionPreset::Maximal.as_str(), "maximal");
}
#[test]
fn from_preset_uses_preset_channels() {
let notice = RightsNotice::new();
let request = ProtectionRequest::from_preset(
ProtectionPreset::LegalNotice,
notice,
RightsPolicy::Allowed,
);
assert!(request.channels().rights_metadata);
assert_eq!(request.channels().hidden_marker, HiddenMarkerMode::Disabled);
}
}
#[cfg(test)]
#[allow(deprecated)]
mod url_validation_tests {
use super::*;
#[test]
fn valid_https_url_passes() {
let meta = LegalMetadata::new().with_license_url("https://example.com/license");
assert!(meta.validate().is_ok());
}
#[test]
fn valid_http_url_passes() {
let meta = LegalMetadata::new().with_license_url("http://example.com/license");
assert!(meta.validate().is_ok());
}
#[test]
fn valid_ftp_url_passes() {
let meta = LegalMetadata::new().with_license_url("ftp://files.example.com/doc");
assert!(meta.validate().is_ok());
}
#[test]
fn missing_scheme_fails() {
let meta = LegalMetadata::new().with_license_url("example.com/license");
let err = meta.validate().unwrap_err();
assert!(
err.to_string().contains("must include a scheme"),
"Expected scheme error, got: {}",
err
);
}
#[test]
fn empty_url_fails() {
let meta = LegalMetadata::new().with_license_url("");
let err = meta.validate().unwrap_err();
assert!(
err.to_string().contains("must not be empty"),
"Expected empty error, got: {}",
err
);
}
#[test]
fn scheme_only_fails() {
let meta = LegalMetadata::new().with_license_url("https://");
let err = meta.validate().unwrap_err();
assert!(
err.to_string().contains("must include an authority"),
"Expected authority error, got: {}",
err
);
}
#[test]
fn web_statement_validates() {
let meta = LegalMetadata::new().with_web_statement_of_rights("not-a-url");
let err = meta.validate().unwrap_err();
assert!(err.to_string().contains("web_statement_of_rights"));
}
#[test]
fn licensor_url_validates() {
let meta = LegalMetadata::new().with_licensor_url("missing-scheme");
let err = meta.validate().unwrap_err();
assert!(err.to_string().contains("licensor_url"));
}
#[test]
fn non_url_fields_not_affected() {
let meta = LegalMetadata::new()
.with_copyright_holder("Test")
.with_creator("Author");
assert!(meta.validate().is_ok());
}
}
#[cfg(test)]
#[allow(deprecated)]
mod localized_text_tests {
use super::*;
#[test]
fn new_defaults_to_x_default() {
let lt = LocalizedText::new("All rights reserved");
assert_eq!(lt.text(), "All rights reserved");
assert_eq!(lt.lang(), "x-default");
}
#[test]
fn with_lang_sets_language() {
let lt = LocalizedText::with_lang("Tous droits réservés.", "fr");
assert_eq!(lt.text(), "Tous droits réservés.");
assert_eq!(lt.lang(), "fr");
}
#[test]
fn from_string_uses_default_lang() {
let lt: LocalizedText = "test".into();
assert_eq!(lt.lang(), "x-default");
}
#[test]
fn display_returns_text() {
let lt = LocalizedText::new("hello");
assert_eq!(format!("{}", lt), "hello");
}
#[test]
fn usage_terms_localized_sets_both_fields() {
let meta = LegalMetadata::new()
.with_usage_terms_localized(LocalizedText::with_lang("Tous droits réservés.", "fr"));
assert_eq!(meta.usage_terms(), Some("Tous droits réservés."));
assert_eq!(meta.usage_terms_lang(), Some("fr"));
}
#[test]
fn usage_terms_localized_from_string_uses_default_lang() {
let meta = LegalMetadata::new().with_usage_terms_localized("All rights reserved");
assert_eq!(meta.usage_terms(), Some("All rights reserved"));
assert_eq!(meta.usage_terms_lang(), Some("x-default"));
}
#[test]
fn usage_terms_plain_has_no_lang() {
let meta = LegalMetadata::new().with_usage_terms("All rights reserved");
assert_eq!(meta.usage_terms(), Some("All rights reserved"));
assert_eq!(meta.usage_terms_lang(), None);
}
}
#[cfg(test)]
#[allow(deprecated)]
mod date_validation_tests {
use super::*;
#[test]
fn valid_date_only() {
let meta = LegalMetadata::new().with_creation_date("2024-01-15");
assert!(meta.validate().is_ok());
}
#[test]
fn valid_datetime_utc() {
let meta = LegalMetadata::new().with_notice_applied_at("2024-01-15T12:30:45Z");
assert!(meta.validate().is_ok());
}
#[test]
fn valid_datetime_offset() {
let meta = LegalMetadata::new().with_metadata_date("2024-01-15T12:30:45+05:30");
assert!(meta.validate().is_ok());
}
#[test]
fn valid_datetime_negative_offset() {
let meta = LegalMetadata::new().with_creation_date("2024-01-15T12:30:45-08:00");
assert!(meta.validate().is_ok());
}
#[test]
fn invalid_date_too_short() {
let meta = LegalMetadata::new().with_creation_date("2024-01");
let err = meta.validate().unwrap_err();
assert!(
err.to_string().contains("ISO 8601"),
"Expected ISO 8601 error, got: {}",
err
);
}
#[test]
fn invalid_date_wrong_separator() {
let meta = LegalMetadata::new().with_creation_date("2024/01/15");
let err = meta.validate().unwrap_err();
assert!(err.to_string().contains("ISO 8601"));
}
#[test]
fn invalid_datetime_missing_t() {
let meta = LegalMetadata::new().with_notice_applied_at("2024-01-15 12:30:45Z");
let err = meta.validate().unwrap_err();
assert!(err.to_string().contains("ISO 8601"));
}
#[test]
fn empty_date_fails() {
let meta = LegalMetadata::new().with_creation_date("");
let err = meta.validate().unwrap_err();
assert!(err.to_string().contains("must not be empty"));
}
#[test]
fn date_only_with_time_component_fails() {
let meta = LegalMetadata::new().with_creation_date("2024-01-15T");
let err = meta.validate().unwrap_err();
assert!(err.to_string().contains("ISO 8601"));
}
#[test]
fn all_date_fields_valid() {
let meta = LegalMetadata::new()
.with_creation_date("2024-01-15")
.with_metadata_date("2024-01-15T12:30:45Z")
.with_notice_applied_at("2024-01-15T12:30:45+05:30");
assert!(meta.validate().is_ok());
}
}