1use crate::bitstream::{BitReader, BitWriter};
9use crate::error::Error;
10
11const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
13
14const HRP: &str = "md";
16
17pub(crate) const REGULAR_CHECKSUM_SYMBOLS: usize = 13;
19
20fn bits_to_symbols(payload_bytes: &[u8], bit_count: usize) -> Result<Vec<u8>, Error> {
24 let symbol_count = (bit_count + 4) / 5;
25 let mut r = BitReader::with_bit_limit(payload_bytes, bit_count);
26 let mut symbols = Vec::with_capacity(symbol_count);
27 for _ in 0..symbol_count {
28 let take = r.remaining_bits().min(5);
29 let val = if take == 0 {
30 0
31 } else {
32 r.read_bits(take)? as u8
33 };
34 let symbol = (val << (5 - take as u32)) & 0x1F;
39 symbols.push(symbol);
40 }
41 Ok(symbols)
42}
43
44fn symbols_to_bytes(symbols: &[u8]) -> Vec<u8> {
46 let mut w = BitWriter::new();
47 for &s in symbols {
48 w.write_bits((s & 0x1F) as u64, 5);
49 }
50 w.into_bytes()
51}
52
53fn symbol_to_char(s: u8) -> char {
54 CODEX32_ALPHABET[(s & 0x1F) as usize] as char
55}
56
57fn char_to_symbol(c: char) -> Option<u8> {
58 let lc = c.to_ascii_lowercase() as u8;
59 CODEX32_ALPHABET
60 .iter()
61 .position(|&b| b == lc)
62 .map(|i| i as u8)
63}
64
65pub fn wrap_payload(payload_bytes: &[u8], bit_count: usize) -> Result<String, Error> {
68 let data_symbols = bits_to_symbols(payload_bytes, bit_count)?;
69 let checksum: [u8; 13] = crate::bch::bch_create_checksum_regular(HRP, &data_symbols);
71
72 let mut s =
73 String::with_capacity(HRP.len() + 1 + data_symbols.len() + REGULAR_CHECKSUM_SYMBOLS);
74 s.push_str(HRP);
75 s.push('1'); for sym in &data_symbols {
77 s.push(symbol_to_char(*sym));
78 }
79 for sym in checksum.iter() {
80 s.push(symbol_to_char(*sym));
81 }
82 Ok(s)
83}
84
85pub fn unwrap_string(s: &str) -> Result<(Vec<u8>, usize), Error> {
93 let prefix = format!("{}1", HRP);
95 if !s.to_ascii_lowercase().starts_with(&prefix) {
96 return Err(Error::Codex32DecodeError(format!(
97 "string does not start with HRP {prefix}"
98 )));
99 }
100 let symbols_str = &s[prefix.len()..];
101
102 let mut symbols = Vec::with_capacity(symbols_str.len());
104 for c in symbols_str.chars() {
105 if c.is_whitespace() || c == '-' {
106 continue;
107 }
108 let sym = char_to_symbol(c).ok_or_else(|| {
109 Error::Codex32DecodeError(format!("character {c:?} not in codex32 alphabet"))
110 })?;
111 symbols.push(sym);
112 }
113
114 if !crate::bch::bch_verify_regular(HRP, &symbols) {
116 return Err(Error::Codex32DecodeError(
117 "BCH checksum verification failed".into(),
118 ));
119 }
120
121 if symbols.len() < REGULAR_CHECKSUM_SYMBOLS {
123 return Err(Error::Codex32DecodeError(
124 "string too short for BCH checksum".into(),
125 ));
126 }
127 let data_symbols = &symbols[..symbols.len() - REGULAR_CHECKSUM_SYMBOLS];
128 let bit_count = 5 * data_symbols.len();
129
130 Ok((symbols_to_bytes(data_symbols), bit_count))
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn wrap_unwrap_round_trip_57_bits() {
140 let mut w = BitWriter::new();
142 w.write_bits(0xDEAD_BEEF_CAFE_BABE_u64 >> 7, 57);
143 let bytes = w.into_bytes();
144 let s = wrap_payload(&bytes, 57).unwrap();
145 assert_eq!(s.len(), 28);
147 assert!(s.starts_with("md1"));
148 let (out_bytes, out_bits) = unwrap_string(&s).unwrap();
149 assert_eq!(out_bits, 60);
151 assert_eq!(&out_bytes[..7], &bytes[..7]);
153 assert_eq!(out_bytes[7] & 0x80, bytes[7] & 0x80);
154 }
155
156 #[test]
162 fn wrap_unwrap_n3_chunk_byte_count_recovers_correctly() {
163 let bit_count = 37 + 24;
165 let mut w = BitWriter::new();
166 w.write_bits(0x1FFF_FFFF_FFFF_u64, 37); w.write_bits(0x00AA_BBCC_u64, 24);
168 let bytes = w.into_bytes();
169 assert_eq!(bytes.len(), 8); let s = wrap_payload(&bytes, bit_count).unwrap();
171 let (_out_bytes, out_bits) = unwrap_string(&s).unwrap();
172 assert_eq!(out_bits, 65);
174 let recovered_payload_byte_count = (out_bits - 37) / 8;
176 assert_eq!(recovered_payload_byte_count, 3);
177 }
178
179 #[test]
180 fn unwrap_rejects_non_md_string() {
181 assert!(unwrap_string("xx1qpz9r4cy7").is_err());
182 }
183
184 #[test]
185 fn unwrap_tolerates_visual_separators() {
186 let mut w = BitWriter::new();
187 w.write_bits(0b1010, 4);
188 let bytes = w.into_bytes();
189 let s = wrap_payload(&bytes, 4).unwrap();
190 let mut grouped = String::new();
191 for (i, c) in s.chars().enumerate() {
192 grouped.push(c);
193 if i == 3 {
194 grouped.push('-');
195 }
196 if i == 8 {
197 grouped.push(' ');
198 }
199 }
200 let _ = unwrap_string(&grouped).unwrap();
201 }
202}