cadmpeg_core/error.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Errors returned by codec parsing and resource enforcement.
3
4use crate::decode::{ErrorContext, ResourceLimit, SourceLocation};
5
6/// Errors a codec can raise.
7///
8/// Marked `#[non_exhaustive]`: external exhaustive matches must carry a
9/// wildcard arm.
10#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum CodecError {
13 /// The bytes are not this codec's format.
14 #[error("not the expected format: {0}")]
15 WrongFormat(String),
16 /// The container was structurally malformed.
17 #[error("malformed container: {0}")]
18 Malformed(String),
19 /// The document supplied to an encoder violates its input contract.
20 #[error("invalid encoder input: {0}")]
21 InvalidInput(String),
22 /// A required read extended past the end of its window after commitment.
23 ///
24 /// Distinct from [`CodecError::Malformed`]: a truncation is missing input,
25 /// not an inconsistency inside the bytes that are present.
26 #[error(
27 "truncated input during {} at space {} offset {}",
28 .context.operation, .location.space.index(), .location.offset
29 )]
30 Truncated {
31 /// Where the truncated read began.
32 location: SourceLocation,
33 /// Static context for the failure.
34 context: ErrorContext,
35 },
36 /// A resource limit refused the decode: policy or the allocator.
37 ///
38 /// Never reported as [`CodecError::Malformed`]: a budget refusal is a
39 /// statement about policy, not about the input.
40 #[error(
41 "resource limit on {:?}: {:?} (limit {}, used {}, requested {})",
42 .0.dimension, .0.reason, .0.limit, .0.used, .0.additional
43 )]
44 ResourceLimit(ResourceLimit),
45 /// Strict decode mode refused a reported loss.
46 ///
47 /// Never reported as [`CodecError::Malformed`]: a strict refusal is a
48 /// statement about the decode mode, not about the input. The bytes can be
49 /// well formed and still refuse under strict mode. The strict-mode gate in
50 /// the `Codec` decode wrapper is the only construction site.
51 #[error("strict mode rejects {loss_code}: {message}")]
52 StrictRefusal {
53 /// Stable `namespace/code` form of the refusing loss.
54 loss_code: String,
55 /// The refusing loss's own message, without any refusal prefix.
56 message: String,
57 },
58 /// The codec does not implement a required capability.
59 #[error("not implemented yet: {0}")]
60 NotImplemented(String),
61 /// Underlying I/O failure.
62 #[error("io error: {0}")]
63 Io(#[from] std::io::Error),
64}
65
66impl CodecError {
67 /// Builds a malformed-container error from a displayable message.
68 pub fn malformed(message: impl std::fmt::Display) -> Self {
69 Self::Malformed(message.to_string())
70 }
71
72 /// Builds a truncation error at a qualified source location.
73 pub const fn truncated(location: SourceLocation, operation: &'static str) -> Self {
74 Self::Truncated {
75 location,
76 context: ErrorContext {
77 operation,
78 location: Some(location),
79 },
80 }
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::CodecError;
87
88 #[test]
89 fn malformed_constructor_formats_the_message_once() {
90 let error = CodecError::malformed(format_args!("field {} is invalid", 7));
91
92 assert_eq!(error.to_string(), "malformed container: field 7 is invalid");
93 }
94
95 #[test]
96 fn a_strict_refusal_names_the_loss_and_claims_no_container_defect() {
97 let error = CodecError::StrictRefusal {
98 loss_code: "step/parse.noncanonical-syntax".into(),
99 message: "complex partial records are not alphabetical".into(),
100 };
101
102 assert_eq!(
103 error.to_string(),
104 "strict mode rejects step/parse.noncanonical-syntax: complex partial \
105 records are not alphabetical"
106 );
107 }
108}