Skip to main content

ic_host_artifacts/artifact/writer/
mod.rs

1//! Bound caller-owned output without choosing its encoding or publication policy.
2
3#[cfg(test)]
4mod tests;
5
6use std::{
7    fmt,
8    io::{self, Write},
9};
10
11/// A write rejected by the output boundary, carried inside [`io::Error`].
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum WriterError {
14    /// The complete offered buffer would exceed the caller's byte allowance.
15    LimitExceeded {
16        /// Maximum bytes permitted across successful writes.
17        limit: u64,
18    },
19    /// The underlying writer violated [`Write::write`]'s accepted-byte contract.
20    InvalidWriteCount {
21        /// Number of bytes offered to the underlying writer.
22        offered: usize,
23        /// Number of bytes it claimed to have accepted.
24        written: usize,
25    },
26}
27
28impl fmt::Display for WriterError {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::LimitExceeded { limit } => write!(f, "output exceeds {limit} bytes"),
32            Self::InvalidWriteCount { offered, written } => {
33                write!(
34                    f,
35                    "writer accepted {written} bytes from a {offered}-byte buffer"
36                )
37            }
38        }
39    }
40}
41
42impl std::error::Error for WriterError {}
43
44/// A byte allowance and successful-write counter around a caller-owned sink.
45///
46/// Oversized offered buffers are rejected in full before invoking the sink.
47/// Short writes count only accepted bytes. Read limits, encoding, hashing,
48/// synchronization, atomic publication and cleanup stay with their owners.
49/// A sink's internal buffering/allocation and partial effects on error are not
50/// controlled here. Do not publish partial output after a failed producer.
51///
52/// Use [`io::sink`] to count serialized bytes without retaining them, or a
53/// hashing dependency's existing `Write` implementation to bound its input.
54pub struct BoundedWriter<W> {
55    inner: W,
56    limit: u64,
57    bytes: u64,
58    exceeded: bool,
59}
60
61impl<W> BoundedWriter<W> {
62    /// Select the sink and the inclusive total accepted-byte allowance.
63    #[must_use]
64    pub const fn new(inner: W, limit: u64) -> Self {
65        Self {
66            inner,
67            limit,
68            bytes: 0,
69            exceeded: false,
70        }
71    }
72
73    /// Bytes reported as accepted by successful underlying writes.
74    ///
75    /// This is neither a durability receipt nor an assertion of serialization
76    /// success. Underlying errors may have effects not reported by `Write`.
77    #[must_use]
78    pub const fn bytes_written(&self) -> u64 {
79        self.bytes
80    }
81
82    /// Whether any offered buffer has been rejected by this byte allowance.
83    ///
84    /// Retained even when an encoder hides the underlying typed IO error.
85    /// A limit rejection leaves the sink and counter unchanged; later writes
86    /// that fit are permitted, without clearing this observation.
87    #[must_use]
88    pub const fn limit_exceeded(&self) -> bool {
89        self.exceeded
90    }
91
92    /// Recover the original sink, including any partial output after failure.
93    ///
94    /// Does not flush, synchronize, retry, publish or discard output. Callers
95    /// must explicitly flush through this wrapper when their sink requires it.
96    #[must_use]
97    pub fn into_inner(self) -> W {
98        self.inner
99    }
100}
101
102impl<W: Write> Write for BoundedWriter<W> {
103    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
104        if !u64::try_from(buffer.len()).is_ok_and(|length| length <= self.limit - self.bytes) {
105            self.exceeded = true;
106            return Err(io::Error::other(WriterError::LimitExceeded {
107                limit: self.limit,
108            }));
109        }
110        let written = self.inner.write(buffer)?;
111        if written > buffer.len() {
112            return Err(io::Error::new(
113                io::ErrorKind::InvalidData,
114                WriterError::InvalidWriteCount {
115                    offered: buffer.len(),
116                    written,
117                },
118            ));
119        }
120        // The admitted buffer length fits u64, and written never exceeds it.
121        self.bytes += written as u64;
122        Ok(written)
123    }
124
125    fn flush(&mut self) -> io::Result<()> {
126        self.inner.flush()
127    }
128}