Skip to main content

ms_codec/
inspect.rs

1//! Structural inspection of an ms1 string for debugging / future ms-cli.
2
3use crate::envelope;
4use crate::error::Result;
5use crate::tag::Tag;
6use codex32::Codex32String;
7
8/// Structural dump of a parsed ms1 string. `#[non_exhaustive]` per SPEC §10
9/// — v0.2+ may add fields (share-index detail, threshold-layer hints,
10/// derivation metadata).
11#[derive(Debug, Clone)]
12#[non_exhaustive]
13pub struct InspectReport {
14    /// Expected "ms" in v0.1.
15    pub hrp: String,
16    /// Expected 0 in v0.1.
17    pub threshold: u8,
18    /// The parsed type tag (id field).
19    pub tag: Tag,
20    /// Expected 's' in v0.1.
21    pub share_index: char,
22    /// 0x00 in v0.1 (reserved); becomes type discriminator in v0.2+.
23    pub prefix_byte: u8,
24    /// Payload bytes after the prefix byte.
25    pub payload_bytes: Vec<u8>,
26    /// BCH verification result. True if the upstream codex32 parser accepted.
27    pub checksum_valid: bool,
28}
29
30/// Inspect an ms1 string. Less strict than `decode()`: returns a report even
31/// for strings that would fail decoder validity rules (e.g., wrong threshold,
32/// reserved-not-emitted tag, non-zero prefix byte) — caller can examine the
33/// fields to diagnose what's wrong. Still requires a valid BIP-93 parse.
34pub fn inspect(s: &str) -> Result<InspectReport> {
35    // `?` leverages From<codex32::Error> for Error.
36    let c = Codex32String::from_string(s.to_string())?;
37    let s_owned = c.to_string();
38    let fields = envelope::extract_wire_fields(&s_owned)?;
39
40    // For tag construction in inspect we accept whatever bytes were on the wire
41    // (alphabet-valid or not) — surfacing the raw observation is the point.
42    let tag = match std::str::from_utf8(&fields.id_bytes) {
43        Ok(t) => Tag::try_new(t).unwrap_or_else(|_| Tag::from_raw_bytes(fields.id_bytes)),
44        Err(_) => Tag::from_raw_bytes(fields.id_bytes),
45    };
46
47    let payload_with_prefix = c.parts().data();
48    let (prefix_byte, payload_bytes) = if payload_with_prefix.is_empty() {
49        (0u8, Vec::new())
50    } else {
51        (payload_with_prefix[0], payload_with_prefix[1..].to_vec())
52    };
53
54    Ok(InspectReport {
55        hrp: fields.hrp.to_string(),
56        threshold: fields.threshold_byte - b'0', // ASCII to digit
57        tag,
58        share_index: fields.share_index_byte as char,
59        prefix_byte,
60        payload_bytes,
61        checksum_valid: true, // if from_string accepted, BCH was valid
62    })
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::{encode, payload::Payload};
69
70    #[test]
71    fn inspect_v01_entr_returns_expected_fields() {
72        let entropy = vec![0xAAu8; 16];
73        let s = encode::encode(Tag::ENTR, &Payload::Entr(entropy.clone())).unwrap();
74        let r = inspect(&s).unwrap();
75        assert_eq!(r.hrp, "ms");
76        assert_eq!(r.threshold, 0);
77        assert_eq!(r.tag, Tag::ENTR);
78        assert_eq!(r.share_index, 's');
79        assert_eq!(r.prefix_byte, 0x00);
80        assert_eq!(r.payload_bytes, entropy);
81        assert!(r.checksum_valid);
82    }
83
84    #[test]
85    fn inspect_returns_report_for_decoder_rejects() {
86        // A non-zero-prefix string: decode() rejects, inspect() returns the report.
87        let mut data = vec![0x01u8];
88        data.extend_from_slice(&[0xAAu8; 16]);
89        let c = Codex32String::from_seed("ms", 0, "entr", codex32::Fe::S, &data).unwrap();
90        let r = inspect(&c.to_string()).unwrap();
91        assert_eq!(r.prefix_byte, 0x01); // would fail decode rule 8, inspect surfaces it
92    }
93}