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",
}
}
}
#[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,
}
}
}
#[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, Serialize, Deserialize, Default)]
pub struct LegalMetadata {
copyright_holder: Option<String>,
contact_email: Option<String>,
license_url: Option<String>,
usage_terms: Option<String>,
creation_date: Option<String>,
ai_constraints: Option<String>,
web_statement_of_rights: Option<String>,
}
impl LegalMetadata {
#[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 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 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
}
}
#[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()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProtectionContext {
intensity: f32,
seed: u64,
input_format: Option<ImageOutputFormat>,
output_format: Option<ImageOutputFormat>,
protection_level: Option<ProtectionLevel>,
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]>,
#[serde(skip)]
config: Option<Arc<ProtectionConfig>>,
}
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 = 15;
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("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)?;
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,
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,
config: 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,
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,
config: 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
}
#[must_use]
pub fn with_dmi(mut self, dmi: DmiValue) -> Self {
self.dmi_value = Some(dmi);
self
}
#[must_use]
pub fn with_metadata_injection(mut self, enable: bool) -> Self {
self.inject_metadata = Some(enable);
self
}
#[must_use]
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 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_protection_level(&mut self, level: ProtectionLevel) {
self.protection_level = Some(level);
}
}
#[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)]
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, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProtectionWarning {
MissingMacKey,
MetadataInjectionDisabled,
ProgressiveJpegFallback,
JpegReencodeFragile,
LsbCapacitySkipped,
DctCapacityInsufficient,
WebpLossyReencodeDestructive,
}
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::WebpLossyReencodeDestructive => write!(
f,
"WebP lossy re-encoding will destroy LSB steganographic payloads. \
Use lossless WebP or another format to preserve steganographic protection."
),
}
}
}
#[cfg(test)]
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"));
}
}