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::{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 codec does not implement a required capability.
73    #[error("not implemented yet: {0}")]
74    NotImplemented(String),
75    /// Underlying I/O failure.
76    #[error("io error: {0}")]
77    Io(#[from] std::io::Error),
78}
79
80impl From<ResourceLimit> for CodecError {
81    fn from(limit: ResourceLimit) -> Self {
82        Self::ResourceLimit(limit)
83    }
84}
85
86impl CodecError {
87    /// Builds a malformed-container error from a displayable message.
88    pub fn malformed(message: impl std::fmt::Display) -> Self {
89        Self::Malformed(message.to_string())
90    }
91
92    /// Builds a truncation error at a qualified source location.
93    pub const fn truncated(location: SourceLocation, operation: &'static str) -> Self {
94        Self::Truncated {
95            location,
96            operation,
97        }
98    }
99}
100
101impl From<TargetRefusal> for CodecError {
102    fn from(refusal: TargetRefusal) -> Self {
103        Self::UnsupportedTarget(Box::new(refusal))
104    }
105}
106
107/// A source property whose key names nothing, or whose key the record already
108/// states, is an inconsistency inside the bytes that are present, so a reader
109/// that refuses the whole property set reports it as a malformed container.
110impl From<crate::text::NamedEntryError> for CodecError {
111    fn from(refused: crate::text::NamedEntryError) -> Self {
112        match refused {
113            crate::text::NamedEntryError::ResourceRefusal(limit) => Self::ResourceLimit(limit),
114            refused => Self::malformed(refused),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::collections::BTreeMap;
122
123    use super::CodecError;
124    use crate::dialect::{DialectLayers, DialectMatch};
125
126    #[test]
127    fn malformed_constructor_formats_the_message_once() {
128        let error = CodecError::malformed(format_args!("field {} is invalid", 7));
129
130        assert_eq!(error.to_string(), "malformed container: field 7 is invalid");
131    }
132
133    #[test]
134    fn a_dialect_refusal_keeps_the_identification_it_refused() {
135        let error = CodecError::UnsupportedDialect {
136            dialects: Box::new(
137                DialectLayers::of(
138                    DialectMatch::refused(crate::dialect_id!("acis:save-format-binary-other"))
139                        .with_declared(BTreeMap::from([(
140                            crate::nonblank_literal!("save_format"),
141                            "700".to_owned(),
142                        )])),
143                )
144                .with(DialectMatch::refused(crate::dialect_id!("sat:binary")))
145                .expect("distinct dialect layer keys"),
146            ),
147            message: "save format 700 has no read grammar".into(),
148        };
149
150        assert_eq!(
151            error.to_string(),
152            "unsupported acis dialect acis:save-format-binary-other: save format 700 has no read grammar"
153        );
154        let CodecError::UnsupportedDialect { dialects, .. } = &error else {
155            panic!("the variant just built is the one matched");
156        };
157        assert_eq!(
158            dialects.primary().dialect().as_str(),
159            "acis:save-format-binary-other"
160        );
161        assert_eq!(dialects.primary().declared()["save_format"], "700");
162        assert_eq!(dialects.iter().count(), 2);
163    }
164}