Skip to main content

weir_core/
error.rs

1//! The decode failure taxonomy: [`DecodeError`] (one variant per frame-validation
2//! step, which the daemon maps to a Nack reason) and the top-level [`WeirError`].
3
4use std::fmt;
5
6/// All errors that can occur when decoding a weir wire frame.
7///
8/// Each variant identifies a specific frame-validation failure, and the daemon
9/// maps it to a distinct Nack reason byte: `VersionMismatch` is kept separate from
10/// `BadMagic` and `HeaderCrcMismatch` so the daemon can return its `WIRE_VERSION`
11/// in the Nack payload. The variants are NOT declared in strict decode order — the
12/// decoder validates magic → version → header CRC → message_type / durability →
13/// payload fields (so e.g. `UnknownMessageType` only arises on an
14/// already-CRC-valid header). Match on the variant, never on its position.
15///
16/// `#[non_exhaustive]`: the decode taxonomy is expected to grow (wire v1 already
17/// gained `ReservedFlagsSet` and `TrailingBytes` during the 1.0 hardening), so
18/// downstream matches must include a wildcard arm and adding a variant later is
19/// not a breaking change.
20#[derive(Debug, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum DecodeError {
23    /// First four bytes are not `b"WEIR"`. Not a weir frame or stream is desynced.
24    BadMagic,
25    /// Version byte does not equal `WIRE_VERSION`. Carries both sides for error messaging.
26    /// Checked before header CRC so a version-mismatched frame from a newer client gets
27    /// `VersionMismatch`, not a confusing `HeaderCrcMismatch`.
28    VersionMismatch {
29        /// The `WIRE_VERSION` this build supports.
30        supported: u8,
31        /// The version byte carried by the rejected frame.
32        received: u8,
33    },
34    /// Message type byte has no known variant.
35    UnknownMessageType(u8),
36    /// Durability byte has no known variant.
37    UnknownDurability(u8),
38    /// Header CRC32 (bytes [12..16]) does not match CRC of bytes [0..12].
39    HeaderCrcMismatch {
40        /// CRC carried in the header.
41        expected: u32,
42        /// CRC computed over bytes [0..12].
43        computed: u32,
44    },
45    /// Payload CRC32 (trailing 4 bytes) does not match CRC of the payload bytes.
46    PayloadCrcMismatch {
47        /// CRC carried in the trailing 4 bytes.
48        expected: u32,
49        /// CRC computed over the payload bytes.
50        computed: u32,
51    },
52    /// Buffer is shorter than the declared frame length.
53    TruncatedFrame,
54    /// `payload_len` exceeds the hard cap. Rejection happens before any heap allocation.
55    PayloadTooLarge {
56        /// The declared payload length.
57        len: usize,
58        /// The hard cap it exceeded.
59        cap: usize,
60    },
61    /// The reserved `flags` byte was nonzero. In wire v1 `flags` must be zero; a
62    /// daemon rejects a frame that sets any flag bit rather than silently
63    /// ignoring it (F52). Carries the offending byte.
64    ReservedFlagsSet {
65        /// The nonzero `flags` byte the frame carried.
66        flags: u8,
67    },
68    /// The buffer was longer than the single frame it declared. `Envelope::decode`
69    /// requires its input to be exactly one frame (`HEADER_LEN + payload_len + 4`
70    /// bytes): a longer buffer is rejected rather than decoding the first frame and
71    /// silently discarding the remainder, so the caller — not the codec — owns
72    /// framing and an over-long buffer surfaces as an error, not lost data (G18).
73    TrailingBytes {
74        /// The number of bytes past the declared frame length.
75        extra: usize,
76    },
77}
78
79impl fmt::Display for DecodeError {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Self::BadMagic => {
83                write!(f, "bad magic bytes: not a weir frame")
84            }
85            Self::VersionMismatch {
86                supported,
87                received,
88            } => {
89                write!(
90                    f,
91                    "wire version mismatch: daemon supports v{supported}, frame carries v{received}"
92                )
93            }
94            Self::UnknownMessageType(t) => {
95                write!(f, "unknown message type: {t:#04x}")
96            }
97            Self::UnknownDurability(d) => {
98                write!(f, "unknown durability byte: {d:#04x}")
99            }
100            Self::HeaderCrcMismatch { expected, computed } => {
101                write!(
102                    f,
103                    "header CRC mismatch: expected {expected:#010x}, computed {computed:#010x}"
104                )
105            }
106            Self::PayloadCrcMismatch { expected, computed } => {
107                write!(
108                    f,
109                    "payload CRC mismatch: expected {expected:#010x}, computed {computed:#010x}"
110                )
111            }
112            Self::TruncatedFrame => {
113                write!(f, "truncated frame: buffer shorter than declared length")
114            }
115            Self::PayloadTooLarge { len, cap } => {
116                write!(f, "payload length {len} exceeds cap {cap}")
117            }
118            Self::ReservedFlagsSet { flags } => {
119                write!(f, "reserved flags byte must be zero, got {flags:#04x}")
120            }
121            Self::TrailingBytes { extra } => {
122                write!(
123                    f,
124                    "buffer has {extra} byte(s) past the declared frame length"
125                )
126            }
127        }
128    }
129}
130
131impl std::error::Error for DecodeError {}
132
133/// Top-level error type for the weir-core crate.
134///
135/// `#[non_exhaustive]`: more top-level error categories may be added post-1.0
136/// (the crate currently only decodes, but the type is the public error root), so
137/// downstream matches must carry a wildcard arm.
138///
139/// Derives `PartialEq`/`Eq` to match its contained [`DecodeError`], so callers can
140/// assert on a `WeirError` directly (S45). If a future variant ever wraps a
141/// non-`Eq` payload (e.g. `io::Error`), drop these derives and document why.
142#[derive(Debug, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum WeirError {
145    /// A wire frame failed to decode.
146    Decode(DecodeError),
147}
148
149impl fmt::Display for WeirError {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        match self {
152            Self::Decode(e) => write!(f, "decode error: {e}"),
153        }
154    }
155}
156
157impl std::error::Error for WeirError {
158    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
159        match self {
160            Self::Decode(e) => Some(e),
161        }
162    }
163}
164
165impl From<DecodeError> for WeirError {
166    fn from(e: DecodeError) -> Self {
167        Self::Decode(e)
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn decode_error_display_is_human_readable() {
177        let e = DecodeError::VersionMismatch {
178            supported: 1,
179            received: 2,
180        };
181        let s = e.to_string();
182        assert!(
183            s.contains("v1"),
184            "display should contain supported version: {s}"
185        );
186        assert!(
187            s.contains("v2"),
188            "display should contain received version: {s}"
189        );
190    }
191
192    /// Every DecodeError variant renders a non-empty, operator-facing message
193    /// that carries its salient datum — these strings are what a rejected frame
194    /// surfaces, and only the VersionMismatch arm was previously asserted.
195    #[test]
196    fn every_decode_error_variant_displays_its_datum() {
197        let cases: Vec<(DecodeError, &str)> = vec![
198            (DecodeError::BadMagic, "magic"),
199            (
200                DecodeError::VersionMismatch {
201                    supported: 1,
202                    received: 2,
203                },
204                "v2",
205            ),
206            (DecodeError::UnknownMessageType(0xff), "0xff"),
207            (DecodeError::UnknownDurability(0xaa), "0xaa"),
208            (
209                DecodeError::HeaderCrcMismatch {
210                    expected: 1,
211                    computed: 2,
212                },
213                "CRC",
214            ),
215            (
216                DecodeError::PayloadCrcMismatch {
217                    expected: 1,
218                    computed: 2,
219                },
220                "CRC",
221            ),
222            (DecodeError::TruncatedFrame, "truncated"),
223            (DecodeError::PayloadTooLarge { len: 99, cap: 10 }, "99"),
224            (DecodeError::ReservedFlagsSet { flags: 0x01 }, "0x01"),
225            (DecodeError::TrailingBytes { extra: 7 }, "7"),
226        ];
227        for (e, needle) in cases {
228            let s = e.to_string();
229            assert!(!s.is_empty(), "{e:?}: Display must be non-empty");
230            assert!(
231                s.contains(needle),
232                "{e:?}: Display {s:?} must contain {needle:?}"
233            );
234        }
235    }
236
237    /// The three byte-mapping error structs render the offending byte as 0x..
238    #[test]
239    fn unknown_byte_error_structs_render_the_raw_byte() {
240        assert!(crate::UnknownMessageType(0xff).to_string().contains("0xff"));
241        assert!(crate::UnknownDurability(0xaa).to_string().contains("0xaa"));
242        assert!(crate::UnknownNackReason(0x0a).to_string().contains("0x0a"));
243    }
244
245    #[test]
246    fn weir_error_source_chains_to_decode_error() {
247        use std::error::Error;
248        let e = WeirError::from(DecodeError::BadMagic);
249        assert!(e.source().is_some());
250    }
251}