Skip to main content

c2pa_text_binding/
zwbin.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Naive zero-width binary carrier.
4//!
5//! One bit per code point: `U+200B` = 0, `U+200C` = 1, eight bits per byte,
6//! most-significant first, no framing beyond a magic and no error correction.
7//! It shares the zero-width alphabet with [`crate::stego`] but omits the
8//! Reed-Solomon coding, so the benchmark can attribute `stego`'s partial-loss
9//! survival to the coding rather than the alphabet.
10
11/// Bit symbols: index 0 = clear, index 1 = set.
12const ZERO: char = '\u{200B}';
13const ONE: char = '\u{200C}';
14
15/// Carrier magic.
16pub const MAGIC: [u8; 4] = *b"C2ZW";
17
18/// Whether `c` is one of the two bit symbols.
19pub fn is_bit(c: char) -> bool {
20    c == ZERO || c == ONE
21}
22
23/// Encode `payload` as `magic || payload`, eight zero-width bits per byte.
24pub fn encode(payload: &[u8]) -> String {
25    let mut bytes = MAGIC.to_vec();
26    bytes.extend_from_slice(payload);
27    let mut out = String::with_capacity(bytes.len() * 8 * 3);
28    for byte in bytes {
29        for bit in (0..8).rev() {
30            out.push(if (byte >> bit) & 1 == 1 { ONE } else { ZERO });
31        }
32    }
33    out
34}
35
36/// Append a zero-width binary carrier for `payload` to the end of `text`.
37pub fn embed(text: &str, payload: &[u8]) -> String {
38    let mut s = String::with_capacity(text.len() + (MAGIC.len() + payload.len()) * 24);
39    s.push_str(text);
40    s.push_str(&encode(payload));
41    s
42}
43
44/// Decode outcome, mirroring [`crate::vs::Decoded`] for uniform classification.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum Decoded {
47    None,
48    Corrupt,
49    Payload(Vec<u8>),
50}
51
52/// Recover a payload from the contiguous zero-width bit run in `text`.
53pub fn extract(text: &str) -> Decoded {
54    let bits: Vec<char> = text.chars().filter(|c| is_bit(*c)).collect();
55    if bits.is_empty() {
56        return Decoded::None;
57    }
58    if !bits.len().is_multiple_of(8) {
59        return Decoded::Corrupt;
60    }
61    let bytes: Vec<u8> = bits
62        .chunks(8)
63        .map(|chunk| {
64            chunk
65                .iter()
66                .fold(0u8, |acc, &c| (acc << 1) | (c == ONE) as u8)
67        })
68        .collect();
69    if bytes.len() >= MAGIC.len() && bytes[..MAGIC.len()] == MAGIC {
70        Decoded::Payload(bytes[MAGIC.len()..].to_vec())
71    } else {
72        Decoded::Corrupt
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn round_trip_recovers_payload() {
82        let payload = b"c2pa-manifest-01";
83        let text = embed("A document body.", payload);
84        assert_eq!(extract(&text), Decoded::Payload(payload.to_vec()));
85    }
86
87    #[test]
88    fn eight_code_points_per_byte() {
89        assert_eq!(encode(b"z").chars().count(), (MAGIC.len() + 1) * 8);
90    }
91
92    #[test]
93    fn stripping_bits_yields_none() {
94        let text = embed("hi", b"x");
95        let stripped: String = text.chars().filter(|c| !is_bit(*c)).collect();
96        assert_eq!(extract(&stripped), Decoded::None);
97    }
98
99    #[test]
100    fn losing_one_bit_fails_safe() {
101        let mut chars: Vec<char> = embed("hi", b"payload").chars().collect();
102        chars.pop();
103        let mangled: String = chars.into_iter().collect();
104        assert_eq!(extract(&mangled), Decoded::Corrupt);
105    }
106}