use crate::bitstream::{BitReader, BitWriter};
use crate::error::Error;
const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
const HRP: &str = "md";
pub(crate) const REGULAR_CHECKSUM_SYMBOLS: usize = 13;
fn bits_to_symbols(payload_bytes: &[u8], bit_count: usize) -> Result<Vec<u8>, Error> {
let symbol_count = (bit_count + 4) / 5;
let mut r = BitReader::with_bit_limit(payload_bytes, bit_count);
let mut symbols = Vec::with_capacity(symbol_count);
for _ in 0..symbol_count {
let take = r.remaining_bits().min(5);
let val = if take == 0 {
0
} else {
r.read_bits(take)? as u8
};
let symbol = (val << (5 - take as u32)) & 0x1F;
symbols.push(symbol);
}
Ok(symbols)
}
fn symbols_to_bytes(symbols: &[u8]) -> Vec<u8> {
let mut w = BitWriter::new();
for &s in symbols {
w.write_bits((s & 0x1F) as u64, 5);
}
w.into_bytes()
}
fn symbol_to_char(s: u8) -> char {
CODEX32_ALPHABET[(s & 0x1F) as usize] as char
}
fn char_to_symbol(c: char) -> Option<u8> {
let lc = c.to_ascii_lowercase() as u8;
CODEX32_ALPHABET
.iter()
.position(|&b| b == lc)
.map(|i| i as u8)
}
pub fn wrap_payload(payload_bytes: &[u8], bit_count: usize) -> Result<String, Error> {
let data_symbols = bits_to_symbols(payload_bytes, bit_count)?;
let checksum: [u8; 13] = crate::bch::bch_create_checksum_regular(HRP, &data_symbols);
let mut s =
String::with_capacity(HRP.len() + 1 + data_symbols.len() + REGULAR_CHECKSUM_SYMBOLS);
s.push_str(HRP);
s.push('1'); for sym in &data_symbols {
s.push(symbol_to_char(*sym));
}
for sym in checksum.iter() {
s.push(symbol_to_char(*sym));
}
Ok(s)
}
pub fn unwrap_string(s: &str) -> Result<(Vec<u8>, usize), Error> {
let prefix = format!("{}1", HRP);
if !s.to_ascii_lowercase().starts_with(&prefix) {
return Err(Error::Codex32DecodeError(format!(
"string does not start with HRP {prefix}"
)));
}
let symbols_str = &s[prefix.len()..];
let mut symbols = Vec::with_capacity(symbols_str.len());
for c in symbols_str.chars() {
if c.is_whitespace() || c == '-' {
continue;
}
let sym = char_to_symbol(c).ok_or_else(|| {
Error::Codex32DecodeError(format!("character {c:?} not in codex32 alphabet"))
})?;
symbols.push(sym);
}
if !crate::bch::bch_verify_regular(HRP, &symbols) {
return Err(Error::Codex32DecodeError(
"BCH checksum verification failed".into(),
));
}
if symbols.len() < REGULAR_CHECKSUM_SYMBOLS {
return Err(Error::Codex32DecodeError(
"string too short for BCH checksum".into(),
));
}
let data_symbols = &symbols[..symbols.len() - REGULAR_CHECKSUM_SYMBOLS];
let bit_count = 5 * data_symbols.len();
Ok((symbols_to_bytes(data_symbols), bit_count))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_unwrap_round_trip_57_bits() {
let mut w = BitWriter::new();
w.write_bits(0xDEAD_BEEF_CAFE_BABE_u64 >> 7, 57);
let bytes = w.into_bytes();
let s = wrap_payload(&bytes, 57).unwrap();
assert_eq!(s.len(), 28);
assert!(s.starts_with("md1"));
let (out_bytes, out_bits) = unwrap_string(&s).unwrap();
assert_eq!(out_bits, 60);
assert_eq!(&out_bytes[..7], &bytes[..7]);
assert_eq!(out_bytes[7] & 0x80, bytes[7] & 0x80);
}
#[test]
fn wrap_unwrap_n3_chunk_byte_count_recovers_correctly() {
let bit_count = 37 + 24;
let mut w = BitWriter::new();
w.write_bits(0x1FFF_FFFF_FFFF_u64, 37); w.write_bits(0x00AA_BBCC_u64, 24);
let bytes = w.into_bytes();
assert_eq!(bytes.len(), 8); let s = wrap_payload(&bytes, bit_count).unwrap();
let (_out_bytes, out_bits) = unwrap_string(&s).unwrap();
assert_eq!(out_bits, 65);
let recovered_payload_byte_count = (out_bits - 37) / 8;
assert_eq!(recovered_payload_byte_count, 3);
}
#[test]
fn unwrap_rejects_non_md_string() {
assert!(unwrap_string("xx1qpz9r4cy7").is_err());
}
#[test]
fn unwrap_tolerates_visual_separators() {
let mut w = BitWriter::new();
w.write_bits(0b1010, 4);
let bytes = w.into_bytes();
let s = wrap_payload(&bytes, 4).unwrap();
let mut grouped = String::new();
for (i, c) in s.chars().enumerate() {
grouped.push(c);
if i == 3 {
grouped.push('-');
}
if i == 8 {
grouped.push(' ');
}
}
let _ = unwrap_string(&grouped).unwrap();
}
}