1use std::fmt;
4
5use schema::{ComponentId, FieldId};
6
7pub type CodecResult<T> = Result<T, CodecError>;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum CodecError {
13 Wire(wire::DecodeError),
15
16 Bitstream(bitstream::BitError),
18
19 OutputTooSmall { needed: usize, available: usize },
21
22 SchemaMismatch { expected: u64, found: u64 },
24
25 LimitsExceeded {
27 kind: LimitKind,
28 limit: usize,
29 actual: usize,
30 },
31
32 InvalidMask { kind: MaskKind, reason: MaskReason },
34
35 InvalidValue {
37 component: ComponentId,
38 field: FieldId,
39 reason: ValueReason,
40 },
41
42 InvalidEntityOrder { previous: u32, current: u32 },
44
45 TrailingSectionData {
47 section: wire::SectionTag,
48 remaining_bits: usize,
49 },
50
51 UnexpectedSection { section: wire::SectionTag },
53
54 DuplicateSection { section: wire::SectionTag },
56
57 DuplicateUpdateEncoding,
59
60 BaselineTickMismatch { expected: u32, found: u32 },
62
63 BaselineNotFound {
65 requested_tick: u32,
67 },
68
69 EntityNotFound {
71 entity_id: u32,
73 },
74
75 ComponentNotFound {
77 entity_id: u32,
79 component_id: u16,
81 },
82
83 DuplicateEntity {
85 entity_id: u32,
87 },
88
89 EntityAlreadyExists {
91 entity_id: u32,
93 },
94
95 SessionMissing,
97
98 SessionInitInvalid,
100
101 SessionUnsupportedMode { mode: u8 },
103
104 SessionOutOfOrder { previous: u32, current: u32 },
106}
107
108impl CodecError {
109 #[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum MaskKind {
143 ComponentMask,
144 FieldMask { component: ComponentId },
145}
146
147#[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#[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}