c2pa_text_binding/
zwbin.rs1const ZERO: char = '\u{200B}';
13const ONE: char = '\u{200C}';
14
15pub const MAGIC: [u8; 4] = *b"C2ZW";
17
18pub fn is_bit(c: char) -> bool {
20 c == ZERO || c == ONE
21}
22
23pub 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
36pub 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#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum Decoded {
47 None,
48 Corrupt,
49 Payload(Vec<u8>),
50}
51
52pub 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}