Skip to main content

c2pa_unstructured_text/
vs.rs

1// Copyright 2026 WritersLogic. All rights reserved.
2// Licensed under the Apache License, Version 2.0 or the MIT license,
3// at your option.
4
5//! The variation-selector byte codec (C2PA 2.4 Appendix A.8).
6//!
7//! Maps a byte onto one invisible Unicode variation selector and back. Low
8//! nibble values `0x00..=0x0F` use the Variation Selectors block
9//! (`U+FE00..U+FE0F`); the remaining `0x10..=0xFF` use Variation Selectors
10//! Supplement (`U+E0100..U+E01EF`). The two ranges are contiguous in byte space,
11//! so the mapping is a bijection over all 256 byte values.
12//!
13//! This module is the transport alphabet only. The framing that carries a
14//! Manifest Store (magic, version, length, payload) lives in
15//! [`crate::wrapper`], and the content binding in [`crate::hardbinding`].
16
17/// First code point of the Variation Selectors block, encoding byte `0x00`.
18const VS_START: u32 = 0xFE00;
19/// Last code point of the Variation Selectors block, encoding byte `0x0F`.
20const VS_END: u32 = 0xFE0F;
21/// First code point of Variation Selectors Supplement, encoding byte `0x10`.
22const VS_SUP_START: u32 = 0xE0100;
23/// Last code point of Variation Selectors Supplement, encoding byte `0xFF`.
24const VS_SUP_END: u32 = 0xE01EF;
25
26/// Map a byte to its variation selector (A.8 `byteToVariationSelector`).
27pub 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    // Both ranges are inside the BMP/SMP with no surrogates, so every value is
34    // a valid Unicode scalar. Exhaustively covered by `codec_is_a_bijection`.
35    char::from_u32(cp).expect("variation-selector code points are valid scalars")
36}
37
38/// Map a variation selector back to its byte, or `None` if `c` is not one
39/// (A.8 `variationSelectorToByte`).
40pub 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
48/// Whether `c` is a variation selector usable by this codec.
49///
50/// Note this is narrower than "is a variation selector": `U+FE0F` and friends
51/// are in range, but the Supplement block extends to `U+E01EF` only, which is
52/// exactly 240 code points and completes the 256-value byte space.
53pub fn is_vs(c: char) -> bool {
54    vs_to_byte(c).is_some()
55}
56
57/// Encode `bytes` as a bare run of variation selectors, with no marker or frame.
58pub fn encode_run(bytes: &[u8]) -> String {
59    bytes.iter().map(|&b| byte_to_vs(b)).collect()
60}
61
62/// Decode a leading run of variation selectors from `s`, stopping at the first
63/// character that is not one. Returns the bytes and the byte offset in `s` where
64/// the run ended.
65pub 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        // Visible text, the wrapper marker, and code points adjacent to both
102        // ranges must all be outside the alphabet.
103        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        // One past the end of the Supplement block.
108        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}