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))
}
#[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 { .. })
));
}
}