|
| 1 | +//! Payload codec: 16-byte compressed URL ⇄ the on-ring bit vector. |
| 2 | +//! |
| 3 | +//! Encoding: pick a version by trimming leading zero bytes (≤14 bytes → v0 with 4 |
| 4 | +//! gap-parity symbols, else v1 with 2), scramble (reverse + XOR `0xA5`), split into |
| 5 | +//! the gaps and arcs blocks, RS-encode each plus a GF(16) metadata block carrying the |
| 6 | +//! version and a gap-inversion flag, assemble `[meta 16][gaps 104][template 0x2A]` |
| 7 | +//! into 128 bits, spatially permute them with the fixed LUT, then append a separator |
| 8 | +//! bit, the 56 arc bits and the extra gap bits. The gap bits are inverted when needed |
| 9 | +//! so the visible rings always carry more arcs than gaps. |
| 10 | +
|
| 11 | +use super::gf::{Gf, rs_decode, rs_encode}; |
| 12 | +use super::tables::GAPS_BITS_ORDER_LUT; |
| 13 | +use crate::error::{Error, Result}; |
| 14 | + |
| 15 | +struct FormatParams { |
| 16 | + gaps_data: usize, |
| 17 | + gaps_parity: usize, |
| 18 | + arcs_data: usize, |
| 19 | + arcs_parity: usize, |
| 20 | +} |
| 21 | + |
| 22 | +fn format_params(version: usize) -> FormatParams { |
| 23 | + if version == 0 { |
| 24 | + FormatParams { |
| 25 | + gaps_data: 9, |
| 26 | + gaps_parity: 4, |
| 27 | + arcs_data: 5, |
| 28 | + arcs_parity: 2, |
| 29 | + } |
| 30 | + } else { |
| 31 | + FormatParams { |
| 32 | + gaps_data: 11, |
| 33 | + gaps_parity: 2, |
| 34 | + arcs_data: 5, |
| 35 | + arcs_parity: 2, |
| 36 | + } |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +/// The template byte `0x2A`, stored LSB-first. |
| 41 | +const TEMPLATE_BITS: [bool; 8] = [false, true, false, true, false, true, false, false]; |
| 42 | + |
| 43 | +/// Encode a compressed-URL payload (≤ 16 bytes) into the final ring bit vector. |
| 44 | +pub fn encode_payload(payload: &[u8]) -> Result<Vec<bool>> { |
| 45 | + let trimmed: &[u8] = { |
| 46 | + let start = payload.iter().position(|&b| b != 0).unwrap_or(payload.len()); |
| 47 | + &payload[start..] |
| 48 | + }; |
| 49 | + if trimmed.len() > 16 { |
| 50 | + return Err(Error::capacity("App Clip payload exceeds 16 bytes")); |
| 51 | + } |
| 52 | + let version = usize::from(trimmed.len() > 14); |
| 53 | + let fp = format_params(version); |
| 54 | + let total = fp.gaps_data + fp.arcs_data; |
| 55 | + |
| 56 | + let mut padded = vec![0u8; total]; |
| 57 | + padded[total - trimmed.len()..].copy_from_slice(trimmed); |
| 58 | + |
| 59 | + // Scramble: reverse byte order and XOR 0xA5. |
| 60 | + let scrambled: Vec<u8> = (0..total).map(|i| padded[total - 1 - i] ^ 0xA5).collect(); |
| 61 | + |
| 62 | + let gf256 = Gf::f256(); |
| 63 | + let gaps_syms: Vec<usize> = scrambled[..fp.gaps_data].iter().map(|&b| b as usize).collect(); |
| 64 | + let mut gaps_bits = symbols_to_bits(&rs_encode(&gf256, &gaps_syms, fp.gaps_parity), 8); |
| 65 | + |
| 66 | + // Invert so the gaps ring always holds more zero (visible) than one bits. |
| 67 | + let gap_zeros = gaps_bits.iter().filter(|&&b| !b).count(); |
| 68 | + let inverted = gap_zeros <= 51; |
| 69 | + if inverted { |
| 70 | + for b in &mut gaps_bits { |
| 71 | + *b = !*b; |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + let gf16 = Gf::f16(); |
| 76 | + let meta = [version >> 3, usize::from(inverted) | ((version & 7) << 1)]; |
| 77 | + let meta_bits = symbols_to_bits(&rs_encode(&gf16, &meta, 2), 4); |
| 78 | + |
| 79 | + let arcs_syms: Vec<usize> = scrambled[total - fp.arcs_data..] |
| 80 | + .iter() |
| 81 | + .map(|&b| b as usize) |
| 82 | + .collect(); |
| 83 | + let arcs_bits = symbols_to_bits(&rs_encode(&gf256, &arcs_syms, fp.arcs_parity), 8); |
| 84 | + |
| 85 | + // Assemble [meta 16][gaps 104][template 8] and permute through the LUT. |
| 86 | + let mut pre = [false; 128]; |
| 87 | + pre[..16].copy_from_slice(&meta_bits); |
| 88 | + pre[16..120].copy_from_slice(&gaps_bits); |
| 89 | + pre[120..].copy_from_slice(&TEMPLATE_BITS); |
| 90 | + |
| 91 | + let zero_count = pre.iter().filter(|&&b| !b).count(); |
| 92 | + let mut out = vec![false; 129 + zero_count]; |
| 93 | + for (i, &b) in pre.iter().enumerate() { |
| 94 | + out[GAPS_BITS_ORDER_LUT[i]] = b; |
| 95 | + } |
| 96 | + |
| 97 | + // Separator (always 0) + arc bits + extra gap bits. |
| 98 | + out[129..129 + 56].copy_from_slice(&arcs_bits); |
| 99 | + let extra = zero_count.saturating_sub(56); |
| 100 | + if extra > 0 && extra <= gaps_bits.len() { |
| 101 | + out[185..185 + extra].copy_from_slice(&gaps_bits[..extra]); |
| 102 | + } |
| 103 | + Ok(out) |
| 104 | +} |
| 105 | + |
| 106 | +/// Decode a ring bit vector back to the right-aligned 16-byte payload. |
| 107 | +pub fn decode_payload(bits: &[bool]) -> Result<[u8; 16]> { |
| 108 | + if bits.len() < 128 { |
| 109 | + return Err(Error::undecodable("App Clip bit vector shorter than 128")); |
| 110 | + } |
| 111 | + |
| 112 | + // Reverse the LUT permutation. |
| 113 | + let mut pre = [false; 128]; |
| 114 | + for (i, p) in pre.iter_mut().enumerate() { |
| 115 | + *p = bits[GAPS_BITS_ORDER_LUT[i]]; |
| 116 | + } |
| 117 | + |
| 118 | + // Metadata: four GF(16) symbols, RS(4,2). |
| 119 | + let gf16 = Gf::f16(); |
| 120 | + let meta_cw: Vec<usize> = (0..4) |
| 121 | + .map(|i| bits_to_symbol(&pre[i * 4..i * 4 + 4])) |
| 122 | + .collect(); |
| 123 | + let meta = rs_decode(&gf16, &meta_cw, 2) |
| 124 | + .ok_or_else(|| Error::undecodable("App Clip metadata RS failed"))?; |
| 125 | + let version = (meta[0] << 3) | (meta[1] >> 1); |
| 126 | + let inverted = meta[1] & 1 == 1; |
| 127 | + if version > 1 { |
| 128 | + return Err(Error::undecodable("invalid App Clip codec version")); |
| 129 | + } |
| 130 | + let fp = format_params(version); |
| 131 | + |
| 132 | + // Gaps: thirteen GF(256) symbols. |
| 133 | + let mut gap_bits: Vec<bool> = pre[16..120].to_vec(); |
| 134 | + if inverted { |
| 135 | + for b in &mut gap_bits { |
| 136 | + *b = !*b; |
| 137 | + } |
| 138 | + } |
| 139 | + let gap_cw: Vec<usize> = (0..13) |
| 140 | + .map(|i| bits_to_symbol(&gap_bits[i * 8..i * 8 + 8])) |
| 141 | + .collect(); |
| 142 | + let gf256 = Gf::f256(); |
| 143 | + let gap_syms = rs_decode(&gf256, &gap_cw, fp.gaps_parity) |
| 144 | + .ok_or_else(|| Error::undecodable("App Clip gaps RS failed"))?; |
| 145 | + |
| 146 | + // Arcs, when the color stream is present. |
| 147 | + let total = fp.gaps_data + fp.arcs_data; |
| 148 | + let mut scrambled = vec![0u8; total]; |
| 149 | + for i in 0..fp.gaps_data { |
| 150 | + scrambled[i] = gap_syms[i] as u8; |
| 151 | + } |
| 152 | + if bits.len() >= 128 + 57 { |
| 153 | + if bits[128] { |
| 154 | + return Err(Error::undecodable("invalid App Clip separator bit")); |
| 155 | + } |
| 156 | + let arc_cw: Vec<usize> = (0..7) |
| 157 | + .map(|i| bits_to_symbol(&bits[129 + i * 8..129 + i * 8 + 8])) |
| 158 | + .collect(); |
| 159 | + let arc_syms = rs_decode(&gf256, &arc_cw, fp.arcs_parity) |
| 160 | + .ok_or_else(|| Error::undecodable("App Clip arcs RS failed"))?; |
| 161 | + for i in 0..fp.arcs_data { |
| 162 | + scrambled[fp.gaps_data + i] = arc_syms[i] as u8; |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + // Unscramble and right-align into 16 bytes. |
| 167 | + let mut payload = [0u8; 16]; |
| 168 | + for i in 0..total { |
| 169 | + payload[16 - total + (total - 1 - i)] = scrambled[i] ^ 0xA5; |
| 170 | + } |
| 171 | + Ok(payload) |
| 172 | +} |
| 173 | + |
| 174 | +/// Symbols to bits, MSB-first per symbol. |
| 175 | +fn symbols_to_bits(symbols: &[usize], bits_per: usize) -> Vec<bool> { |
| 176 | + let mut out = Vec::with_capacity(symbols.len() * bits_per); |
| 177 | + for &s in symbols { |
| 178 | + for j in (0..bits_per).rev() { |
| 179 | + out.push((s >> j) & 1 == 1); |
| 180 | + } |
| 181 | + } |
| 182 | + out |
| 183 | +} |
| 184 | + |
| 185 | +fn bits_to_symbol(bits: &[bool]) -> usize { |
| 186 | + bits.iter().fold(0usize, |acc, &b| (acc << 1) | usize::from(b)) |
| 187 | +} |
| 188 | + |
| 189 | +#[cfg(test)] |
| 190 | +mod tests { |
| 191 | + use super::*; |
| 192 | + |
| 193 | + #[test] |
| 194 | + fn payload_roundtrip_both_versions() { |
| 195 | + // ≤14 significant bytes → version 0; 15-16 → version 1. |
| 196 | + let mut short = [0u8; 16]; |
| 197 | + short[14] = 0x42; |
| 198 | + short[15] = 0xAA; |
| 199 | + let mut long = [0u8; 16]; |
| 200 | + for (i, b) in long.iter_mut().enumerate() { |
| 201 | + *b = (i as u8).wrapping_mul(17).wrapping_add(1); |
| 202 | + } |
| 203 | + for payload in [short, long] { |
| 204 | + let bits = encode_payload(&payload).unwrap(); |
| 205 | + assert!(bits.len() >= 129, "vector too short: {}", bits.len()); |
| 206 | + let back = decode_payload(&bits).unwrap(); |
| 207 | + assert_eq!(back, payload); |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + #[test] |
| 212 | + fn gap_ring_majority_zero() { |
| 213 | + // The inversion step guarantees more zero (arc) than one (gap) bits among the |
| 214 | + // 104 gap bits, whatever the payload. |
| 215 | + for seed in 0u8..16 { |
| 216 | + let payload = [seed.wrapping_mul(37); 16]; |
| 217 | + let bits = encode_payload(&payload).unwrap(); |
| 218 | + // Count zeros in the whole 128-bit permuted block: ≥ the guarantee on the |
| 219 | + // gaps portion alone minus meta/template variance; the strict per-spec |
| 220 | + // check is on the pre-permutation gaps, so re-derive them. |
| 221 | + let mut pre = [false; 128]; |
| 222 | + for (i, p) in pre.iter_mut().enumerate() { |
| 223 | + *p = bits[GAPS_BITS_ORDER_LUT[i]]; |
| 224 | + } |
| 225 | + let zeros = pre[16..120].iter().filter(|&&b| !b).count(); |
| 226 | + assert!(zeros >= 52, "gap zeros {zeros} < 52 for seed {seed}"); |
| 227 | + } |
| 228 | + } |
| 229 | +} |
0 commit comments