Skip to main content

ms_codec/
decode.rs

1//! Public decoder. Applies SPEC §4 validity rules in order.
2
3use crate::consts::{RESERVED_NOT_EMITTED_V01, TAG_ENTR, VALID_STR_LENGTHS};
4use crate::envelope;
5use crate::error::{Error, Result};
6use crate::payload::Payload;
7use crate::tag::Tag;
8use codex32::Codex32String;
9
10/// Decode a v0.1 ms1 string into `(Tag, Payload)`.
11///
12/// Rejects per SPEC §4 rules 1-10:
13///
14/// - Rule 1: upstream codex32 parse failure (Codex32 variant).
15/// - Rules 2-4, 8: wire-invariant violations (delegated to envelope::discriminate).
16/// - Rules 5-7: tag-table membership rules (here).
17/// - Rule 9: total string length not in v0.1-emittable set (here, before parse).
18/// - Rule 10: payload byte length mismatch for the tag (here, via Payload::validate()).
19pub fn decode(s: &str) -> Result<(Tag, Payload)> {
20    // §4 rule 9: total string length must be in the v0.1 set.
21    if !VALID_STR_LENGTHS.contains(&s.len()) {
22        return Err(Error::UnexpectedStringLength {
23            got: s.len(),
24            allowed: VALID_STR_LENGTHS,
25        });
26    }
27
28    // §4 rule 1: delegate parse + checksum to rust-codex32. `?` leverages the
29    // From<codex32::Error> for Error impl in error.rs.
30    let c = Codex32String::from_string(s.to_string())?;
31
32    // §4 rules 2, 3, 4, 8 + tag-alphabet rule 5: envelope.
33    let (tag, payload_bytes) = envelope::discriminate(&c)?;
34
35    // §4 rule 7: reserved-not-emitted tags.
36    if RESERVED_NOT_EMITTED_V01.contains(tag.as_bytes()) {
37        return Err(Error::ReservedTagNotEmittedInV01 {
38            got: *tag.as_bytes(),
39        });
40    }
41
42    // §4 rule 6: tag must be in the v0.1 accept set (currently {entr}).
43    // SPEC v0.9.0 §1 item 2 — wrap the OWNED entropy buffer in `Zeroizing`
44    // so the intermediate scrub runs on function exit. `Payload::Entr(Vec<u8>)`
45    // is the public return shape (unwrapped per SPEC §3 OOS-2); the
46    // caller wraps before storing — see `payload.rs` doc-comment.
47    use zeroize::Zeroizing;
48    let payload = match *tag.as_bytes() {
49        x if x == TAG_ENTR => {
50            let scrubbed: Zeroizing<Vec<u8>> = Zeroizing::new(payload_bytes);
51            let p = Payload::Entr((*scrubbed).clone());
52            // §4 rule 10: validate payload length against the tag's expected set.
53            p.validate()?;
54            p
55        }
56        _ => {
57            return Err(Error::UnknownTag {
58                got: *tag.as_bytes(),
59            });
60        }
61    };
62
63    Ok((tag, payload))
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::encode;
70
71    #[test]
72    fn round_trip_entr_all_lengths() {
73        for len in [16usize, 20, 24, 28, 32] {
74            let entropy = (0..len as u8)
75                .map(|i| i.wrapping_mul(7))
76                .collect::<Vec<_>>();
77            let p = Payload::Entr(entropy.clone());
78            let s = encode::encode(Tag::ENTR, &p).unwrap();
79            let (tag, recovered) = decode(&s).unwrap();
80            assert_eq!(tag, Tag::ENTR);
81            assert_eq!(recovered, p);
82        }
83    }
84
85    #[test]
86    fn decode_rejects_unexpected_length() {
87        // 51 chars is not a v0.1 emittable length.
88        let s = "ms10entrsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
89        assert!(matches!(
90            decode(s),
91            Err(Error::UnexpectedStringLength { .. })
92        ));
93    }
94
95    #[test]
96    fn decode_rejects_short_seed_string_with_reserved_tag() {
97        // Hand-build a 50-char string with id="seed" — 16-B entropy worth.
98        // The string-length check passes; tag-rule 7 fails.
99        let mut data = vec![0x00u8];
100        data.extend_from_slice(&[0xAAu8; 16]);
101        let c = Codex32String::from_seed("ms", 0, "seed", codex32::Fe::S, &data).unwrap();
102        let s = c.to_string();
103        assert_eq!(s.len(), 50, "expected str.len 50 for 16-B + prefix");
104        assert!(matches!(
105            decode(&s),
106            Err(Error::ReservedTagNotEmittedInV01 { .. })
107        ));
108    }
109}