use crate::consts::{RESERVED_NOT_EMITTED_V01, TAG_ENTR, VALID_STR_LENGTHS};
use crate::envelope;
use crate::error::{Error, Result};
use crate::payload::Payload;
use crate::tag::Tag;
use codex32::Codex32String;
pub fn decode(s: &str) -> Result<(Tag, Payload)> {
if !VALID_STR_LENGTHS.contains(&s.len()) {
return Err(Error::UnexpectedStringLength {
got: s.len(),
allowed: VALID_STR_LENGTHS,
});
}
let c = Codex32String::from_string(s.to_string())?;
let (tag, payload_bytes) = envelope::discriminate(&c)?;
if RESERVED_NOT_EMITTED_V01.contains(tag.as_bytes()) {
return Err(Error::ReservedTagNotEmittedInV01 {
got: *tag.as_bytes(),
});
}
use zeroize::Zeroizing;
let payload = match *tag.as_bytes() {
x if x == TAG_ENTR => {
let scrubbed: Zeroizing<Vec<u8>> = Zeroizing::new(payload_bytes);
let p = Payload::Entr((*scrubbed).clone());
p.validate()?;
p
}
_ => {
return Err(Error::UnknownTag {
got: *tag.as_bytes(),
});
}
};
Ok((tag, payload))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CorrectionDetail {
pub position: usize,
pub was: char,
pub now: char,
}
const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
const HRP_PREFIX: &str = "ms1";
fn parse_ms1_symbols(s: &str) -> Result<Vec<u8>> {
let lower = s.to_ascii_lowercase();
if !lower.starts_with(HRP_PREFIX) {
let hrp_end = lower.rfind('1').map(|i| i + 1).unwrap_or(lower.len());
let got = lower[..hrp_end.saturating_sub(1)].to_string();
return Err(Error::WrongHrp { got });
}
let rest = &lower[HRP_PREFIX.len()..];
let mut symbols: Vec<u8> = Vec::with_capacity(rest.len());
for c in rest.chars() {
let lc = c as u8;
let sym = CODEX32_ALPHABET
.iter()
.position(|&b| b == lc)
.ok_or(Error::UnexpectedStringLength {
got: s.len(),
allowed: VALID_STR_LENGTHS,
})? as u8;
symbols.push(sym);
}
Ok(symbols)
}
fn encode_ms1_string(data_with_checksum: &[u8]) -> String {
let mut out = String::with_capacity(HRP_PREFIX.len() + data_with_checksum.len());
out.push_str(HRP_PREFIX);
for &v in data_with_checksum {
out.push(CODEX32_ALPHABET[(v & 0x1F) as usize] as char);
}
out
}
pub fn decode_with_correction(s: &str) -> Result<(Tag, Payload, Vec<CorrectionDetail>)> {
let symbols = parse_ms1_symbols(s)?;
let mut input = crate::bch::hrp_expand("ms");
input.extend_from_slice(&symbols);
let residue = crate::bch::polymod_run(&input) ^ crate::bch::MS_REGULAR_CONST;
if residue == 0 {
let (tag, payload) = decode(s)?;
return Ok((tag, payload, Vec::new()));
}
let (positions, magnitudes) = crate::bch_decode::decode_regular_errors(residue, symbols.len())
.ok_or(Error::TooManyErrors { bound: 8 })?;
let mut corrected = symbols.clone();
let mut details: Vec<CorrectionDetail> = Vec::with_capacity(positions.len());
for (&pos, &mag) in positions.iter().zip(&magnitudes) {
if pos >= corrected.len() {
return Err(Error::TooManyErrors { bound: 8 });
}
let was_byte = corrected[pos];
let now_byte = was_byte ^ mag;
let was = CODEX32_ALPHABET[(was_byte & 0x1F) as usize] as char;
let now = CODEX32_ALPHABET[(now_byte & 0x1F) as usize] as char;
details.push(CorrectionDetail {
position: pos,
was,
now,
});
corrected[pos] = now_byte;
}
let mut verify_input = crate::bch::hrp_expand("ms");
verify_input.extend_from_slice(&corrected);
let verify_residue =
crate::bch::polymod_run(&verify_input) ^ crate::bch::MS_REGULAR_CONST;
if verify_residue != 0 {
return Err(Error::TooManyErrors { bound: 8 });
}
let corrected_str = encode_ms1_string(&corrected);
let (tag, payload) = decode(&corrected_str)?;
Ok((tag, payload, details))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encode;
#[test]
fn round_trip_entr_all_lengths() {
for len in [16usize, 20, 24, 28, 32] {
let entropy = (0..len as u8)
.map(|i| i.wrapping_mul(7))
.collect::<Vec<_>>();
let p = Payload::Entr(entropy.clone());
let s = encode::encode(Tag::ENTR, &p).unwrap();
let (tag, recovered) = decode(&s).unwrap();
assert_eq!(tag, Tag::ENTR);
assert_eq!(recovered, p);
}
}
#[test]
fn decode_rejects_unexpected_length() {
let s = "ms10entrsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
assert!(matches!(
decode(s),
Err(Error::UnexpectedStringLength { .. })
));
}
#[test]
fn decode_rejects_short_seed_string_with_reserved_tag() {
let mut data = vec![0x00u8];
data.extend_from_slice(&[0xAAu8; 16]);
let c = Codex32String::from_seed("ms", 0, "seed", codex32::Fe::S, &data).unwrap();
let s = c.to_string();
assert_eq!(s.len(), 50, "expected str.len 50 for 16-B + prefix");
assert!(matches!(
decode(&s),
Err(Error::ReservedTagNotEmittedInV01 { .. })
));
}
}