Skip to main content

ms_codec/
encode.rs

1//! Public encoder. v0.1 entr-only; future kinds in v0.2+ via the envelope seam.
2
3use crate::consts::RESERVED_NOT_EMITTED_V01;
4use crate::envelope;
5use crate::error::{Error, Result};
6use crate::payload::Payload;
7use crate::tag::Tag;
8
9/// Encode a `(Tag, Payload)` as a v0.1 ms1 string.
10///
11/// Per SPEC §3.5 + §3.5.1:
12/// - Encoder validates `Payload` length first (rejects out-of-set entr lengths).
13/// - Encoder rejects reserved-not-emitted tags symmetrically with the decoder
14///   (SPEC §4 rule 7), preventing a v0.1 ms-codec from emitting a string that
15///   v0.1 ms-codec itself cannot decode.
16pub fn encode(tag: Tag, payload: &Payload) -> Result<String> {
17    // §3.5.1: encoder symmetry on reserved-not-emitted tags.
18    if RESERVED_NOT_EMITTED_V01.contains(tag.as_bytes()) {
19        return Err(Error::ReservedTagNotEmittedInV01 {
20            got: *tag.as_bytes(),
21        });
22    }
23    // §3.5: payload length validation.
24    payload.validate()?;
25    // Hand off to envelope.
26    let c = envelope::package(tag, payload.as_bytes())?;
27    Ok(c.to_string())
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use crate::consts::VALID_STR_LENGTHS;
34
35    #[test]
36    fn encode_entr_all_lengths_succeed() {
37        for (i, len) in [16usize, 20, 24, 28, 32].iter().enumerate() {
38            let p = Payload::Entr(vec![0xAAu8; *len]);
39            let s = encode(Tag::ENTR, &p).unwrap();
40            assert_eq!(s.len(), VALID_STR_LENGTHS[i]);
41            assert!(s.starts_with("ms10entrs"), "got {}", s);
42        }
43    }
44
45    #[test]
46    fn encode_rejects_seed_tag() {
47        let p = Payload::Entr(vec![0u8; 16]);
48        let seed_tag = Tag::try_new("seed").unwrap();
49        assert!(matches!(
50            encode(seed_tag, &p),
51            Err(Error::ReservedTagNotEmittedInV01 { .. })
52        ));
53    }
54
55    #[test]
56    fn encode_rejects_xprv_tag() {
57        let p = Payload::Entr(vec![0u8; 16]);
58        let xprv_tag = Tag::try_new("xprv").unwrap();
59        assert!(matches!(
60            encode(xprv_tag, &p),
61            Err(Error::ReservedTagNotEmittedInV01 { .. })
62        ));
63    }
64
65    #[test]
66    fn encode_rejects_off_by_one_entr_length() {
67        let p = Payload::Entr(vec![0u8; 17]);
68        assert!(matches!(
69            encode(Tag::ENTR, &p),
70            Err(Error::PayloadLengthMismatch { .. })
71        ));
72    }
73}