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}