Skip to main content

base64_ng/v2/
formatting.rs

1//! Allocation-free formatting and exact counted-sink encoding.
2
3use super::{
4    chunks::{EncodedChunk, EncodedChunks},
5    contracts::BackendFault,
6    ordinary::OneShotError,
7    specifications::{Base64, Codec, CodecSettings},
8};
9
10/// Error from allocation-free formatter encoding.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum FormatWriteError {
14    /// Encoding preflight failed before any formatter call.
15    Encoding(OneShotError),
16    /// A formatter call returned `fmt::Error`.
17    Formatter {
18        /// Bytes passed through fully successful prior `write_str` calls.
19        confirmed: usize,
20    },
21    /// A validated internal output invariant failed.
22    Backend {
23        /// Internal backend failure classification.
24        fault: BackendFault,
25        /// Bytes passed through fully successful prior `write_str` calls.
26        confirmed: usize,
27    },
28}
29
30impl FormatWriteError {
31    /// Returns bytes confirmed by fully successful formatter calls.
32    ///
33    /// A failing `write_str` implementation may have partially mutated its
34    /// sink before returning. Those unreported bytes are intentionally not
35    /// included.
36    #[must_use]
37    pub const fn confirmed(&self) -> usize {
38        match self {
39            Self::Encoding(_) => 0,
40            Self::Formatter { confirmed } | Self::Backend { confirmed, .. } => *confirmed,
41        }
42    }
43}
44
45impl core::fmt::Display for FormatWriteError {
46    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        match self {
48            Self::Encoding(error) => error.fmt(formatter),
49            Self::Formatter { confirmed } => write!(
50                formatter,
51                "formatter failed after {confirmed} confirmed Base64 bytes"
52            ),
53            Self::Backend { fault, confirmed } => write!(
54                formatter,
55                "Base64 backend {} failed after {confirmed} confirmed bytes",
56                fault.as_str()
57            ),
58        }
59    }
60}
61
62#[cfg(feature = "std")]
63impl std::error::Error for FormatWriteError {
64    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
65        match self {
66            Self::Encoding(error) => Some(error),
67            Self::Formatter { .. } | Self::Backend { .. } => None,
68        }
69    }
70}
71
72/// A sink whose successful writes report their exact accepted prefix.
73///
74/// `write` must return an accepted count no larger than `bytes.len()`. `Err`
75/// must mean that the failing call accepted zero bytes. This stronger contract
76/// lets [`Base64::encode_to_counted`] report exact committed progress; sinks
77/// that can mutate before returning `Err` must use formatter or I/O contracts
78/// with weaker prefix guarantees instead.
79pub trait CountedSink {
80    /// Sink-specific failure.
81    type Error;
82
83    /// Accepts and reports the exact committed prefix of `bytes`.
84    fn write(&mut self, bytes: &[u8]) -> Result<usize, Self::Error>;
85}
86
87/// Failure from exact-progress counted-sink encoding.
88#[derive(Debug)]
89#[non_exhaustive]
90pub enum CountedWriteError<E> {
91    /// Encoding preflight failed before the sink was called.
92    Encoding(OneShotError),
93    /// The sink rejected a call without accepting bytes from that call.
94    Sink {
95        /// Sink-specific error.
96        error: E,
97        /// Exact bytes accepted by successful prior calls.
98        committed: usize,
99    },
100    /// The sink accepted zero bytes from a non-empty call.
101    WriteZero {
102        /// Exact bytes accepted by successful prior calls.
103        committed: usize,
104    },
105    /// The sink violated its count contract.
106    InvalidCount {
107        /// Count reported by the sink.
108        reported: usize,
109        /// Bytes offered to the sink.
110        offered: usize,
111        /// Exact bytes accepted before the invalid report.
112        committed: usize,
113    },
114    /// A validated internal output invariant failed.
115    Backend {
116        /// Internal backend failure classification.
117        fault: BackendFault,
118        /// Exact bytes accepted by successful prior calls.
119        committed: usize,
120    },
121}
122
123impl<E> CountedWriteError<E> {
124    /// Returns the exact committed byte count before failure.
125    #[must_use]
126    pub const fn committed(&self) -> usize {
127        match self {
128            Self::Encoding(_) => 0,
129            Self::Sink { committed, .. }
130            | Self::WriteZero { committed }
131            | Self::InvalidCount { committed, .. }
132            | Self::Backend { committed, .. } => *committed,
133        }
134    }
135}
136
137impl<E: core::fmt::Display> core::fmt::Display for CountedWriteError<E> {
138    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
139        match self {
140            Self::Encoding(error) => error.fmt(formatter),
141            Self::Sink { error, committed } => write!(
142                formatter,
143                "counted sink failed after {committed} committed Base64 bytes: {error}"
144            ),
145            Self::WriteZero { committed } => write!(
146                formatter,
147                "counted sink accepted zero bytes after {committed} committed Base64 bytes"
148            ),
149            Self::InvalidCount {
150                reported,
151                offered,
152                committed,
153            } => write!(
154                formatter,
155                "counted sink reported {reported} bytes for a {offered}-byte write after \
156                 {committed} committed Base64 bytes"
157            ),
158            Self::Backend { fault, committed } => write!(
159                formatter,
160                "Base64 backend {} failed after {committed} committed bytes",
161                fault.as_str()
162            ),
163        }
164    }
165}
166
167#[cfg(feature = "std")]
168impl<E: std::error::Error + 'static> std::error::Error for CountedWriteError<E> {
169    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
170        match self {
171            Self::Encoding(error) => Some(error),
172            Self::Sink { error, .. } => Some(error),
173            Self::WriteZero { .. } | Self::InvalidCount { .. } | Self::Backend { .. } => None,
174        }
175    }
176}
177
178/// Lazy allocation-free encoded display for one borrowed input.
179///
180/// This value owns copied validated codec settings and borrows only the input.
181/// Construct it with [`Base64::display`] so length errors are returned before
182/// formatting begins.
183#[derive(Clone, Copy)]
184pub struct EncodedDisplay<'a> {
185    settings: CodecSettings,
186    input: &'a [u8],
187}
188
189impl core::fmt::Debug for EncodedDisplay<'_> {
190    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
191        formatter
192            .debug_struct("EncodedDisplay")
193            .field("input_len", &self.input.len())
194            .finish_non_exhaustive()
195    }
196}
197
198impl core::fmt::Display for EncodedDisplay<'_> {
199    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
200        for chunk in EncodedChunks::new(self.settings, self.input) {
201            formatter.write_str(chunk_text(&chunk).map_err(|_| core::fmt::Error)?)?;
202        }
203        Ok(())
204    }
205}
206
207impl<S: Codec> Base64<S> {
208    /// Creates a lazy allocation-free display after encoding preflight.
209    pub fn display<'a>(&self, input: &'a [u8]) -> Result<EncodedDisplay<'a>, OneShotError> {
210        self.encoded_len(input.len())?;
211        Ok(EncodedDisplay {
212            settings: self.settings(),
213            input,
214        })
215    }
216
217    /// Encodes through `core::fmt::Write` without allocating.
218    ///
219    /// On formatter failure, confirmed progress excludes the failing call
220    /// because `fmt::Write` cannot report whether that call partially mutated
221    /// its sink.
222    pub fn encode_to_fmt<W: core::fmt::Write + ?Sized>(
223        &self,
224        input: &[u8],
225        writer: &mut W,
226    ) -> Result<usize, FormatWriteError> {
227        let chunks = self
228            .encoded_chunks(input)
229            .map_err(FormatWriteError::Encoding)?;
230        let mut confirmed = 0;
231        for chunk in chunks {
232            let text = chunk_text(&chunk).map_err(|_| FormatWriteError::Backend {
233                fault: BackendFault::ImpossibleState,
234                confirmed,
235            })?;
236            writer
237                .write_str(text)
238                .map_err(|_| FormatWriteError::Formatter { confirmed })?;
239            confirmed += text.len();
240        }
241        Ok(confirmed)
242    }
243
244    /// Encodes through an exact-progress counted sink without allocating.
245    pub fn encode_to_counted<W: CountedSink + ?Sized>(
246        &self,
247        input: &[u8],
248        writer: &mut W,
249    ) -> Result<usize, CountedWriteError<W::Error>> {
250        let chunks = self
251            .encoded_chunks(input)
252            .map_err(CountedWriteError::Encoding)?;
253        let mut committed = 0;
254        for chunk in chunks {
255            let mut pending = chunk.as_bytes();
256            while !pending.is_empty() {
257                let written = writer
258                    .write(pending)
259                    .map_err(|error| CountedWriteError::Sink { error, committed })?;
260                if written == 0 {
261                    return Err(CountedWriteError::WriteZero { committed });
262                }
263                if written > pending.len() {
264                    return Err(CountedWriteError::InvalidCount {
265                        reported: written,
266                        offered: pending.len(),
267                        committed,
268                    });
269                }
270                committed += written;
271                pending = &pending[written..];
272            }
273        }
274        Ok(committed)
275    }
276}
277
278fn chunk_text(chunk: &EncodedChunk) -> Result<&str, core::str::Utf8Error> {
279    chunk.as_str()
280}