qubit_io/wrappers/async_checksum_output.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8
9use std::hash::Hasher;
10use std::io;
11use std::pin::Pin;
12use std::task::Context;
13use std::task::Poll;
14
15use crate::AsyncClose;
16use crate::AsyncOutput;
17use crate::traits::normalize_async_error;
18
19/// Asynchronous byte output that hashes successfully accepted bytes.
20///
21/// Pending and failed writes do not change the hasher. The checksum algorithm
22/// and stability guarantees are those of the supplied [`Hasher`].
23///
24/// # Type Parameters
25///
26/// - `O`: Wrapped asynchronous byte output type.
27/// - `H`: Checksum hasher type.
28#[must_use]
29#[derive(Debug)]
30pub struct AsyncChecksumOutput<O, H> {
31 /// Output whose successful writes are hashed.
32 inner: O,
33 /// Hasher tracking accepted bytes.
34 hasher: H,
35}
36
37impl<O, H> AsyncClose for AsyncChecksumOutput<O, H>
38where
39 O: AsyncClose<Item = u8>,
40 H: Hasher,
41{
42 /// Polls closing through the wrapped output.
43 ///
44 /// # Parameters
45 ///
46 /// - `cx`: Task context used to register a wake-up.
47 ///
48 /// # Returns
49 ///
50 /// Returns [`Poll::Pending`] while closing is incomplete, otherwise a
51 /// ready success result.
52 ///
53 /// # Errors
54 ///
55 /// Returns an error reported by the wrapped output. Invalid asynchronous
56 /// error kinds are normalized to [`io::ErrorKind::InvalidData`].
57 #[inline(always)]
58 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
59 // SAFETY: `inner` is never moved while projecting this pinned wrapper.
60 let this = unsafe { self.get_unchecked_mut() };
61 // SAFETY: The pinned wrapper keeps `inner` at a stable address.
62 unsafe { Pin::new_unchecked(&mut this.inner) }
63 .poll_close(cx)
64 .map(|result| result.map_err(normalize_async_error))
65 }
66}
67
68impl<O, H> AsyncChecksumOutput<O, H>
69where
70 H: Hasher,
71{
72 /// Creates a checksum-tracking asynchronous output.
73 ///
74 /// # Parameters
75 ///
76 /// - `inner`: Asynchronous byte output to wrap.
77 /// - `hasher`: Hasher updated after successful writes.
78 ///
79 /// # Returns
80 ///
81 /// Returns an output with the supplied initial hasher state.
82 #[inline(always)]
83 pub const fn new(inner: O, hasher: H) -> Self {
84 Self { inner, hasher }
85 }
86
87 /// Returns the current checksum value.
88 ///
89 /// # Returns
90 ///
91 /// Returns [`Hasher::finish`] for the current state.
92 #[inline(always)]
93 #[must_use]
94 pub fn checksum(&self) -> u64 {
95 self.hasher.finish()
96 }
97
98 /// Returns a shared reference to the wrapped output.
99 ///
100 /// # Returns
101 ///
102 /// Returns the wrapped asynchronous byte output.
103 #[inline(always)]
104 #[must_use]
105 pub const fn inner(&self) -> &O {
106 &self.inner
107 }
108
109 /// Returns a mutable reference to the wrapped output.
110 ///
111 /// Writes performed directly on the returned output do not update this
112 /// wrapper's hasher.
113 ///
114 /// # Returns
115 ///
116 /// Returns the wrapped asynchronous byte output.
117 #[inline(always)]
118 #[must_use]
119 pub fn inner_mut(&mut self) -> &mut O {
120 &mut self.inner
121 }
122
123 /// Returns a shared reference to the hasher.
124 ///
125 /// # Returns
126 ///
127 /// Returns the current hasher state.
128 #[inline(always)]
129 #[must_use]
130 pub const fn hasher(&self) -> &H {
131 &self.hasher
132 }
133
134 /// Returns a mutable reference to the hasher.
135 ///
136 /// Mutating the returned hasher changes the checksum independently of
137 /// bytes written through this wrapper.
138 ///
139 /// # Returns
140 ///
141 /// Returns the current hasher state mutably.
142 #[inline(always)]
143 #[must_use]
144 pub fn hasher_mut(&mut self) -> &mut H {
145 &mut self.hasher
146 }
147
148 /// Consumes this wrapper without flushing the wrapped output.
149 ///
150 /// This method does not call [`AsyncOutput::flush_async`] and performs no
151 /// asynchronous I/O. Any buffering owned by the returned output remains
152 /// pending and unchanged.
153 ///
154 /// # Returns
155 ///
156 /// Returns the wrapped output and final hasher state.
157 #[inline(always)]
158 #[must_use]
159 pub fn into_parts(self) -> (O, H) {
160 (self.inner, self.hasher)
161 }
162}
163
164impl<O, H> AsyncOutput for AsyncChecksumOutput<O, H>
165where
166 O: AsyncOutput<Item = u8>,
167 H: Hasher,
168{
169 /// Byte item hashed after successful writes.
170 type Item = u8;
171
172 /// Preserves the wrapped output's buffering declaration.
173 ///
174 /// # Returns
175 ///
176 /// Returns the wrapped output's buffering declaration.
177 #[inline(always)]
178 fn is_buffered(&self) -> bool {
179 self.inner.is_buffered()
180 }
181
182 /// Polls a write and hashes only bytes in a successful ready result.
183 ///
184 /// # Parameters
185 ///
186 /// - `cx`: Task context used to register a wake-up.
187 /// - `input`: Source byte slice.
188 /// - `index`: Starting source index.
189 /// - `count`: Maximum number of bytes to write.
190 ///
191 /// # Returns
192 ///
193 /// Returns [`Poll::Pending`] when the output is not ready. A ready success
194 /// contains the number of bytes accepted and hashed.
195 ///
196 /// # Errors
197 ///
198 /// Returns an I/O error reported by the wrapped output without changing the
199 /// hasher.
200 ///
201 /// # Safety
202 ///
203 /// The range `index..index + count` must be valid for `input`.
204 unsafe fn poll_write_unchecked(
205 mut self: Pin<&mut Self>,
206 cx: &mut Context<'_>,
207 input: &[u8],
208 index: usize,
209 count: usize,
210 ) -> Poll<io::Result<usize>> {
211 // SAFETY: `inner` is never moved while projecting this pinned wrapper.
212 let this = unsafe { self.as_mut().get_unchecked_mut() };
213 let source = &input[index..index + count];
214 // SAFETY: The pinned wrapper keeps `inner` at a stable address.
215 let inner = unsafe { Pin::new_unchecked(&mut this.inner) };
216 match inner.poll_write(cx, source) {
217 Poll::Ready(Ok(written)) => {
218 this.hasher.write(&input[index..index + written]);
219 Poll::Ready(Ok(written))
220 }
221 Poll::Ready(Err(error)) => Poll::Ready(Err(error)),
222 Poll::Pending => Poll::Pending,
223 }
224 }
225
226 /// Polls the wrapped output's flush operation.
227 ///
228 /// # Parameters
229 ///
230 /// - `cx`: Task context used to register a wake-up.
231 ///
232 /// # Returns
233 ///
234 /// Returns [`Poll::Pending`] while flushing is incomplete, otherwise a
235 /// ready success result.
236 ///
237 /// # Errors
238 ///
239 /// Returns an error reported by the wrapped output. Invalid asynchronous
240 /// error kinds are normalized to [`io::ErrorKind::InvalidData`].
241 #[inline(always)]
242 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
243 // SAFETY: `inner` is never moved while projecting this pinned wrapper.
244 let this = unsafe { self.get_unchecked_mut() };
245 // SAFETY: The pinned wrapper keeps `inner` at a stable address.
246 unsafe { Pin::new_unchecked(&mut this.inner) }
247 .poll_flush(cx)
248 .map(|result| result.map_err(normalize_async_error))
249 }
250}