Skip to main content

ms_codec/
error.rs

1//! ms-codec error taxonomy. Variants mirror SPEC §4 decoder validity rules
2//! plus the encoder-side validation surface from SPEC §3.5 / §3.5.1.
3
4use std::fmt;
5
6/// ms-codec error type.
7#[derive(Debug)]
8#[non_exhaustive]
9pub enum Error {
10    /// Upstream codex32 parse / checksum failure (delegated from rust-codex32).
11    Codex32(codex32::Error),
12    /// Mnem wordlist-language byte was not in the valid range 0..=9 (SPEC v0.2 §3).
13    MnemUnknownLanguage(u8),
14    /// HRP was not "ms" (SPEC §4 rule 2).
15    WrongHrp {
16        /// The HRP that was observed.
17        got: String,
18    },
19    /// Threshold was not 0 (SPEC §4 rule 3).
20    ThresholdNotZero {
21        /// The threshold-position byte (ASCII digit) that was observed.
22        got: u8,
23    },
24    /// Share-index was not 's' — BIP-93 requires 's' for threshold=0 (SPEC §4 rule 4).
25    ShareIndexNotSecret {
26        /// The share-index character that was observed.
27        got: char,
28    },
29    /// Tag bytes were not in the codex32 alphabet (SPEC §4 rule 5).
30    TagInvalidAlphabet {
31        /// The 4-byte id-field bytes that failed alphabet validation.
32        got: [u8; 4],
33    },
34    /// Tag was structurally valid but not in RESERVED_TAG_TABLE (SPEC §4 rule 6).
35    UnknownTag {
36        /// The 4-byte tag that was not recognized.
37        got: [u8; 4],
38    },
39    /// Tag was in RESERVED_TAG_TABLE but reserved-not-emitted in v0.1 (SPEC §4 rule 7,
40    /// SPEC §3.5.1 encoder symmetry).
41    ReservedTagNotEmittedInV01 {
42        /// The 4-byte reserved tag (one of seed/xprv/mnem/prvk in v0.1).
43        got: [u8; 4],
44    },
45    /// Reserved-prefix byte was not 0x00 (SPEC §4 rule 8).
46    ReservedPrefixViolation {
47        /// The non-zero prefix byte that was observed.
48        got: u8,
49    },
50    /// Total string length was outside the v0.1 emittable set (SPEC §4 rule 9).
51    UnexpectedStringLength {
52        /// The total string length that was observed.
53        got: usize,
54        /// The set of v0.1-emittable lengths.
55        allowed: &'static [usize],
56    },
57    /// Payload byte length did not match the tag's spec (SPEC §3.5, §4 rule 10).
58    PayloadLengthMismatch {
59        /// The 4-byte tag whose length set was checked against.
60        tag: [u8; 4],
61        /// The set of valid byte lengths for this tag.
62        expected: &'static [usize],
63        /// The observed payload byte length (after stripping the prefix byte).
64        got: usize,
65    },
66    /// BCH error-correction (`bch_decode`) reported the input is uncorrectable
67    /// — the number of symbol errors exceeds the regular code's `t = 4`
68    /// correction capacity (singleton bound `d = 8`). Surfaced by
69    /// [`crate::decode_with_correction`] when `bch_decode::decode_regular_errors`
70    /// returns `None`, or when a post-correction re-verification step fails
71    /// (catches pathological 5+-error patterns that fool the decoder into
72    /// producing a "consistent" but invalid locator). Added v0.2.0 per plan
73    /// §1 D29 + §2.B.2.
74    ///
75    /// `bound = 8` is the BCH(93,80,8) singleton bound. ms1 is single-chunk
76    /// only — no `chunk_index` field (cf. md-codec's `TooManyErrors` which
77    /// carries chunk-set context).
78    TooManyErrors {
79        /// Singleton bound for the BCH regular code (always 8).
80        bound: u8,
81    },
82
83    // --- v0.2 K-of-N share variants (SPEC_ms_v0_2_kofn §2) ---
84    //
85    // Inserted alphabetically AMONG THEMSELVES (the pre-existing v0.1 variants
86    // above are NOT retro-sorted — mirrors the toolkit's
87    // `error-rs-retroactive-alphabetical-sort` deferral). These carry `Display`
88    // arms only: `ms_codec::Error` has no `exit_code`/`kind` methods — the
89    // exit-code/message mapping is ms-cli's `CliError` job.
90    /// Share count `n` was outside the valid range for threshold `k` (need
91    /// `k <= n <= 31`; there are exactly 31 valid non-`s` share indices).
92    InvalidShareCount {
93        /// The threshold `k` that was requested.
94        k: u8,
95        /// The share count `n` that was requested (out of range).
96        n: usize,
97    },
98    /// Threshold `k` was not in the valid share range `2..=9`
99    /// (`Threshold::ZERO` is the unshared single-string sentinel, a const).
100    InvalidThreshold(u8),
101    /// A single-string `decode` was handed one share of a K-of-N share-set
102    /// (threshold char `2..9`). Use `ms combine` to recombine K shares.
103    IsShareNotSingleString {
104        /// The threshold char observed on the wire (`'2'..'9'`).
105        threshold: char,
106        /// The share-index char observed on the wire.
107        index: char,
108    },
109    /// `combine_shares` was handed the secret-at-S (index `s`) as an input.
110    /// The secret-at-S is the recovery target, never a combine input; codex32's
111    /// `interpolate_at` would short-circuit on it and bypass validation (C1).
112    SecretShareSuppliedToCombine,
113}
114
115impl fmt::Display for Error {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        match self {
118            Error::Codex32(e) => write!(f, "codex32 parse error: {:?}", e),
119            Error::MnemUnknownLanguage(code) => {
120                write!(f, "unknown mnem wordlist-language code: {0}", code)
121            }
122            Error::WrongHrp { got } => write!(f, "wrong HRP: got {:?}, expected \"ms\"", got),
123            Error::ThresholdNotZero { got } => {
124                write!(
125                    f,
126                    "threshold not 0 (got '{}'); v0.1 is single-string only",
127                    *got as char
128                )
129            }
130            Error::ShareIndexNotSecret { got } => {
131                write!(
132                    f,
133                    "share-index not 's' (got '{}'); BIP-93 requires 's' for threshold=0",
134                    got
135                )
136            }
137            Error::TagInvalidAlphabet { got } => {
138                write!(f, "tag bytes not in codex32 alphabet: {:?}", got)
139            }
140            Error::UnknownTag { got } => write!(
141                f,
142                "unknown tag {:?}; not a member of RESERVED_TAG_TABLE",
143                std::str::from_utf8(got).unwrap_or("<non-utf8>")
144            ),
145            Error::ReservedTagNotEmittedInV01 { got } => write!(
146                f,
147                "tag {:?} reserved-not-emitted in v0.1; deferred to v0.2+",
148                std::str::from_utf8(got).unwrap_or("<non-utf8>")
149            ),
150            Error::ReservedPrefixViolation { got } => {
151                write!(f, "reserved-prefix byte was 0x{:02x}, expected 0x00", got)
152            }
153            Error::UnexpectedStringLength { got, allowed } => {
154                write!(f, "string length {} outside v0.1 set {:?}", got, allowed)
155            }
156            Error::PayloadLengthMismatch { tag, expected, got } => write!(
157                f,
158                "tag {:?} payload length {} not in expected set {:?}",
159                std::str::from_utf8(tag).unwrap_or("<non-utf8>"),
160                got,
161                expected
162            ),
163            Error::TooManyErrors { bound } => {
164                write!(f, "more than {} errors; uncorrectable", bound)
165            }
166            Error::InvalidShareCount { k, n } => write!(
167                f,
168                "invalid share count n={} for threshold k={}; require k <= n <= 31",
169                n, k
170            ),
171            Error::InvalidThreshold(k) => write!(
172                f,
173                "invalid threshold {}; K-of-N shares require k in 2..=9",
174                k
175            ),
176            Error::IsShareNotSingleString { threshold, index } => write!(
177                f,
178                "this is one share of a K-of-N set (threshold '{}', index '{}'); \
179                 use `ms combine` to recombine K shares",
180                threshold, index
181            ),
182            Error::SecretShareSuppliedToCombine => write!(
183                f,
184                "the secret share (index 's') cannot be supplied to combine; \
185                 supply only distributed shares (the secret is the recovery target)"
186            ),
187        }
188    }
189}
190
191impl std::error::Error for Error {
192    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
193        // codex32::Error doesn't impl std::error::Error in v0.1.0; chain stops here.
194        None
195    }
196}
197
198impl From<codex32::Error> for Error {
199    fn from(e: codex32::Error) -> Self {
200        Error::Codex32(e)
201    }
202}
203
204/// Result alias for ms-codec.
205pub type Result<T> = std::result::Result<T, Error>;