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}