use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::minhash::MinHash;
use crate::simhash::{Fingerprint, Hash256};
pub const SOFT_BINDING_LABEL: &str = "c2pa.soft-binding";
pub const ALG_FINGERPRINT: &str = "com.writerslogic.text-fingerprint.1";
pub const ALG_WATERMARK: &str = "com.writerslogic.zwc-watermark.2";
pub const ALG_STRUCTURE: &str = "com.writerslogic.text-structure.1";
pub const ALG_MINHASH: &str = "com.writerslogic.text-minhash.1";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SoftBinding {
pub alg: String,
pub blocks: Vec<Block>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(with = "serde_bytes")]
pub pad: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Block {
pub scope: Scope,
pub value: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Scope {
#[serde(skip_serializing_if = "Option::is_none")]
pub region: Option<RegionOfInterest>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegionOfInterest {
pub region: Vec<Range>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Range {
#[serde(rename = "type")]
pub range_type: RangeType,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<Text>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RangeType {
Textual,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Text {
pub selectors: Vec<TextSelectorRange>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextSelectorRange {
pub selector: TextSelector,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextSelector {
pub fragment: String,
pub start: Option<i32>,
pub end: Option<i32>,
}
impl SoftBinding {
pub fn whole(alg: &str, value_hex: String) -> Self {
SoftBinding {
alg: alg.to_string(),
blocks: vec![Block {
scope: Scope::default(),
value: value_hex,
}],
name: None,
pad: Vec::new(),
}
}
pub fn push_window(&mut self, start: usize, len: usize, value_hex: String) {
let s = i32::try_from(start).unwrap_or(i32::MAX);
let e = i32::try_from(start.saturating_add(len)).unwrap_or(i32::MAX);
self.blocks.push(Block {
scope: Scope {
region: Some(RegionOfInterest {
region: vec![Range {
range_type: RangeType::Textual,
text: Some(Text {
selectors: vec![TextSelectorRange {
selector: TextSelector {
fragment: String::new(),
start: Some(s),
end: Some(e),
},
}],
}),
}],
}),
},
value: value_hex,
});
}
pub fn to_cbor(&self) -> Result<Vec<u8>, Error> {
let mut out = Vec::new();
ciborium::into_writer(self, &mut out)
.map_err(|e| Error::GenerationFailed(format!("soft-binding CBOR encode: {e}")))?;
Ok(out)
}
pub fn from_cbor(bytes: &[u8]) -> Result<Self, Error> {
ciborium::from_reader(bytes)
.map_err(|e| Error::InvalidInput(format!("soft-binding CBOR decode: {e}")))
}
}
pub fn from_fingerprint(fp: &Fingerprint) -> SoftBinding {
let mut sb = SoftBinding::whole(ALG_FINGERPRINT, fp.whole.to_hex());
for w in &fp.windows {
sb.push_window(w.start, w.len, w.hash.to_hex());
}
sb
}
pub fn from_structure(hash: &Hash256) -> SoftBinding {
SoftBinding::whole(ALG_STRUCTURE, hash.to_hex())
}
pub fn from_minhash(mh: &MinHash) -> SoftBinding {
let mut bytes = Vec::with_capacity(mh.sig.len() * 8);
for v in &mh.sig {
bytes.extend_from_slice(&v.to_be_bytes());
}
SoftBinding::whole(ALG_MINHASH, hex::encode(bytes))
}
pub fn from_watermark_pointer(pointer: &[u8]) -> SoftBinding {
SoftBinding::whole(ALG_WATERMARK, hex::encode(pointer))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whole_binding_roundtrips_through_cbor() {
let sb = SoftBinding::whole(ALG_FINGERPRINT, "deadbeef".into());
let bytes = sb.to_cbor().unwrap();
assert_eq!(SoftBinding::from_cbor(&bytes).unwrap(), sb);
}
#[test]
fn windows_carry_textual_char_ranges() {
let mut sb = SoftBinding::whole(ALG_FINGERPRINT, "00".into());
sb.push_window(0, 512, "11".into());
sb.push_window(256, 512, "22".into());
assert_eq!(sb.blocks.len(), 3);
let win = &sb.blocks[1];
let sel = &win.scope.region.as_ref().unwrap().region[0]
.text
.as_ref()
.unwrap()
.selectors[0]
.selector;
assert_eq!(sel.start, Some(0));
assert_eq!(sel.end, Some(512));
assert!(sb.blocks[0].scope.region.is_none());
let bytes = sb.to_cbor().unwrap();
assert_eq!(SoftBinding::from_cbor(&bytes).unwrap(), sb);
}
#[test]
fn alg_is_required_and_present() {
let sb = SoftBinding::whole(ALG_WATERMARK, "abcd".into());
let bytes = sb.to_cbor().unwrap();
let back = SoftBinding::from_cbor(&bytes).unwrap();
assert_eq!(back.alg, ALG_WATERMARK);
assert!(!back.blocks.is_empty());
}
}