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