Skip to main content

codec/
error.rs

1//! Error types for codec operations.
2
3use std::fmt;
4
5use schema::{ComponentId, FieldId};
6
7/// Result type for codec operations.
8pub type CodecResult<T> = Result<T, CodecError>;
9
10/// Errors that can occur during snapshot/delta encoding/decoding.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum CodecError {
13    /// Wire format error.
14    Wire(wire::DecodeError),
15
16    /// Bitstream error.
17    Bitstream(bitstream::BitError),
18
19    /// Output buffer is too small.
20    OutputTooSmall { needed: usize, available: usize },
21
22    /// Schema hash mismatch.
23    SchemaMismatch { expected: u64, found: u64 },
24
25    /// Limits exceeded.
26    LimitsExceeded {
27        kind: LimitKind,
28        limit: usize,
29        actual: usize,
30    },
31
32    /// Invalid mask data.
33    InvalidMask { kind: MaskKind, reason: MaskReason },
34
35    /// Invalid field value for the schema.
36    InvalidValue {
37        component: ComponentId,
38        field: FieldId,
39        reason: ValueReason,
40    },
41
42    /// Entities are not provided in deterministic order.
43    InvalidEntityOrder { previous: u32, current: u32 },
44
45    /// Section body had trailing bits after parsing.
46    TrailingSectionData {
47        section: wire::SectionTag,
48        remaining_bits: usize,
49    },
50
51    /// Unexpected section for the current packet type.
52    UnexpectedSection { section: wire::SectionTag },
53
54    /// Duplicate section encountered.
55    DuplicateSection { section: wire::SectionTag },
56
57    /// Baseline tick does not match the packet.
58    BaselineTickMismatch { expected: u32, found: u32 },
59
60    /// Baseline tick not found in history.
61    BaselineNotFound {
62        /// The requested baseline tick.
63        requested_tick: u32,
64    },
65
66    /// Entity not found when applying delta.
67    EntityNotFound {
68        /// The missing entity ID.
69        entity_id: u32,
70    },
71
72    /// Component not found when applying delta.
73    ComponentNotFound {
74        /// The entity ID.
75        entity_id: u32,
76        /// The missing component ID.
77        component_id: u16,
78    },
79
80    /// Duplicate entity in create section.
81    DuplicateEntity {
82        /// The duplicate entity ID.
83        entity_id: u32,
84    },
85
86    /// Entity already exists when creating.
87    EntityAlreadyExists {
88        /// The existing entity ID.
89        entity_id: u32,
90    },
91}
92
93/// Specific limit that was exceeded.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum LimitKind {
96    EntitiesCreate,
97    EntitiesUpdate,
98    EntitiesDestroy,
99    TotalEntitiesAfterApply,
100    ComponentsPerEntity,
101    FieldsPerComponent,
102    SectionBytes,
103}
104
105/// Mask validation error kinds.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum MaskKind {
108    ComponentMask,
109    FieldMask { component: ComponentId },
110}
111
112/// Details for invalid mask errors.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum MaskReason {
115    NotEnoughBits { expected: usize, available: usize },
116    FieldCountMismatch { expected: usize, actual: usize },
117    MissingField { field: FieldId },
118    UnknownComponent { component: ComponentId },
119    ComponentPresenceMismatch { component: ComponentId },
120    EmptyFieldMask { component: ComponentId },
121}
122
123/// Details for invalid value errors.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum ValueReason {
126    UnsignedOutOfRange {
127        bits: u8,
128        value: u64,
129    },
130    SignedOutOfRange {
131        bits: u8,
132        value: i64,
133    },
134    VarUIntOutOfRange {
135        value: u64,
136    },
137    VarSIntOutOfRange {
138        value: i64,
139    },
140    FixedPointOutOfRange {
141        min_q: i64,
142        max_q: i64,
143        value: i64,
144    },
145    TypeMismatch {
146        expected: &'static str,
147        found: &'static str,
148    },
149}
150
151impl fmt::Display for CodecError {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match self {
154            Self::Wire(e) => write!(f, "wire error: {e}"),
155            Self::Bitstream(e) => write!(f, "bitstream error: {e}"),
156            Self::OutputTooSmall { needed, available } => {
157                write!(f, "output too small: need {needed}, have {available}")
158            }
159            Self::SchemaMismatch { expected, found } => {
160                write!(
161                    f,
162                    "schema hash mismatch: expected 0x{expected:016X}, found 0x{found:016X}"
163                )
164            }
165            Self::LimitsExceeded {
166                kind,
167                limit,
168                actual,
169            } => {
170                write!(f, "{kind} limit exceeded: {actual} > {limit}")
171            }
172            Self::InvalidMask { kind, reason } => {
173                write!(f, "invalid {kind}: {reason}")
174            }
175            Self::InvalidValue {
176                component,
177                field,
178                reason,
179            } => {
180                write!(f, "invalid value for {component:?}:{field:?}: {reason}")
181            }
182            Self::InvalidEntityOrder { previous, current } => {
183                write!(f, "entity order invalid: {previous} then {current}")
184            }
185            Self::TrailingSectionData {
186                section,
187                remaining_bits,
188            } => {
189                write!(
190                    f,
191                    "trailing data in section {section:?}: {remaining_bits} bits"
192                )
193            }
194            Self::UnexpectedSection { section } => {
195                write!(f, "unexpected section {section:?} in full snapshot")
196            }
197            Self::DuplicateSection { section } => {
198                write!(f, "duplicate section {section:?} in packet")
199            }
200            Self::BaselineTickMismatch { expected, found } => {
201                write!(
202                    f,
203                    "baseline tick mismatch: expected {expected}, found {found}"
204                )
205            }
206            Self::BaselineNotFound { requested_tick } => {
207                write!(f, "baseline tick {requested_tick} not found in history")
208            }
209            Self::EntityNotFound { entity_id } => {
210                write!(f, "entity {entity_id} not found")
211            }
212            Self::ComponentNotFound {
213                entity_id,
214                component_id,
215            } => {
216                write!(
217                    f,
218                    "component {component_id} not found on entity {entity_id}"
219                )
220            }
221            Self::DuplicateEntity { entity_id } => {
222                write!(f, "duplicate entity {entity_id} in create section")
223            }
224            Self::EntityAlreadyExists { entity_id } => {
225                write!(f, "entity {entity_id} already exists")
226            }
227        }
228    }
229}
230
231impl fmt::Display for LimitKind {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        let name = match self {
234            Self::EntitiesCreate => "entities",
235            Self::EntitiesUpdate => "update entities",
236            Self::EntitiesDestroy => "destroy entities",
237            Self::TotalEntitiesAfterApply => "total entities",
238            Self::ComponentsPerEntity => "components per entity",
239            Self::FieldsPerComponent => "fields per component",
240            Self::SectionBytes => "section bytes",
241        };
242        write!(f, "{name}")
243    }
244}
245
246impl fmt::Display for MaskKind {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match self {
249            Self::ComponentMask => write!(f, "component mask"),
250            Self::FieldMask { component } => write!(f, "field mask for {component:?}"),
251        }
252    }
253}
254
255impl fmt::Display for MaskReason {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            Self::NotEnoughBits {
259                expected,
260                available,
261            } => {
262                write!(f, "need {expected} bits, have {available}")
263            }
264            Self::FieldCountMismatch { expected, actual } => {
265                write!(f, "expected {expected} fields, got {actual}")
266            }
267            Self::MissingField { field } => {
268                write!(f, "missing field {field:?} in full snapshot")
269            }
270            Self::UnknownComponent { component } => {
271                write!(f, "unknown component {component:?} in snapshot")
272            }
273            Self::ComponentPresenceMismatch { component } => {
274                write!(f, "component presence mismatch for {component:?}")
275            }
276            Self::EmptyFieldMask { component } => {
277                write!(f, "empty field mask for {component:?} is invalid")
278            }
279        }
280    }
281}
282
283impl fmt::Display for ValueReason {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        match self {
286            Self::UnsignedOutOfRange { bits, value } => {
287                write!(f, "unsigned value {value} does not fit in {bits} bits")
288            }
289            Self::SignedOutOfRange { bits, value } => {
290                write!(f, "signed value {value} does not fit in {bits} bits")
291            }
292            Self::VarUIntOutOfRange { value } => {
293                write!(f, "varuint value {value} exceeds u32::MAX")
294            }
295            Self::VarSIntOutOfRange { value } => {
296                write!(f, "varsint value {value} exceeds i32 range")
297            }
298            Self::FixedPointOutOfRange {
299                min_q,
300                max_q,
301                value,
302            } => {
303                write!(f, "fixed-point value {value} outside [{min_q}, {max_q}]")
304            }
305            Self::TypeMismatch { expected, found } => {
306                write!(f, "expected {expected} but got {found}")
307            }
308        }
309    }
310}
311
312impl std::error::Error for CodecError {
313    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
314        match self {
315            Self::Wire(e) => Some(e),
316            Self::Bitstream(e) => Some(e),
317            _ => None,
318        }
319    }
320}
321
322impl From<wire::DecodeError> for CodecError {
323    fn from(err: wire::DecodeError) -> Self {
324        Self::Wire(err)
325    }
326}
327
328impl From<bitstream::BitError> for CodecError {
329    fn from(err: bitstream::BitError) -> Self {
330        Self::Bitstream(err)
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn error_display_baseline_not_found() {
340        let err = CodecError::BaselineNotFound { requested_tick: 42 };
341        let msg = err.to_string();
342        assert!(msg.contains("42"), "should mention tick");
343        assert!(msg.contains("baseline"), "should mention baseline");
344    }
345
346    #[test]
347    fn error_display_entity_not_found() {
348        let err = CodecError::EntityNotFound { entity_id: 123 };
349        let msg = err.to_string();
350        assert!(msg.contains("123"), "should mention entity id");
351    }
352
353    #[test]
354    fn error_display_component_not_found() {
355        let err = CodecError::ComponentNotFound {
356            entity_id: 10,
357            component_id: 5,
358        };
359        let msg = err.to_string();
360        assert!(msg.contains("10"), "should mention entity id");
361        assert!(msg.contains('5'), "should mention component id");
362    }
363
364    #[test]
365    fn error_display_duplicate_entity() {
366        let err = CodecError::DuplicateEntity { entity_id: 42 };
367        let msg = err.to_string();
368        assert!(msg.contains("42"), "should mention entity id");
369        assert!(msg.contains("duplicate"), "should mention duplicate");
370    }
371
372    #[test]
373    fn error_display_entity_already_exists() {
374        let err = CodecError::EntityAlreadyExists { entity_id: 99 };
375        let msg = err.to_string();
376        assert!(msg.contains("99"), "should mention entity id");
377        assert!(msg.contains("exists"), "should mention exists");
378    }
379
380    #[test]
381    fn error_from_wire_error() {
382        let wire_err = wire::DecodeError::InvalidMagic { found: 0x1234 };
383        let codec_err: CodecError = wire_err.into();
384        assert!(matches!(codec_err, CodecError::Wire(_)));
385    }
386
387    #[test]
388    fn error_from_bitstream_error() {
389        let bit_err = bitstream::BitError::UnexpectedEof {
390            requested: 1,
391            available: 0,
392        };
393        let codec_err: CodecError = bit_err.into();
394        assert!(matches!(codec_err, CodecError::Bitstream(_)));
395    }
396
397    #[test]
398    fn error_source_wire() {
399        let wire_err = wire::DecodeError::InvalidMagic { found: 0x1234 };
400        let codec_err = CodecError::Wire(wire_err);
401        let source = std::error::Error::source(&codec_err);
402        assert!(source.is_some(), "should have a source");
403    }
404
405    #[test]
406    fn error_source_none_for_others() {
407        let err = CodecError::EntityNotFound { entity_id: 1 };
408        let source = std::error::Error::source(&err);
409        assert!(source.is_none(), "non-wrapped errors should have no source");
410    }
411
412    #[test]
413    fn error_equality() {
414        let err1 = CodecError::EntityNotFound { entity_id: 42 };
415        let err2 = CodecError::EntityNotFound { entity_id: 42 };
416        let err3 = CodecError::EntityNotFound { entity_id: 43 };
417
418        assert_eq!(err1, err2);
419        assert_ne!(err1, err3);
420    }
421
422    #[test]
423    fn error_is_std_error() {
424        fn assert_error<E: std::error::Error>() {}
425        assert_error::<CodecError>();
426    }
427}