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.div_ceil(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(crate) fn is_mixed_case(s: &str) -> bool {
91 let mut has_upper = false;
92 let mut has_lower = false;
93 for c in s.chars() {
94 if c.is_ascii_uppercase() {
95 has_upper = true;
96 } else if c.is_ascii_lowercase() {
97 has_lower = true;
98 }
99 if has_upper && has_lower {
100 return true;
101 }
102 }
103 false
104}
105
106pub fn unwrap_string(s: &str) -> Result<(Vec<u8>, usize), Error> {
114 if is_mixed_case(s) {
118 return Err(Error::Codex32DecodeError(
119 "string mixes upper and lower case (BIP-173 forbids mixed case)".to_string(),
120 ));
121 }
122 let prefix = format!("{}1", HRP);
124 if !s.to_ascii_lowercase().starts_with(&prefix) {
125 return Err(Error::Codex32DecodeError(format!(
126 "string does not start with HRP {prefix}"
127 )));
128 }
129 let symbols_str = &s[prefix.len()..];
130
131 let mut symbols = Vec::with_capacity(symbols_str.len());
133 for c in symbols_str.chars() {
134 if c.is_whitespace() || c == '-' {
135 continue;
136 }
137 let sym = char_to_symbol(c).ok_or_else(|| {
138 Error::Codex32DecodeError(format!("character {c:?} not in codex32 alphabet"))
139 })?;
140 symbols.push(sym);
141 }
142
143 if !crate::bch::bch_verify_regular(HRP, &symbols) {
145 return Err(Error::Codex32DecodeError(
146 "BCH checksum verification failed".into(),
147 ));
148 }
149
150 if symbols.len() < REGULAR_CHECKSUM_SYMBOLS {
152 return Err(Error::Codex32DecodeError(
153 "string too short for BCH checksum".into(),
154 ));
155 }
156 let data_symbols = &symbols[..symbols.len() - REGULAR_CHECKSUM_SYMBOLS];
157 let bit_count = 5 * data_symbols.len();
158
159 Ok((symbols_to_bytes(data_symbols), bit_count))
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn wrap_unwrap_round_trip_57_bits() {
169 let mut w = BitWriter::new();
171 w.write_bits(0xDEAD_BEEF_CAFE_BABE_u64 >> 7, 57);
172 let bytes = w.into_bytes();
173 let s = wrap_payload(&bytes, 57).unwrap();
174 assert_eq!(s.len(), 28);
176 assert!(s.starts_with("md1"));
177 let (out_bytes, out_bits) = unwrap_string(&s).unwrap();
178 assert_eq!(out_bits, 60);
180 assert_eq!(&out_bytes[..7], &bytes[..7]);
182 assert_eq!(out_bytes[7] & 0x80, bytes[7] & 0x80);
183 }
184
185 #[test]
191 fn wrap_unwrap_n3_chunk_byte_count_recovers_correctly() {
192 let bit_count = 37 + 24;
194 let mut w = BitWriter::new();
195 w.write_bits(0x1FFF_FFFF_FFFF_u64, 37); w.write_bits(0x00AA_BBCC_u64, 24);
197 let bytes = w.into_bytes();
198 assert_eq!(bytes.len(), 8); let s = wrap_payload(&bytes, bit_count).unwrap();
200 let (_out_bytes, out_bits) = unwrap_string(&s).unwrap();
201 assert_eq!(out_bits, 65);
203 let recovered_payload_byte_count = (out_bits - 37) / 8;
205 assert_eq!(recovered_payload_byte_count, 3);
206 }
207
208 #[test]
209 fn unwrap_rejects_non_md_string() {
210 assert!(unwrap_string("xx1qpz9r4cy7").is_err());
211 }
212
213 #[test]
214 fn unwrap_tolerates_visual_separators() {
215 let mut w = BitWriter::new();
216 w.write_bits(0b1010, 4);
217 let bytes = w.into_bytes();
218 let s = wrap_payload(&bytes, 4).unwrap();
219 let mut grouped = String::new();
220 for (i, c) in s.chars().enumerate() {
221 grouped.push(c);
222 if i == 3 {
223 grouped.push('-');
224 }
225 if i == 8 {
226 grouped.push(' ');
227 }
228 }
229 let _ = unwrap_string(&grouped).unwrap();
230 }
231}