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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
//! Allocation-free formatting and exact counted-sink encoding.
use super::{
chunks::{EncodedChunk, EncodedChunks},
contracts::BackendFault,
ordinary::OneShotError,
specifications::{Base64, Codec, CodecSettings},
};
/// Error from allocation-free formatter encoding.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum FormatWriteError {
/// Encoding preflight failed before any formatter call.
Encoding(OneShotError),
/// A formatter call returned `fmt::Error`.
Formatter {
/// Bytes passed through fully successful prior `write_str` calls.
confirmed: usize,
},
/// A validated internal output invariant failed.
Backend {
/// Internal backend failure classification.
fault: BackendFault,
/// Bytes passed through fully successful prior `write_str` calls.
confirmed: usize,
},
}
impl FormatWriteError {
/// Returns bytes confirmed by fully successful formatter calls.
///
/// A failing `write_str` implementation may have partially mutated its
/// sink before returning. Those unreported bytes are intentionally not
/// included.
#[must_use]
pub const fn confirmed(&self) -> usize {
match self {
Self::Encoding(_) => 0,
Self::Formatter { confirmed } | Self::Backend { confirmed, .. } => *confirmed,
}
}
}
impl core::fmt::Display for FormatWriteError {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Encoding(error) => error.fmt(formatter),
Self::Formatter { confirmed } => write!(
formatter,
"formatter failed after {confirmed} confirmed Base64 bytes"
),
Self::Backend { fault, confirmed } => write!(
formatter,
"Base64 backend {} failed after {confirmed} confirmed bytes",
fault.as_str()
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for FormatWriteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Encoding(error) => Some(error),
Self::Formatter { .. } | Self::Backend { .. } => None,
}
}
}
/// A sink whose successful writes report their exact accepted prefix.
///
/// `write` must return an accepted count no larger than `bytes.len()`. `Err`
/// must mean that the failing call accepted zero bytes. This stronger contract
/// lets [`Base64::encode_to_counted`] report exact committed progress; sinks
/// that can mutate before returning `Err` must use formatter or I/O contracts
/// with weaker prefix guarantees instead.
pub trait CountedSink {
/// Sink-specific failure.
type Error;
/// Accepts and reports the exact committed prefix of `bytes`.
fn write(&mut self, bytes: &[u8]) -> Result<usize, Self::Error>;
}
/// Failure from exact-progress counted-sink encoding.
#[derive(Debug)]
#[non_exhaustive]
pub enum CountedWriteError<E> {
/// Encoding preflight failed before the sink was called.
Encoding(OneShotError),
/// The sink rejected a call without accepting bytes from that call.
Sink {
/// Sink-specific error.
error: E,
/// Exact bytes accepted by successful prior calls.
committed: usize,
},
/// The sink accepted zero bytes from a non-empty call.
WriteZero {
/// Exact bytes accepted by successful prior calls.
committed: usize,
},
/// The sink violated its count contract.
InvalidCount {
/// Count reported by the sink.
reported: usize,
/// Bytes offered to the sink.
offered: usize,
/// Exact bytes accepted before the invalid report.
committed: usize,
},
/// A validated internal output invariant failed.
Backend {
/// Internal backend failure classification.
fault: BackendFault,
/// Exact bytes accepted by successful prior calls.
committed: usize,
},
}
impl<E> CountedWriteError<E> {
/// Returns the exact committed byte count before failure.
#[must_use]
pub const fn committed(&self) -> usize {
match self {
Self::Encoding(_) => 0,
Self::Sink { committed, .. }
| Self::WriteZero { committed }
| Self::InvalidCount { committed, .. }
| Self::Backend { committed, .. } => *committed,
}
}
}
impl<E: core::fmt::Display> core::fmt::Display for CountedWriteError<E> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Encoding(error) => error.fmt(formatter),
Self::Sink { error, committed } => write!(
formatter,
"counted sink failed after {committed} committed Base64 bytes: {error}"
),
Self::WriteZero { committed } => write!(
formatter,
"counted sink accepted zero bytes after {committed} committed Base64 bytes"
),
Self::InvalidCount {
reported,
offered,
committed,
} => write!(
formatter,
"counted sink reported {reported} bytes for a {offered}-byte write after \
{committed} committed Base64 bytes"
),
Self::Backend { fault, committed } => write!(
formatter,
"Base64 backend {} failed after {committed} committed bytes",
fault.as_str()
),
}
}
}
#[cfg(feature = "std")]
impl<E: std::error::Error + 'static> std::error::Error for CountedWriteError<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Encoding(error) => Some(error),
Self::Sink { error, .. } => Some(error),
Self::WriteZero { .. } | Self::InvalidCount { .. } | Self::Backend { .. } => None,
}
}
}
/// Lazy allocation-free encoded display for one borrowed input.
///
/// This value owns copied validated codec settings and borrows only the input.
/// Construct it with [`Base64::display`] so length errors are returned before
/// formatting begins.
#[derive(Clone, Copy)]
pub struct EncodedDisplay<'a> {
settings: CodecSettings,
input: &'a [u8],
}
impl core::fmt::Debug for EncodedDisplay<'_> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("EncodedDisplay")
.field("input_len", &self.input.len())
.finish_non_exhaustive()
}
}
impl core::fmt::Display for EncodedDisplay<'_> {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for chunk in EncodedChunks::new(self.settings, self.input) {
formatter.write_str(chunk_text(&chunk).map_err(|_| core::fmt::Error)?)?;
}
Ok(())
}
}
impl<S: Codec> Base64<S> {
/// Creates a lazy allocation-free display after encoding preflight.
pub fn display<'a>(&self, input: &'a [u8]) -> Result<EncodedDisplay<'a>, OneShotError> {
self.encoded_len(input.len())?;
Ok(EncodedDisplay {
settings: self.settings(),
input,
})
}
/// Encodes through `core::fmt::Write` without allocating.
///
/// On formatter failure, confirmed progress excludes the failing call
/// because `fmt::Write` cannot report whether that call partially mutated
/// its sink.
pub fn encode_to_fmt<W: core::fmt::Write + ?Sized>(
&self,
input: &[u8],
writer: &mut W,
) -> Result<usize, FormatWriteError> {
let chunks = self
.encoded_chunks(input)
.map_err(FormatWriteError::Encoding)?;
let mut confirmed = 0;
for chunk in chunks {
let text = chunk_text(&chunk).map_err(|_| FormatWriteError::Backend {
fault: BackendFault::ImpossibleState,
confirmed,
})?;
writer
.write_str(text)
.map_err(|_| FormatWriteError::Formatter { confirmed })?;
confirmed += text.len();
}
Ok(confirmed)
}
/// Encodes through an exact-progress counted sink without allocating.
pub fn encode_to_counted<W: CountedSink + ?Sized>(
&self,
input: &[u8],
writer: &mut W,
) -> Result<usize, CountedWriteError<W::Error>> {
let chunks = self
.encoded_chunks(input)
.map_err(CountedWriteError::Encoding)?;
let mut committed = 0;
for chunk in chunks {
let mut pending = chunk.as_bytes();
while !pending.is_empty() {
let written = writer
.write(pending)
.map_err(|error| CountedWriteError::Sink { error, committed })?;
if written == 0 {
return Err(CountedWriteError::WriteZero { committed });
}
if written > pending.len() {
return Err(CountedWriteError::InvalidCount {
reported: written,
offered: pending.len(),
committed,
});
}
committed += written;
pending = &pending[written..];
}
}
Ok(committed)
}
}
fn chunk_text(chunk: &EncodedChunk) -> Result<&str, core::str::Utf8Error> {
chunk.as_str()
}