use super::bch_decode;
use crate::consts::{HRP, MK_LONG_CONST, MK_REGULAR_CONST};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BchCode {
Regular,
Long,
}
pub const ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
const ALPHABET_INV: [u8; 128] = build_alphabet_inv();
const fn build_alphabet_inv() -> [u8; 128] {
let mut inv = [0xFFu8; 128];
let mut i = 0;
while i < 32 {
inv[ALPHABET[i] as usize] = i as u8;
i += 1;
}
inv
}
pub fn bytes_to_5bit(bytes: &[u8]) -> Vec<u8> {
let mut acc: u32 = 0;
let mut bits = 0u32;
let mut out = Vec::with_capacity((bytes.len() * 8).div_ceil(5));
for &b in bytes {
acc = (acc << 8) | b as u32;
bits += 8;
while bits >= 5 {
bits -= 5;
out.push(((acc >> bits) & 0x1F) as u8);
}
}
if bits > 0 {
out.push(((acc << (5 - bits)) & 0x1F) as u8);
}
out
}
pub fn five_bit_to_bytes(values: &[u8]) -> Option<Vec<u8>> {
let mut acc: u32 = 0;
let mut bits = 0u32;
let mut out = Vec::with_capacity(values.len() * 5 / 8);
for &v in values {
if v >= 32 {
return None;
}
acc = (acc << 5) | v as u32;
bits += 5;
if bits >= 8 {
bits -= 8;
out.push(((acc >> bits) & 0xFF) as u8);
}
}
if bits >= 5 {
return None;
}
if (acc & ((1 << bits) - 1)) != 0 {
return None;
}
Some(out)
}
pub const SEPARATOR: char = '1';
pub fn bch_code_for_length(data_part_len: usize) -> Option<BchCode> {
match data_part_len {
14..=93 => Some(BchCode::Regular),
94..=95 => None,
96..=108 => Some(BchCode::Long),
_ => None,
}
}
pub fn case_check(s: &str) -> CaseStatus {
let mut has_lower = false;
let mut has_upper = false;
for c in s.chars() {
if c.is_ascii_lowercase() {
has_lower = true;
} else if c.is_ascii_uppercase() {
has_upper = true;
}
if has_lower && has_upper {
break;
}
}
match (has_lower, has_upper) {
(true, true) => CaseStatus::Mixed,
(true, false) => CaseStatus::Lower,
(false, true) => CaseStatus::Upper,
(false, false) => CaseStatus::Lower, }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaseStatus {
Lower,
Upper,
Mixed,
}
pub const GEN_REGULAR: [u128; 5] = [
0x19dc500ce73fde210,
0x1bfae00def77fe529,
0x1fbd920fffe7bee52,
0x1739640bdeee3fdad,
0x07729a039cfc75f5a,
];
pub const POLYMOD_INIT: u128 = 0x23181b3;
pub const REGULAR_SHIFT: u32 = 60;
pub const REGULAR_MASK: u128 = 0x0fffffffffffffff;
pub const GEN_LONG: [u128; 5] = [
0x3d59d273535ea62d897,
0x7a9becb6361c6c51507,
0x543f9b7e6c38d8a2a0e,
0x0c577eaeccf1990d13c,
0x1887f74f8dc71b10651,
];
pub const LONG_SHIFT: u32 = 70;
pub const LONG_MASK: u128 = 0x3fffffffffffffffff;
fn polymod_step(residue: u128, value: u128, r#gen: &[u128; 5], shift: u32, mask: u128) -> u128 {
let b = residue >> shift;
let mut new_residue = ((residue & mask) << 5) ^ value;
for (i, &g) in r#gen.iter().enumerate() {
if (b >> i) & 1 != 0 {
new_residue ^= g;
}
}
new_residue
}
pub fn hrp_expand(hrp: &str) -> Vec<u8> {
let bytes = hrp.as_bytes();
let mut out = Vec::with_capacity(bytes.len() * 2 + 1);
for &c in bytes {
out.push(c >> 5);
}
out.push(0);
for &c in bytes {
out.push(c & 31);
}
out
}
pub(in crate::string_layer) fn polymod_run(
values: &[u8],
r#gen: &[u128; 5],
shift: u32,
mask: u128,
) -> u128 {
let mut residue = POLYMOD_INIT;
for &v in values {
residue = polymod_step(residue, v as u128, r#gen, shift, mask);
}
residue
}
pub fn bch_create_checksum_regular(hrp: &str, data: &[u8]) -> [u8; 13] {
let mut input = hrp_expand(hrp);
input.extend_from_slice(data);
input.extend(std::iter::repeat_n(0, 13));
let polymod = polymod_run(&input, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK) ^ MK_REGULAR_CONST;
let mut out = [0u8; 13];
for (i, slot) in out.iter_mut().enumerate() {
*slot = ((polymod >> (5 * (12 - i))) & 0x1F) as u8;
}
out
}
pub fn bch_verify_regular(hrp: &str, data_with_checksum: &[u8]) -> bool {
if data_with_checksum.len() < 13 {
return false;
}
let mut input = hrp_expand(hrp);
input.extend_from_slice(data_with_checksum);
polymod_run(&input, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK) == MK_REGULAR_CONST
}
pub fn bch_create_checksum_long(hrp: &str, data: &[u8]) -> [u8; 15] {
let mut input = hrp_expand(hrp);
input.extend_from_slice(data);
input.extend(std::iter::repeat_n(0, 15));
let polymod = polymod_run(&input, &GEN_LONG, LONG_SHIFT, LONG_MASK) ^ MK_LONG_CONST;
let mut out = [0u8; 15];
for (i, slot) in out.iter_mut().enumerate() {
*slot = ((polymod >> (5 * (14 - i))) & 0x1F) as u8;
}
out
}
pub fn bch_verify_long(hrp: &str, data_with_checksum: &[u8]) -> bool {
if data_with_checksum.len() < 15 {
return false;
}
let mut input = hrp_expand(hrp);
input.extend_from_slice(data_with_checksum);
polymod_run(&input, &GEN_LONG, LONG_SHIFT, LONG_MASK) == MK_LONG_CONST
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CorrectionResult {
pub data: Vec<u8>,
pub corrections_applied: usize,
pub corrected_positions: Vec<usize>,
}
pub fn bch_correct_regular(
hrp: &str,
data_with_checksum: &[u8],
) -> Result<CorrectionResult, crate::Error> {
if bch_verify_regular(hrp, data_with_checksum) {
return Ok(CorrectionResult {
data: data_with_checksum.to_vec(),
corrections_applied: 0,
corrected_positions: vec![],
});
}
let mut input = hrp_expand(hrp);
input.extend_from_slice(data_with_checksum);
let residue = polymod_run(&input, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK) ^ MK_REGULAR_CONST;
if let Some((positions, magnitudes)) =
bch_decode::decode_regular_errors(residue, data_with_checksum.len())
{
if positions.is_empty() {
return Ok(CorrectionResult {
data: data_with_checksum.to_vec(),
corrections_applied: 0,
corrected_positions: vec![],
});
}
let mut corrected = data_with_checksum.to_vec();
for (&p, &m) in positions.iter().zip(&magnitudes) {
if p >= corrected.len() {
return Err(crate::Error::BchUncorrectable(format!(
"decoder reported error position {p} outside data ({} symbols)",
corrected.len()
)));
}
corrected[p] ^= m;
}
if bch_verify_regular(hrp, &corrected) {
return Ok(CorrectionResult {
corrections_applied: positions.len(),
corrected_positions: positions,
data: corrected,
});
}
}
Err(crate::Error::BchUncorrectable(
"regular code: more than 4 substitutions or pathological pattern".into(),
))
}
pub fn bch_correct_long(
hrp: &str,
data_with_checksum: &[u8],
) -> Result<CorrectionResult, crate::Error> {
if bch_verify_long(hrp, data_with_checksum) {
return Ok(CorrectionResult {
data: data_with_checksum.to_vec(),
corrections_applied: 0,
corrected_positions: vec![],
});
}
let mut input = hrp_expand(hrp);
input.extend_from_slice(data_with_checksum);
let residue = polymod_run(&input, &GEN_LONG, LONG_SHIFT, LONG_MASK) ^ MK_LONG_CONST;
if let Some((positions, magnitudes)) =
bch_decode::decode_long_errors(residue, data_with_checksum.len())
{
if positions.is_empty() {
return Ok(CorrectionResult {
data: data_with_checksum.to_vec(),
corrections_applied: 0,
corrected_positions: vec![],
});
}
let mut corrected = data_with_checksum.to_vec();
for (&p, &m) in positions.iter().zip(&magnitudes) {
if p >= corrected.len() {
return Err(crate::Error::BchUncorrectable(format!(
"decoder reported error position {p} outside data ({} symbols)",
corrected.len()
)));
}
corrected[p] ^= m;
}
if bch_verify_long(hrp, &corrected) {
return Ok(CorrectionResult {
corrections_applied: positions.len(),
corrected_positions: positions,
data: corrected,
});
}
}
Err(crate::Error::BchUncorrectable(
"long code: more than 4 substitutions or pathological pattern".into(),
))
}
pub fn encode_5bit_to_string(data_5bit: &[u8]) -> Result<String, crate::Error> {
use crate::Error;
let regular_total = data_5bit.len() + 13;
let long_total = data_5bit.len() + 15;
let code = match (
bch_code_for_length(regular_total),
bch_code_for_length(long_total),
) {
(Some(BchCode::Regular), _) => BchCode::Regular,
(_, Some(BchCode::Long)) => BchCode::Long,
_ => {
return Err(Error::InvalidStringLength(long_total));
}
};
let checksum: Vec<u8> = match code {
BchCode::Regular => bch_create_checksum_regular(HRP, data_5bit).to_vec(),
BchCode::Long => bch_create_checksum_long(HRP, data_5bit).to_vec(),
};
let mut full = String::with_capacity(HRP.len() + 1 + data_5bit.len() + checksum.len());
full.push_str(HRP);
full.push(SEPARATOR);
for &v in data_5bit {
full.push(ALPHABET[v as usize] as char);
}
for v in checksum {
full.push(ALPHABET[v as usize] as char);
}
Ok(full)
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedString {
pub code: BchCode,
pub corrections_applied: usize,
pub corrected_positions: Vec<usize>,
pub data_with_checksum: Vec<u8>,
}
impl DecodedString {
pub fn data(&self) -> &[u8] {
let checksum_len = match self.code {
BchCode::Regular => 13,
BchCode::Long => 15,
};
&self.data_with_checksum[..self.data_with_checksum.len() - checksum_len]
}
pub fn corrected_char_at(&self, char_position: usize) -> char {
let v = self.data_with_checksum[char_position];
ALPHABET[v as usize] as char
}
}
pub fn decode_string(s: &str) -> Result<DecodedString, crate::Error> {
use crate::Error;
if matches!(case_check(s), CaseStatus::Mixed) {
return Err(Error::MixedCase);
}
let s_lower = s.to_lowercase();
let sep_pos = s_lower
.rfind(SEPARATOR)
.ok_or_else(|| Error::InvalidHrp(s_lower.clone()))?;
let (hrp, rest) = s_lower.split_at(sep_pos);
let data_part = &rest[1..];
if hrp != HRP {
return Err(Error::InvalidHrp(hrp.to_string()));
}
let code =
bch_code_for_length(data_part.len()).ok_or(Error::InvalidStringLength(data_part.len()))?;
let mut values: Vec<u8> = Vec::with_capacity(data_part.len());
for (i, c) in data_part.chars().enumerate() {
if !c.is_ascii() {
return Err(Error::InvalidChar { ch: c, position: i });
}
let v = ALPHABET_INV[c as usize];
if v == 0xFF {
return Err(Error::InvalidChar { ch: c, position: i });
}
values.push(v);
}
let correction = match code {
BchCode::Regular => bch_correct_regular(hrp, &values),
BchCode::Long => bch_correct_long(hrp, &values),
};
let result = correction?;
Ok(DecodedString {
code,
corrections_applied: result.corrections_applied,
corrected_positions: result.corrected_positions,
data_with_checksum: result.data,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bch_code_equality() {
assert_eq!(BchCode::Regular, BchCode::Regular);
assert_ne!(BchCode::Regular, BchCode::Long);
}
#[test]
fn bch_code_can_be_hashed() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(BchCode::Regular);
set.insert(BchCode::Long);
set.insert(BchCode::Regular);
assert_eq!(set.len(), 2);
}
#[test]
fn alphabet_is_32_unique_chars() {
let mut seen = std::collections::HashSet::new();
for &c in ALPHABET {
assert!(seen.insert(c), "duplicate char in alphabet: {}", c as char);
}
assert_eq!(seen.len(), 32);
}
#[test]
fn bytes_to_5bit_round_trip_zero() {
let bytes = vec![0x00];
let fives = bytes_to_5bit(&bytes);
assert_eq!(fives, vec![0, 0]);
let back = five_bit_to_bytes(&fives).unwrap();
assert_eq!(back, bytes);
}
#[test]
fn bytes_to_5bit_round_trip_known_value() {
let bytes = vec![0xFF];
let fives = bytes_to_5bit(&bytes);
assert_eq!(fives, vec![31, 28]);
}
#[test]
fn bytes_to_5bit_round_trip_multibyte() {
let bytes = vec![0xDE, 0xAD, 0xBE];
let back = five_bit_to_bytes(&bytes_to_5bit(&bytes)).unwrap();
assert_eq!(back, bytes);
}
#[test]
fn five_bit_to_bytes_rejects_nonzero_padding() {
assert!(five_bit_to_bytes(&[31, 1]).is_none());
}
#[test]
fn five_bit_to_bytes_rejects_value_out_of_range() {
assert!(five_bit_to_bytes(&[32]).is_none());
}
#[test]
fn bch_code_for_length_regular() {
assert_eq!(bch_code_for_length(14), Some(BchCode::Regular));
assert_eq!(bch_code_for_length(93), Some(BchCode::Regular));
}
#[test]
fn bch_code_for_length_long() {
assert_eq!(bch_code_for_length(96), Some(BchCode::Long));
assert_eq!(bch_code_for_length(108), Some(BchCode::Long));
}
#[test]
fn bch_code_for_length_rejects_94_and_95() {
assert_eq!(bch_code_for_length(94), None);
assert_eq!(bch_code_for_length(95), None);
}
#[test]
fn bch_code_for_length_rejects_extremes() {
assert_eq!(bch_code_for_length(0), None);
assert_eq!(bch_code_for_length(13), None);
assert_eq!(bch_code_for_length(109), None);
assert_eq!(bch_code_for_length(1000), None);
}
#[test]
fn case_check_lowercase() {
assert_eq!(case_check("md1qq"), CaseStatus::Lower);
}
#[test]
fn case_check_uppercase() {
assert_eq!(case_check("MD1QQ"), CaseStatus::Upper);
}
#[test]
fn case_check_mixed() {
assert_eq!(case_check("mD1qq"), CaseStatus::Mixed);
}
#[test]
fn case_check_empty_string_is_lower() {
assert_eq!(case_check(""), CaseStatus::Lower);
}
#[test]
fn case_check_digits_only_is_lower() {
assert_eq!(case_check("1234"), CaseStatus::Lower);
}
#[test]
fn gen_regular_has_5_entries() {
assert_eq!(GEN_REGULAR.len(), 5);
}
#[test]
fn gen_long_has_5_entries() {
assert_eq!(GEN_LONG.len(), 5);
}
#[test]
fn gen_regular_matches_bip93_canonical_values() {
assert_eq!(GEN_REGULAR[0], 0x19dc500ce73fde210);
assert_eq!(GEN_REGULAR[1], 0x1bfae00def77fe529);
assert_eq!(GEN_REGULAR[2], 0x1fbd920fffe7bee52);
assert_eq!(GEN_REGULAR[3], 0x1739640bdeee3fdad);
assert_eq!(GEN_REGULAR[4], 0x07729a039cfc75f5a);
}
#[test]
fn gen_long_matches_bip93_canonical_values() {
assert_eq!(GEN_LONG[0], 0x3d59d273535ea62d897);
assert_eq!(GEN_LONG[1], 0x7a9becb6361c6c51507);
assert_eq!(GEN_LONG[2], 0x543f9b7e6c38d8a2a0e);
assert_eq!(GEN_LONG[3], 0x0c577eaeccf1990d13c);
assert_eq!(GEN_LONG[4], 0x1887f74f8dc71b10651);
}
#[test]
fn polymod_init_matches_bip93() {
assert_eq!(POLYMOD_INIT, 0x23181b3);
}
#[test]
fn polymod_masks_are_consistent_with_shifts() {
assert_eq!(REGULAR_MASK, (1u128 << REGULAR_SHIFT) - 1);
assert_eq!(LONG_MASK, (1u128 << LONG_SHIFT) - 1);
assert_eq!(REGULAR_SHIFT, 60);
assert_eq!(LONG_SHIFT, 70);
}
#[test]
fn polymod_step_zero_residue_zero_value() {
assert_eq!(
polymod_step(0, 0, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK),
0
);
}
#[test]
fn polymod_step_value_only_xor_when_residue_zero() {
assert_eq!(
polymod_step(0, 7, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK),
7
);
}
#[test]
fn polymod_step_isolates_each_gen_entry() {
for i in 0..5 {
let r = 1u128 << (REGULAR_SHIFT + i);
assert_eq!(
polymod_step(r, 0, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK),
GEN_REGULAR[i as usize],
"bit {} of b should isolate GEN_REGULAR[{}]",
i,
i
);
}
}
#[test]
fn polymod_step_xors_multiple_gens_when_multiple_b_bits_set() {
let r = 0b00011u128 << REGULAR_SHIFT;
assert_eq!(
polymod_step(r, 0, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK),
GEN_REGULAR[0] ^ GEN_REGULAR[1]
);
let r = 0b11111u128 << REGULAR_SHIFT;
let expected =
GEN_REGULAR[0] ^ GEN_REGULAR[1] ^ GEN_REGULAR[2] ^ GEN_REGULAR[3] ^ GEN_REGULAR[4];
assert_eq!(
polymod_step(r, 0, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK),
expected
);
}
#[test]
fn polymod_step_works_for_long_code() {
let r = 1u128 << LONG_SHIFT;
assert_eq!(
polymod_step(r, 0, &GEN_LONG, LONG_SHIFT, LONG_MASK),
GEN_LONG[0]
);
let r = 0b11111u128 << LONG_SHIFT;
let expected = GEN_LONG[0] ^ GEN_LONG[1] ^ GEN_LONG[2] ^ GEN_LONG[3] ^ GEN_LONG[4];
assert_eq!(
polymod_step(r, 0, &GEN_LONG, LONG_SHIFT, LONG_MASK),
expected
);
}
#[test]
fn polymod_step_init_residue_first_iteration() {
assert_eq!(
polymod_step(POLYMOD_INIT, 0, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK),
POLYMOD_INIT << 5
);
assert_eq!(
polymod_step(POLYMOD_INIT, 31, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK),
(POLYMOD_INIT << 5) ^ 31
);
}
#[test]
fn polymod_step_value_and_gen_xor_combined() {
let r = 1u128 << REGULAR_SHIFT;
assert_eq!(
polymod_step(r, 5, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK),
GEN_REGULAR[0] ^ 5
);
}
#[test]
fn hrp_expand_mk_matches_spec() {
assert_eq!(hrp_expand(crate::consts::HRP), vec![3, 3, 0, 13, 11]);
}
#[test]
fn hrp_expand_empty_returns_just_separator() {
assert_eq!(hrp_expand(""), vec![0]);
}
#[test]
fn bch_round_trip_regular() {
let hrp = "mk";
let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let checksum = bch_create_checksum_regular(hrp, &data);
assert_eq!(checksum.len(), 13);
let mut full = data.clone();
full.extend_from_slice(&checksum);
assert!(bch_verify_regular(hrp, &full));
}
#[test]
fn bch_verify_rejects_single_char_tampering_regular() {
let hrp = "mk";
let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let checksum = bch_create_checksum_regular(hrp, &data);
let mut full = data.clone();
full.extend_from_slice(&checksum);
full[5] ^= 0x01;
assert!(!bch_verify_regular(hrp, &full));
}
#[test]
fn bch_verify_rejects_too_short_input_regular() {
assert!(!bch_verify_regular("mk", &[0, 1, 2]));
assert!(!bch_verify_regular("mk", &[]));
}
#[test]
fn bch_zero_data_does_not_self_validate_regular() {
let mut zero = vec![0u8; 8];
zero.extend(std::iter::repeat_n(0, 13));
assert!(!bch_verify_regular("mk", &zero));
}
#[test]
fn bch_round_trip_empty_data_regular() {
let checksum = bch_create_checksum_regular("mk", &[]);
assert!(bch_verify_regular("mk", &checksum));
}
#[test]
fn bch_round_trip_long() {
let hrp = "mk";
let data: Vec<u8> = (0..16).collect();
let checksum = bch_create_checksum_long(hrp, &data);
assert_eq!(checksum.len(), 15);
let mut full = data.clone();
full.extend_from_slice(&checksum);
assert!(bch_verify_long(hrp, &full));
}
#[test]
fn bch_verify_rejects_single_char_tampering_long() {
let hrp = "mk";
let data: Vec<u8> = (0..16).collect();
let checksum = bch_create_checksum_long(hrp, &data);
let mut full = data.clone();
full.extend_from_slice(&checksum);
full[7] ^= 0x01;
assert!(!bch_verify_long(hrp, &full));
}
#[test]
fn bch_verify_rejects_too_short_input_long() {
assert!(!bch_verify_long("mk", &[0; 14]));
assert!(!bch_verify_long("mk", &[]));
}
#[test]
fn bch_zero_data_does_not_self_validate_long() {
let mut zero = vec![0u8; 16];
zero.extend(std::iter::repeat_n(0, 15));
assert!(!bch_verify_long("mk", &zero));
}
#[test]
fn bch_round_trip_empty_data_long() {
let checksum = bch_create_checksum_long("mk", &[]);
assert!(bch_verify_long("mk", &checksum));
}
#[test]
fn bch_correct_regular_clean_input() {
let hrp = "mk";
let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let checksum = bch_create_checksum_regular(hrp, &data);
let mut full = data.clone();
full.extend_from_slice(&checksum);
let r = bch_correct_regular(hrp, &full).unwrap();
assert_eq!(r.corrections_applied, 0);
assert!(r.corrected_positions.is_empty());
assert_eq!(r.data, full);
}
#[test]
fn bch_correct_regular_one_error() {
let hrp = "mk";
let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let checksum = bch_create_checksum_regular(hrp, &data);
let mut full = data.clone();
full.extend_from_slice(&checksum);
let original = full.clone();
full[3] = (full[3] + 1) & 0x1F;
let r = bch_correct_regular(hrp, &full).unwrap();
assert_eq!(r.corrections_applied, 1);
assert_eq!(r.corrected_positions, vec![3]);
assert_eq!(r.data, original);
}
#[test]
fn bch_correct_regular_two_errors_recovered_v0_2() {
let hrp = "mk";
let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let checksum = bch_create_checksum_regular(hrp, &data);
let mut full = data.clone();
full.extend_from_slice(&checksum);
let original = full.clone();
full[3] = (full[3] + 1) & 0x1F;
full[7] = (full[7] + 1) & 0x1F;
let r = bch_correct_regular(hrp, &full).unwrap();
assert_eq!(r.corrections_applied, 2);
assert!(r.corrected_positions.contains(&3));
assert!(r.corrected_positions.contains(&7));
assert_eq!(r.data, original);
}
#[test]
fn bch_correct_long_clean_input() {
let hrp = "mk";
let data: Vec<u8> = (0..16).collect();
let checksum = bch_create_checksum_long(hrp, &data);
let mut full = data.clone();
full.extend_from_slice(&checksum);
let r = bch_correct_long(hrp, &full).unwrap();
assert_eq!(r.corrections_applied, 0);
}
#[test]
fn bch_correct_long_one_error() {
let hrp = "mk";
let data: Vec<u8> = (0..16).collect();
let checksum = bch_create_checksum_long(hrp, &data);
let mut full = data.clone();
full.extend_from_slice(&checksum);
let original = full.clone();
full[5] = (full[5] + 1) & 0x1F;
let r = bch_correct_long(hrp, &full).unwrap();
assert_eq!(r.corrections_applied, 1);
assert_eq!(r.corrected_positions, vec![5]);
assert_eq!(r.data, original);
}
#[test]
fn bch_correct_returns_correction_result_with_position() {
let hrp = "mk";
let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7];
let checksum = bch_create_checksum_regular(hrp, &data);
let mut full = data.clone();
full.extend_from_slice(&checksum);
full[9] = (full[9] + 7) & 0x1F;
let r = bch_correct_regular(hrp, &full).unwrap();
assert_eq!(r.corrected_positions, vec![9]);
}
fn build_5bit_data(header_symbols: &[u8], payload_bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(header_symbols.len() + payload_bytes.len() * 2);
out.extend_from_slice(header_symbols);
out.extend(bytes_to_5bit(payload_bytes));
out
}
#[test]
fn encode_5bit_to_string_round_trip_regular() {
let header_symbols = [0u8, 0u8];
let payload = vec![0xDE, 0xAD, 0xBE, 0xEF];
let data_5bit = build_5bit_data(&header_symbols, &payload);
let s = encode_5bit_to_string(&data_5bit).unwrap();
assert!(s.starts_with("mk1"), "string did not start with mk1: {}", s);
let decoded = decode_string(&s).unwrap();
assert_eq!(decoded.code, BchCode::Regular);
assert_eq!(decoded.corrections_applied, 0);
assert!(decoded.corrected_positions.is_empty());
assert_eq!(decoded.data(), data_5bit.as_slice());
let payload_5bit = &decoded.data()[2..];
let recovered = five_bit_to_bytes(payload_5bit).unwrap();
assert_eq!(recovered, payload);
}
#[test]
fn encode_5bit_to_string_round_trip_long() {
let header_symbols = [0u8; 8];
let payload = vec![0xA5u8; 53];
let data_5bit = build_5bit_data(&header_symbols, &payload);
assert_eq!(
data_5bit.len(),
93,
"fixture invariant: 8 header + 85 payload symbols"
);
let s = encode_5bit_to_string(&data_5bit).unwrap();
assert!(s.starts_with("mk1"));
let decoded = decode_string(&s).unwrap();
assert_eq!(decoded.code, BchCode::Long);
assert_eq!(decoded.data(), data_5bit.as_slice());
let recovered = five_bit_to_bytes(&decoded.data()[8..]).unwrap();
assert_eq!(recovered, payload);
}
#[test]
fn encode_starts_with_hrp_and_separator() {
let s = encode_5bit_to_string(&[1u8]).unwrap();
assert!(s.starts_with("mk1"), "string did not start with mk1: {}", s);
}
#[test]
fn decode_rejects_invalid_hrp() {
let s = encode_5bit_to_string(&[0u8; 10]).unwrap();
let bad = s.replacen("mk", "bt", 1);
assert!(matches!(
decode_string(&bad),
Err(crate::Error::InvalidHrp(_))
));
}
#[test]
fn decode_rejects_mixed_case() {
let s = encode_5bit_to_string(&[0u8; 10]).unwrap();
let bad: String = s
.chars()
.enumerate()
.map(|(i, c)| if i == 5 { c.to_ascii_uppercase() } else { c })
.collect();
assert!(matches!(decode_string(&bad), Err(crate::Error::MixedCase)));
}
#[test]
fn decode_rejects_invalid_char() {
let s = encode_5bit_to_string(&[0u8; 10]).unwrap();
let mut chars: Vec<char> = s.chars().collect();
chars[5] = 'b';
let bad: String = chars.into_iter().collect();
assert!(matches!(
decode_string(&bad),
Err(crate::Error::InvalidChar { .. })
));
}
#[test]
fn decode_rejects_missing_separator() {
let bad = "mknoseparatorhere";
assert!(matches!(
decode_string(bad),
Err(crate::Error::InvalidHrp(_))
));
}
#[test]
fn decode_recovers_one_error() {
let data_5bit = vec![0u8, 0u8, 1, 2, 3, 4, 5];
let s = encode_5bit_to_string(&data_5bit).unwrap();
let mut chars: Vec<char> = s.chars().collect();
let original_char = chars[6];
chars[6] = if original_char == 'q' { 'p' } else { 'q' };
let corrupted: String = chars.into_iter().collect();
let decoded = decode_string(&corrupted).unwrap();
assert_eq!(decoded.corrections_applied, 1);
assert_eq!(decoded.corrected_positions.len(), 1);
assert_eq!(decoded.data(), data_5bit.as_slice());
}
#[test]
fn encode_rejects_data_part_in_reserved_invalid_length_range() {
let too_long = vec![0u8; 94];
let result = encode_5bit_to_string(&too_long);
assert!(matches!(result, Err(crate::Error::InvalidStringLength(_))));
}
}