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