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 pub(super) const fn get_ref(&self) -> &W {
93 &self.inner
94 }
95
96 /// Recover the original sink, including any partial output after failure.
97 ///
98 /// Does not flush, synchronize, retry, publish or discard output. Callers
99 /// must explicitly flush through this wrapper when their sink requires it.
100 #[must_use]
101 pub fn into_inner(self) -> W {
102 self.inner
103 }
104}
105
106impl<W: Write> Write for BoundedWriter<W> {
107 fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
108 if !u64::try_from(buffer.len()).is_ok_and(|length| length <= self.limit - self.bytes) {
109 self.exceeded = true;
110 return Err(io::Error::other(WriterError::LimitExceeded {
111 limit: self.limit,
112 }));
113 }
114 let written = self.inner.write(buffer)?;
115 if written > buffer.len() {
116 return Err(io::Error::new(
117 io::ErrorKind::InvalidData,
118 WriterError::InvalidWriteCount {
119 offered: buffer.len(),
120 written,
121 },
122 ));
123 }
124 // The admitted buffer length fits u64, and written never exceeds it.
125 self.bytes += written as u64;
126 Ok(written)
127 }
128
129 fn flush(&mut self) -> io::Result<()> {
130 self.inner.flush()
131 }
132}