Skip to main content

base64_ng/v2/
contracts.rs

1//! Error, progress, lifecycle, and reporting contracts for the 2.0 core.
2
3use core::num::NonZeroUsize;
4
5#[path = "contracts/reporting.rs"]
6mod reporting;
7
8// These become reachable when the complete 2.0 surface is exposed. Commit 8
9// keeps the model private while compiling its future external shape in CI.
10#[allow(unused_imports)]
11pub(crate) use super::lifecycle::Lifecycle;
12#[allow(unused_imports)]
13pub use super::lifecycle::SourceSpan;
14#[allow(unused_imports)]
15pub use reporting::{AssuranceClass, Atomicity, BackendClass, ProtocolScope};
16
17/// Exact progress committed by one transform call.
18#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
19pub struct Progress {
20    input_consumed: usize,
21    output_produced: usize,
22}
23
24impl Progress {
25    /// A call that consumed and produced no bytes.
26    pub const ZERO: Self = Self::new(0, 0);
27
28    /// Constructs an exact progress report for crate-owned state machines.
29    pub(crate) const fn new(input_consumed: usize, output_produced: usize) -> Self {
30        Self {
31            input_consumed,
32            output_produced,
33        }
34    }
35
36    /// Returns the input prefix accepted by this call.
37    #[must_use]
38    pub const fn input_consumed(self) -> usize {
39        self.input_consumed
40    }
41
42    /// Returns the output prefix initialized by this call.
43    #[must_use]
44    pub const fn output_produced(self) -> usize {
45        self.output_produced
46    }
47}
48
49/// Retry information when the current destination cannot make progress.
50#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
51pub struct OutputFull {
52    minimum_output: NonZeroUsize,
53}
54
55impl OutputFull {
56    /// Constructs a retry requirement that always permits progress.
57    pub(crate) const fn new(minimum_output: NonZeroUsize) -> Self {
58        Self { minimum_output }
59    }
60
61    /// Returns the minimum destination bytes needed by the next retry.
62    #[must_use]
63    pub const fn minimum_output(self) -> NonZeroUsize {
64        self.minimum_output
65    }
66}
67
68/// Non-failing state reached after one transform call.
69#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
70#[non_exhaustive]
71pub enum Status {
72    /// More input or an explicit finish call is required.
73    NeedInput,
74    /// Retry with at least the reported output capacity.
75    OutputFull(OutputFull),
76    /// The transform completed successfully and accepts no more input.
77    Complete,
78}
79
80impl Status {
81    /// Returns the stable lowercase identifier for this status class.
82    #[must_use]
83    pub const fn as_str(&self) -> &'static str {
84        match self {
85            Self::NeedInput => "need-input",
86            Self::OutputFull(_) => "output-full",
87            Self::Complete => "complete",
88        }
89    }
90}
91
92/// One non-failing transform result.
93#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
94pub struct Step {
95    progress: Progress,
96    status: Status,
97}
98
99impl Step {
100    pub(crate) const fn new(progress: Progress, status: Status) -> Self {
101        Self { progress, status }
102    }
103
104    /// Returns exact input and output progress for this call.
105    #[must_use]
106    pub const fn progress(self) -> Progress {
107        self.progress
108    }
109
110    /// Returns the state reached by this call.
111    #[must_use]
112    pub const fn status(self) -> Status {
113        self.status
114    }
115}
116
117/// Redacted malformed-input classification.
118#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
119#[non_exhaustive]
120pub enum InputErrorKind {
121    /// A byte is outside the selected alphabet.
122    InvalidByte,
123    /// Padding is missing, forbidden, misplaced, or excessive.
124    InvalidPadding,
125    /// Unused trailing bits are non-zero.
126    NonCanonicalTrailingBits,
127    /// The encoded length cannot represent a complete value.
128    InvalidLength,
129    /// Finishing found an incomplete encoded quantum.
130    TruncatedInput,
131    /// Input followed a terminal padded quantum.
132    TrailingData,
133    /// Wrapped body layout is malformed.
134    InvalidLineWrap,
135}
136
137impl InputErrorKind {
138    /// Returns the stable lowercase identifier for this error class.
139    #[must_use]
140    pub const fn as_str(self) -> &'static str {
141        match self {
142            Self::InvalidByte => "invalid-byte",
143            Self::InvalidPadding => "invalid-padding",
144            Self::NonCanonicalTrailingBits => "noncanonical-trailing-bits",
145            Self::InvalidLength => "invalid-length",
146            Self::TruncatedInput => "truncated-input",
147            Self::TrailingData => "trailing-data",
148            Self::InvalidLineWrap => "invalid-line-wrap",
149        }
150    }
151}
152
153/// Detailed ordinary malformed-input diagnostic.
154///
155/// `Debug` is redacted. `Display` includes ordinary input diagnostics and must
156/// not be logged for secret-bearing input.
157#[derive(Clone, Copy, Eq, Hash, PartialEq)]
158#[non_exhaustive]
159pub enum InputError {
160    /// A byte is outside the selected alphabet.
161    InvalidByte {
162        /// Original absolute source index.
163        index: usize,
164        /// Rejected byte.
165        byte: u8,
166    },
167    /// Padding became invalid at this original absolute source index.
168    InvalidPadding {
169        /// Original absolute source index.
170        index: usize,
171    },
172    /// Unused trailing bits are non-zero.
173    NonCanonicalTrailingBits {
174        /// Original absolute source index of the final significant symbol.
175        index: usize,
176    },
177    /// The encoded length cannot represent a complete value.
178    InvalidLength,
179    /// Finishing found an incomplete encoded quantum.
180    TruncatedInput {
181        /// Original absolute source index at end of input.
182        index: usize,
183    },
184    /// Input followed a terminal padded quantum.
185    TrailingData {
186        /// Original absolute source index of the first trailing byte.
187        index: usize,
188    },
189    /// Wrapped body layout is malformed.
190    InvalidLineWrap {
191        /// Original absolute source index where layout became invalid.
192        index: usize,
193    },
194}
195
196impl InputError {
197    /// Returns a redacted, stable error class.
198    #[must_use]
199    pub const fn kind(self) -> InputErrorKind {
200        match self {
201            Self::InvalidByte { .. } => InputErrorKind::InvalidByte,
202            Self::InvalidPadding { .. } => InputErrorKind::InvalidPadding,
203            Self::NonCanonicalTrailingBits { .. } => InputErrorKind::NonCanonicalTrailingBits,
204            Self::InvalidLength => InputErrorKind::InvalidLength,
205            Self::TruncatedInput { .. } => InputErrorKind::TruncatedInput,
206            Self::TrailingData { .. } => InputErrorKind::TrailingData,
207            Self::InvalidLineWrap { .. } => InputErrorKind::InvalidLineWrap,
208        }
209    }
210}
211
212impl core::fmt::Debug for InputError {
213    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
214        formatter
215            .debug_struct("InputError")
216            .field("kind", &self.kind())
217            .finish_non_exhaustive()
218    }
219}
220
221impl core::fmt::Display for InputError {
222    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
223        match self {
224            Self::InvalidByte { index, byte } => {
225                write!(
226                    formatter,
227                    "invalid byte 0x{byte:02x} at source index {index}"
228                )
229            }
230            Self::InvalidPadding { index } => {
231                write!(formatter, "invalid padding at source index {index}")
232            }
233            Self::NonCanonicalTrailingBits { index } => {
234                write!(
235                    formatter,
236                    "noncanonical trailing bits at source index {index}"
237                )
238            }
239            Self::InvalidLength => formatter.write_str("invalid encoded input length"),
240            Self::TruncatedInput { index } => {
241                write!(formatter, "truncated input at source index {index}")
242            }
243            Self::TrailingData { index } => {
244                write!(formatter, "trailing data at source index {index}")
245            }
246            Self::InvalidLineWrap { index } => {
247                write!(formatter, "invalid line wrapping at source index {index}")
248            }
249        }
250    }
251}
252
253/// Internal backend integrity failure, separate from attacker input errors.
254#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
255#[non_exhaustive]
256pub enum BackendFault {
257    /// A backend known-answer test failed.
258    SelfTestFailed,
259    /// Checked output diverged from the independent reference path.
260    OutputMismatch,
261    /// A backend reached an impossible internal state.
262    ImpossibleState,
263    /// Scalar retry after quarantining an accelerated backend failed.
264    ScalarRetryFailed,
265}
266
267impl BackendFault {
268    /// Returns the stable lowercase identifier for this fault class.
269    #[must_use]
270    pub const fn as_str(self) -> &'static str {
271        match self {
272            Self::SelfTestFailed => "backend-self-test-failed",
273            Self::OutputMismatch => "backend-output-mismatch",
274            Self::ImpossibleState => "backend-impossible-state",
275            Self::ScalarRetryFailed => "backend-scalar-retry-failed",
276        }
277    }
278}
279
280/// Absorbing transform failure.
281#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
282#[non_exhaustive]
283pub enum Failure {
284    /// Attacker-controlled or otherwise malformed ordinary input.
285    Input(InputError),
286    /// Absolute source position can no longer be represented by `usize`.
287    PositionOverflow,
288    /// Internal backend integrity failure.
289    Backend(BackendFault),
290    /// A caller-declared input, output, allocation, or frame limit was exceeded.
291    ResourceLimit,
292}
293
294impl Failure {
295    /// Returns the stable lowercase identifier for this failure class.
296    #[must_use]
297    pub const fn as_str(self) -> &'static str {
298        match self {
299            Self::Input(error) => error.kind().as_str(),
300            Self::PositionOverflow => "position-overflow",
301            Self::Backend(fault) => fault.as_str(),
302            Self::ResourceLimit => "resource-limit",
303        }
304    }
305}
306
307/// Illegal call against a successfully completed state.
308#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
309#[non_exhaustive]
310pub enum TerminalError {
311    /// New input was supplied after finalization began but before output drained.
312    InputAfterFinish,
313    /// New input was supplied after successful completion.
314    InputAfterComplete,
315}
316
317impl TerminalError {
318    /// Returns the stable lowercase identifier for this terminal call error.
319    #[must_use]
320    pub const fn as_str(self) -> &'static str {
321        match self {
322            Self::InputAfterFinish => "input-after-finish",
323            Self::InputAfterComplete => "input-after-complete",
324        }
325    }
326}
327
328/// Error returned by a lifecycle operation.
329#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
330#[non_exhaustive]
331pub enum OperationError {
332    /// The transform entered or was already in an absorbing failure state.
333    Failed(Failure),
334    /// The call is not legal after successful completion.
335    Terminal(TerminalError),
336}
337
338impl OperationError {
339    /// Returns the stable lowercase identifier for this error class.
340    #[must_use]
341    pub const fn as_str(self) -> &'static str {
342        match self {
343            Self::Failed(failure) => failure.as_str(),
344            Self::Terminal(error) => error.as_str(),
345        }
346    }
347}
348
349impl core::fmt::Display for OperationError {
350    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
351        match self {
352            Self::Failed(Failure::Input(error)) => error.fmt(formatter),
353            Self::Failed(failure) => formatter.write_str(failure.as_str()),
354            Self::Terminal(error) => formatter.write_str(error.as_str()),
355        }
356    }
357}
358
359impl core::error::Error for OperationError {}