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    /// Multiple update encodings present in one packet.
58    DuplicateUpdateEncoding,
59
60    /// Baseline tick does not match the packet.
61    BaselineTickMismatch { expected: u32, found: u32 },
62
63    /// Baseline tick not found in history.
64    BaselineNotFound {
65        /// The requested baseline tick.
66        requested_tick: u32,
67    },
68
69    /// Entity not found when applying delta.
70    EntityNotFound {
71        /// The missing entity ID.
72        entity_id: u32,
73    },
74
75    /// Component not found when applying delta.
76    ComponentNotFound {
77        /// The entity ID.
78        entity_id: u32,
79        /// The missing component ID.
80        component_id: u16,
81    },
82
83    /// Duplicate entity in create section.
84    DuplicateEntity {
85        /// The duplicate entity ID.
86        entity_id: u32,
87    },
88
89    /// Entity already exists when creating.
90    EntityAlreadyExists {
91        /// The existing entity ID.
92        entity_id: u32,
93    },
94
95    /// Missing session state for compact packets.
96    SessionMissing,
97
98    /// Session init packet was invalid.
99    SessionInitInvalid,
100
101    /// Session compact header mode is unsupported.
102    SessionUnsupportedMode { mode: u8 },
103
104    /// Session packets arrived out of order.
105    SessionOutOfOrder { previous: u32, current: u32 },
106}
107
108impl CodecError {
109    /// Returns `true` if the error indicates the client should resync via a
110    /// session init + full snapshot.
111    #[must_use]
112    pub fn needs_resync(&self) -> bool {
113        matches!(
114            self,
115            Self::SessionMissing
116                | Self::SessionInitInvalid
117                | Self::SessionUnsupportedMode { .. }
118                | Self::SessionOutOfOrder { .. }
119                | Self::SchemaMismatch { .. }
120                | Self::BaselineNotFound { .. }
121                | Self::BaselineTickMismatch { .. }
122                | Self::Wire(wire::DecodeError::InvalidBaselineTick { .. })
123                | Self::Wire(wire::DecodeError::InvalidFlags { .. })
124        )
125    }
126}
127
128/// Specific limit that was exceeded.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum LimitKind {
131    EntitiesCreate,
132    EntitiesUpdate,
133    EntitiesDestroy,
134    TotalEntitiesAfterApply,
135    ComponentsPerEntity,
136    FieldsPerComponent,
137    SectionBytes,
138}
139
140/// Mask validation error kinds.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum MaskKind {
143    ComponentMask,
144    FieldMask { component: ComponentId },
145}
146
147/// Details for invalid mask errors.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum MaskReason {
150    NotEnoughBits { expected: usize, available: usize },
151    FieldCountMismatch { expected: usize, actual: usize },
152    MissingField { field: FieldId },
153    UnknownComponent { component: ComponentId },
154    InvalidComponentId { raw: u16 },
155    InvalidFieldIndex { field_index: usize, max: usize },
156    ComponentPresenceMismatch { component: ComponentId },
157    EmptyFieldMask { component: ComponentId },
158}
159
160/// Details for invalid value errors.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum ValueReason {
163    UnsignedOutOfRange {
164        bits: u8,
165        value: u64,
166    },
167    SignedOutOfRange {
168        bits: u8,
169        value: i64,
170    },
171    VarUIntOutOfRange {
172        value: u64,
173    },
174    VarSIntOutOfRange {
175        value: i64,
176    },
177    FixedPointOutOfRange {
178        min_q: i64,
179        max_q: i64,
180        value: i64,
181    },
182    TypeMismatch {
183        expected: &'static str,
184        found: &'static str,
185    },
186}
187
188impl fmt::Display for CodecError {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            Self::Wire(e) => write!(f, "wire error: {e}"),
192            Self::Bitstream(e) => write!(f, "bitstream error: {e}"),
193            Self::OutputTooSmall { needed, available } => {
194                write!(f, "output too small: need {needed}, have {available}")
195            }
196            Self::SchemaMismatch { expected, found } => {
197                write!(
198                    f,
199                    "schema hash mismatch: expected 0x{expected:016X}, found 0x{found:016X}"
200                )
201            }
202            Self::LimitsExceeded {
203                kind,
204                limit,
205                actual,
206            } => {
207                write!(f, "{kind} limit exceeded: {actual} > {limit}")
208            }
209            Self::InvalidMask { kind, reason } => {
210                write!(f, "invalid {kind}: {reason}")
211            }
212            Self::InvalidValue {
213                component,
214                field,
215                reason,
216            } => {
217                write!(f, "invalid value for {component:?}:{field:?}: {reason}")
218            }
219            Self::InvalidEntityOrder { previous, current } => {
220                write!(f, "entity order invalid: {previous} then {current}")
221            }
222            Self::TrailingSectionData {
223                section,
224                remaining_bits,
225            } => {
226                write!(
227                    f,
228                    "trailing data in section {section:?}: {remaining_bits} bits"
229                )
230            }
231            Self::UnexpectedSection { section } => {
232                write!(f, "unexpected section {section:?} in full snapshot")
233            }
234            Self::DuplicateSection { section } => {
235                write!(f, "duplicate section {section:?} in packet")
236            }
237            Self::DuplicateUpdateEncoding => {
238                write!(f, "multiple update encodings present in packet")
239            }
240            Self::BaselineTickMismatch { expected, found } => {
241                write!(
242                    f,
243                    "baseline tick mismatch: expected {expected}, found {found}"
244                )
245            }
246            Self::BaselineNotFound { requested_tick } => {
247                write!(f, "baseline tick {requested_tick} not found in history")
248            }
249            Self::EntityNotFound { entity_id } => {
250                write!(f, "entity {entity_id} not found")
251            }
252            Self::ComponentNotFound {
253                entity_id,
254                component_id,
255            } => {
256                write!(
257                    f,
258                    "component {component_id} not found on entity {entity_id}"
259                )
260            }
261            Self::DuplicateEntity { entity_id } => {
262                write!(f, "duplicate entity {entity_id} in create section")
263            }
264            Self::EntityAlreadyExists { entity_id } => {
265                write!(f, "entity {entity_id} already exists")
266            }
267            Self::SessionMissing => {
268                write!(f, "session state missing for compact packet")
269            }
270            Self::SessionInitInvalid => {
271                write!(f, "session init packet invalid")
272            }
273            Self::SessionUnsupportedMode { mode } => {
274                write!(f, "session compact mode {mode} unsupported")
275            }
276            Self::SessionOutOfOrder { previous, current } => {
277                write!(f, "session packet out of order: {previous} then {current}")
278            }
279        }
280    }
281}
282
283impl fmt::Display for LimitKind {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        let name = match self {
286            Self::EntitiesCreate => "entities",
287            Self::EntitiesUpdate => "update entities",
288            Self::EntitiesDestroy => "destroy entities",
289            Self::TotalEntitiesAfterApply => "total entities",
290            Self::ComponentsPerEntity => "components per entity",
291            Self::FieldsPerComponent => "fields per component",
292            Self::SectionBytes => "section bytes",
293        };
294        write!(f, "{name}")
295    }
296}
297
298impl fmt::Display for MaskKind {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        match self {
301            Self::ComponentMask => write!(f, "component mask"),
302            Self::FieldMask { component } => write!(f, "field mask for {component:?}"),
303        }
304    }
305}
306
307impl fmt::Display for MaskReason {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        match self {
310            Self::NotEnoughBits {
311                expected,
312                available,
313            } => {
314                write!(f, "need {expected} bits, have {available}")
315            }
316            Self::FieldCountMismatch { expected, actual } => {
317                write!(f, "expected {expected} fields, got {actual}")
318            }
319            Self::MissingField { field } => {
320                write!(f, "missing field {field:?} in full snapshot")
321            }
322            Self::UnknownComponent { component } => {
323                write!(f, "unknown component {component:?} in snapshot")
324            }
325            Self::InvalidComponentId { raw } => {
326                write!(f, "invalid component id {raw} in snapshot")
327            }
328            Self::InvalidFieldIndex { field_index, max } => {
329                write!(f, "field index {field_index} exceeds max {max}")
330            }
331            Self::ComponentPresenceMismatch { component } => {
332                write!(f, "component presence mismatch for {component:?}")
333            }
334            Self::EmptyFieldMask { component } => {
335                write!(f, "empty field mask for {component:?} is invalid")
336            }
337        }
338    }
339}
340
341impl fmt::Display for ValueReason {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        match self {
344            Self::UnsignedOutOfRange { bits, value } => {
345                write!(f, "unsigned value {value} does not fit in {bits} bits")
346            }
347            Self::SignedOutOfRange { bits, value } => {
348                write!(f, "signed value {value} does not fit in {bits} bits")
349            }
350            Self::VarUIntOutOfRange { value } => {
351                write!(f, "varuint value {value} exceeds u32::MAX")
352            }
353            Self::VarSIntOutOfRange { value } => {
354                write!(f, "varsint value {value} exceeds i32 range")
355            }
356            Self::FixedPointOutOfRange {
357                min_q,
358                max_q,
359                value,
360            } => {
361                write!(f, "fixed-point value {value} outside [{min_q}, {max_q}]")
362            }
363            Self::TypeMismatch { expected, found } => {
364                write!(f, "expected {expected} but got {found}")
365            }
366        }
367    }
368}
369
370impl std::error::Error for CodecError {
371    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
372        match self {
373            Self::Wire(e) => Some(e),
374            Self::Bitstream(e) => Some(e),
375            _ => None,
376        }
377    }
378}
379
380impl From<wire::DecodeError> for CodecError {
381    fn from(err: wire::DecodeError) -> Self {
382        Self::Wire(err)
383    }
384}
385
386impl From<bitstream::BitError> for CodecError {
387    fn from(err: bitstream::BitError) -> Self {
388        Self::Bitstream(err)
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn error_display_baseline_not_found() {
398        let err = CodecError::BaselineNotFound { requested_tick: 42 };
399        let msg = err.to_string();
400        assert!(msg.contains("42"), "should mention tick");
401        assert!(msg.contains("baseline"), "should mention baseline");
402    }
403
404    #[test]
405    fn error_display_entity_not_found() {
406        let err = CodecError::EntityNotFound { entity_id: 123 };
407        let msg = err.to_string();
408        assert!(msg.contains("123"), "should mention entity id");
409    }
410
411    #[test]
412    fn error_display_component_not_found() {
413        let err = CodecError::ComponentNotFound {
414            entity_id: 10,
415            component_id: 5,
416        };
417        let msg = err.to_string();
418        assert!(msg.contains("10"), "should mention entity id");
419        assert!(msg.contains('5'), "should mention component id");
420    }
421
422    #[test]
423    fn error_display_duplicate_entity() {
424        let err = CodecError::DuplicateEntity { entity_id: 42 };
425        let msg = err.to_string();
426        assert!(msg.contains("42"), "should mention entity id");
427        assert!(msg.contains("duplicate"), "should mention duplicate");
428    }
429
430    #[test]
431    fn error_display_entity_already_exists() {
432        let err = CodecError::EntityAlreadyExists { entity_id: 99 };
433        let msg = err.to_string();
434        assert!(msg.contains("99"), "should mention entity id");
435        assert!(msg.contains("exists"), "should mention exists");
436    }
437
438    #[test]
439    fn error_from_wire_error() {
440        let wire_err = wire::DecodeError::InvalidMagic { found: 0x1234 };
441        let codec_err: CodecError = wire_err.into();
442        assert!(matches!(codec_err, CodecError::Wire(_)));
443    }
444
445    #[test]
446    fn error_from_bitstream_error() {
447        let bit_err = bitstream::BitError::UnexpectedEof {
448            requested: 1,
449            available: 0,
450        };
451        let codec_err: CodecError = bit_err.into();
452        assert!(matches!(codec_err, CodecError::Bitstream(_)));
453    }
454
455    #[test]
456    fn error_source_wire() {
457        let wire_err = wire::DecodeError::InvalidMagic { found: 0x1234 };
458        let codec_err = CodecError::Wire(wire_err);
459        let source = std::error::Error::source(&codec_err);
460        assert!(source.is_some(), "should have a source");
461    }
462
463    #[test]
464    fn error_source_none_for_others() {
465        let err = CodecError::EntityNotFound { entity_id: 1 };
466        let source = std::error::Error::source(&err);
467        assert!(source.is_none(), "non-wrapped errors should have no source");
468    }
469
470    #[test]
471    fn error_equality() {
472        let err1 = CodecError::EntityNotFound { entity_id: 42 };
473        let err2 = CodecError::EntityNotFound { entity_id: 42 };
474        let err3 = CodecError::EntityNotFound { entity_id: 43 };
475
476        assert_eq!(err1, err2);
477        assert_ne!(err1, err3);
478    }
479
480    #[test]
481    fn error_is_std_error() {
482        fn assert_error<E: std::error::Error>() {}
483        assert_error::<CodecError>();
484    }
485}