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    // SPEC_ms_hashlock §1 rule 2, emit side: never mint a single whose tag
24    // names a different kind than its prefix byte -- decode would refuse it.
25    // Placed AFTER the reserved-not-emitted check so `seed`/`xprv` keep the
26    // v0.1 SPEC §4 rule 7 error they have always returned.
27    if tag != payload.kind().single_tag() {
28        return Err(Error::TagKindMismatch {
29            tag: *tag.as_bytes(),
30            prefix: crate::envelope::prefix_of(payload),
31        });
32    }
33    // §3.5: payload length validation.
34    payload.validate()?;
35    // Hand off to envelope.
36    let c = envelope::package(tag, payload)?;
37    Ok(c.to_string())
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43    use crate::consts::VALID_STR_LENGTHS;
44
45    #[test]
46    fn encode_entr_all_lengths_succeed() {
47        for (i, len) in [16usize, 20, 24, 28, 32].iter().enumerate() {
48            let p = Payload::Entr(vec![0xAAu8; *len]);
49            let s = encode(Tag::ENTR, &p).unwrap();
50            assert_eq!(s.len(), VALID_STR_LENGTHS[i]);
51            assert!(s.starts_with("ms10entrs"), "got {}", s);
52        }
53    }
54
55    #[test]
56    fn encode_rejects_seed_tag() {
57        let p = Payload::Entr(vec![0u8; 16]);
58        let seed_tag = Tag::try_new("seed").unwrap();
59        assert!(matches!(
60            encode(seed_tag, &p),
61            Err(Error::ReservedTagNotEmittedInV01 { .. })
62        ));
63    }
64
65    #[test]
66    fn encode_rejects_xprv_tag() {
67        let p = Payload::Entr(vec![0u8; 16]);
68        let xprv_tag = Tag::try_new("xprv").unwrap();
69        assert!(matches!(
70            encode(xprv_tag, &p),
71            Err(Error::ReservedTagNotEmittedInV01 { .. })
72        ));
73    }
74
75    #[test]
76    fn encode_rejects_off_by_one_entr_length() {
77        let p = Payload::Entr(vec![0u8; 17]);
78        assert!(matches!(
79            encode(Tag::ENTR, &p),
80            Err(Error::PayloadLengthMismatch { .. })
81        ));
82    }
83}