c2pa_unstructured_text/
vs.rs1const VS_START: u32 = 0xFE00;
19const VS_END: u32 = 0xFE0F;
21const VS_SUP_START: u32 = 0xE0100;
23const VS_SUP_END: u32 = 0xE01EF;
25
26pub fn byte_to_vs(b: u8) -> char {
28 let cp = if b <= 0x0F {
29 VS_START + b as u32
30 } else {
31 VS_SUP_START + (b as u32 - 0x10)
32 };
33 char::from_u32(cp).expect("variation-selector code points are valid scalars")
36}
37
38pub fn vs_to_byte(c: char) -> Option<u8> {
41 match c as u32 {
42 cp @ VS_START..=VS_END => Some((cp - VS_START) as u8),
43 cp @ VS_SUP_START..=VS_SUP_END => Some((cp - VS_SUP_START + 0x10) as u8),
44 _ => None,
45 }
46}
47
48pub fn is_vs(c: char) -> bool {
54 vs_to_byte(c).is_some()
55}
56
57pub fn encode_run(bytes: &[u8]) -> String {
59 bytes.iter().map(|&b| byte_to_vs(b)).collect()
60}
61
62pub fn decode_run(s: &str) -> (Vec<u8>, usize) {
66 let mut out = Vec::new();
67 for (offset, c) in s.char_indices() {
68 match vs_to_byte(c) {
69 Some(b) => out.push(b),
70 None => return (out, offset),
71 }
72 }
73 (out, s.len())
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn spec_boundary_vectors() {
82 assert_eq!(byte_to_vs(0x00), '\u{FE00}');
83 assert_eq!(byte_to_vs(0x0F), '\u{FE0F}');
84 assert_eq!(byte_to_vs(0x10), '\u{E0100}');
85 assert_eq!(byte_to_vs(0xFF), '\u{E01EF}');
86 }
87
88 #[test]
89 fn codec_is_a_bijection() {
90 let mut seen = std::collections::HashSet::new();
91 for b in 0u8..=0xFF {
92 let c = byte_to_vs(b);
93 assert_eq!(vs_to_byte(c), Some(b), "byte {b:#04x} did not round-trip");
94 assert!(seen.insert(c), "byte {b:#04x} collided on {c:?}");
95 }
96 assert_eq!(seen.len(), 256);
97 }
98
99 #[test]
100 fn non_selectors_are_rejected() {
101 for c in ['a', 'Z', '0', ' ', '\n', '\u{FEFF}', '\u{FDFF}', '\u{FE10}'] {
104 assert_eq!(vs_to_byte(c), None, "{c:?} was accepted");
105 assert!(!is_vs(c));
106 }
107 assert_eq!(vs_to_byte('\u{E01F0}'), None);
109 }
110
111 #[test]
112 fn run_round_trips_and_reports_its_end() {
113 let payload = [0xDEu8, 0xAD, 0xBE, 0xEF, 0x00, 0x0F, 0x10];
114 let run = encode_run(&payload);
115 let (decoded, end) = decode_run(&run);
116 assert_eq!(decoded, payload);
117 assert_eq!(end, run.len());
118 }
119
120 #[test]
121 fn run_stops_at_visible_text() {
122 let mut s = encode_run(&[0x01, 0x02]);
123 let run_len = s.len();
124 s.push_str("tail");
125 let (decoded, end) = decode_run(&s);
126 assert_eq!(decoded, vec![0x01, 0x02]);
127 assert_eq!(
128 end, run_len,
129 "end offset must be the first non-selector byte"
130 );
131 assert_eq!(&s[end..], "tail");
132 }
133
134 #[test]
135 fn empty_input_decodes_to_nothing() {
136 assert_eq!(decode_run(""), (Vec::new(), 0));
137 assert_eq!(encode_run(&[]), "");
138 }
139}