Skip to main content

cadmpeg_core/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Errors returned by codec parsing and resource enforcement.
3
4use crate::decode::{DecodeDepth, ResourceLimit, SourceLocation};
5use crate::dialect::DialectLayers;
6use crate::target::TargetRefusal;
7
8/// Errors a codec can raise.
9///
10/// Marked `#[non_exhaustive]`: external exhaustive matches must carry a
11/// wildcard arm.
12#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14pub enum CodecError {
15    /// The bytes are not this codec's format.
16    #[error("not the expected format: {0}")]
17    WrongFormat(String),
18    /// The container was structurally malformed.
19    #[error("malformed container: {0}")]
20    Malformed(String),
21    /// The document supplied to an encoder violates its input contract.
22    #[error("invalid encoder input: {0}")]
23    InvalidInput(String),
24    /// A required read extended past the end of its window after commitment.
25    ///
26    /// Distinct from [`CodecError::Malformed`]: a truncation is missing input,
27    /// not an inconsistency inside the bytes that are present.
28    #[error(
29        "truncated input during {} at space {} offset {}",
30        .operation, .location.space.index(), .location.offset
31    )]
32    Truncated {
33        /// Where the truncated read began.
34        location: SourceLocation,
35        /// Static operation that required the missing bytes.
36        operation: &'static str,
37    },
38    /// A resource limit refused the decode: policy or the allocator.
39    ///
40    /// Never reported as [`CodecError::Malformed`]: a budget refusal is a
41    /// statement about policy, not about the input.
42    #[error(
43        "resource limit on {:?}: {:?} (limit {}, used {}, requested {})",
44        .0.dimension, .0.reason, .0.limit, .0.used, .0.additional
45    )]
46    ResourceLimit(ResourceLimit),
47    /// The document was identified, and its dialect is not supported.
48    ///
49    /// Never reported as [`CodecError::WrongFormat`]: the bytes are this
50    /// codec's format, and the codec says so by carrying the identification it
51    /// made. Identity survives refusal, so a caller can name what it was handed
52    /// even though nothing was decoded.
53    ///
54    /// The SAT codec constructs this variant for a recognized stream kind that
55    /// does not frame. The STEP codec constructs it for the Part 26 HDF5, Part
56    /// 28 XML, and AP242 business-object XML encodings that it identifies but
57    /// does not decode.
58    ///
59    /// The identification is boxed: it is the widest payload any variant of
60    /// this enum carries, and every `Result<_, CodecError>` in the workspace
61    /// would otherwise grow to its width.
62    #[error("unsupported {} dialect {}: {message}", .dialects.primary().format(), .dialects.primary().dialect())]
63    UnsupportedDialect {
64        /// Every format layer identified before the refusal.
65        dialects: Box<DialectLayers>,
66        /// Why the identified dialect is not supported.
67        message: String,
68    },
69    /// The encoder could not resolve or deliver a write target.
70    #[error("{0}")]
71    UnsupportedTarget(Box<TargetRefusal>),
72    /// The document records less of its source than the encoder plans from.
73    #[error(
74        "the {format} encoder needs a {needed}-depth document; this one records {recorded} depth"
75    )]
76    InsufficientDepth {
77        /// Output format of the refusing encoder.
78        format: &'static str,
79        /// The least depth the encoder plans from.
80        needed: DecodeDepth,
81        /// The depth the document records.
82        recorded: DecodeDepth,
83    },
84    /// The codec does not implement a required capability.
85    #[error("not implemented yet: {0}")]
86    NotImplemented(String),
87    /// Underlying I/O failure.
88    #[error("io error: {0}")]
89    Io(#[from] std::io::Error),
90}
91
92impl From<ResourceLimit> for CodecError {
93    fn from(limit: ResourceLimit) -> Self {
94        Self::ResourceLimit(limit)
95    }
96}
97
98impl CodecError {
99    /// Builds a malformed-container error from a displayable message.
100    pub fn malformed(message: impl std::fmt::Display) -> Self {
101        Self::Malformed(message.to_string())
102    }
103
104    /// Builds a truncation error at a qualified source location.
105    pub const fn truncated(location: SourceLocation, operation: &'static str) -> Self {
106        Self::Truncated {
107            location,
108            operation,
109        }
110    }
111}
112
113impl From<TargetRefusal> for CodecError {
114    fn from(refusal: TargetRefusal) -> Self {
115        Self::UnsupportedTarget(Box::new(refusal))
116    }
117}
118
119/// A source property whose key names nothing, or whose key the record already
120/// states, is an inconsistency inside the bytes that are present, so a reader
121/// that refuses the whole property set reports it as a malformed container.
122impl From<crate::text::NamedEntryError> for CodecError {
123    fn from(refused: crate::text::NamedEntryError) -> Self {
124        Self::malformed(refused)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use std::collections::BTreeMap;
131
132    use super::CodecError;
133    use crate::dialect::{DialectLayers, DialectMatch};
134
135    #[test]
136    fn malformed_constructor_formats_the_message_once() {
137        let error = CodecError::malformed(format_args!("field {} is invalid", 7));
138
139        assert_eq!(error.to_string(), "malformed container: field 7 is invalid");
140    }
141
142    #[test]
143    fn a_dialect_refusal_keeps_the_identification_it_refused() {
144        let error = CodecError::UnsupportedDialect {
145            dialects: Box::new(
146                DialectLayers::of(
147                    DialectMatch::refused(crate::dialect_id!("acis:save-format-binary-other"))
148                        .with_declared(BTreeMap::from([(
149                            crate::nonblank_literal!("save_format"),
150                            "700".to_owned(),
151                        )])),
152                )
153                .with(DialectMatch::refused(crate::dialect_id!("sat:binary")))
154                .expect("distinct dialect layer keys"),
155            ),
156            message: "save format 700 has no read grammar".into(),
157        };
158
159        assert_eq!(
160            error.to_string(),
161            "unsupported acis dialect acis:save-format-binary-other: save format 700 has no read grammar"
162        );
163        let CodecError::UnsupportedDialect { dialects, .. } = &error else {
164            panic!("the variant just built is the one matched");
165        };
166        assert_eq!(
167            dialects.primary().dialect().as_str(),
168            "acis:save-format-binary-other"
169        );
170        assert_eq!(dialects.primary().declared()["save_format"], "700");
171        assert_eq!(dialects.iter().count(), 2);
172    }
173}