qubit_io/ext/input_ext.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// qubit-style: allow coverage-cfg
9
10#[cfg(coverage)]
11use std::cell::Cell;
12use std::io::{
13 Error,
14 ErrorKind,
15 Result,
16};
17
18use crate::capacity_const::DEFAULT_BUFFER_CAPACITY;
19use crate::ext::output_ext::OutputExt;
20use crate::traits::validate_read_count;
21use crate::util::{
22 UncheckedSlice,
23 create_vec,
24 try_reserve_vec,
25};
26use crate::{
27 Input,
28 Output,
29};
30
31/// Extension methods for [`Input`] values.
32///
33/// `InputExt` keeps convenience and complete-read helpers outside the minimal
34/// [`Input`] trait. The methods are implemented for every item-oriented input,
35/// including `dyn Input` trait objects.
36pub trait InputExt: Input {
37 /// Reads exactly enough items to fill `output`.
38 ///
39 /// # Parameters
40 /// - `output`: Destination storage to fill.
41 ///
42 /// # Errors
43 /// Returns [`ErrorKind::UnexpectedEof`] when EOF is reached before the
44 /// output slice is full. Returns the first non-[`ErrorKind::Interrupted`]
45 /// error reported by the input. Interrupted reads are retried. Returns
46 /// [`ErrorKind::InvalidData`] if the input reports more items than
47 /// requested.
48 #[inline]
49 fn read_exact(&mut self, output: &mut [Self::Item]) -> Result<()> {
50 let read = self.read_exact_or_eof(output)?;
51 if read == output.len() {
52 Ok(())
53 } else {
54 Err(Error::new(
55 ErrorKind::UnexpectedEof,
56 "failed to fill whole buffer",
57 ))
58 }
59 }
60
61 /// Reads exactly `count` items into an indexed output range without
62 /// checking the range bounds in release builds.
63 ///
64 /// This method has the same blocking and error behavior as
65 /// [`InputExt::read_exact`], but writes into
66 /// `output[index..index + count]` using indexed unchecked reads.
67 ///
68 /// # Parameters
69 /// - `output`: Destination storage.
70 /// - `index`: Start index inside `output`.
71 /// - `count`: Number of items to read.
72 ///
73 /// # Errors
74 /// Returns [`ErrorKind::UnexpectedEof`] when EOF is reached before the
75 /// range is full. Returns the first non-[`ErrorKind::Interrupted`] error
76 /// reported by the input. Interrupted reads are retried. Returns
77 /// [`ErrorKind::InvalidData`] if the input reports more items than
78 /// requested.
79 ///
80 /// # Safety
81 /// The caller must guarantee that `index..index + count` is a valid range
82 /// inside `output` and that the addition does not overflow.
83 #[inline]
84 unsafe fn read_exact_unchecked(
85 &mut self,
86 output: &mut [Self::Item],
87 index: usize,
88 count: usize,
89 ) -> Result<()> {
90 let read =
91 unsafe { self.read_exact_or_eof_unchecked(output, index, count)? };
92 if read == count {
93 Ok(())
94 } else {
95 Err(Error::new(
96 ErrorKind::UnexpectedEof,
97 "failed to fill whole buffer",
98 ))
99 }
100 }
101
102 /// Reads items until `output` is full or EOF is reached.
103 ///
104 /// This method treats EOF as a successful partial result. It keeps retrying
105 /// short reads until the output slice is full, EOF is reached, or a
106 /// non-interrupted error occurs.
107 ///
108 /// # Parameters
109 /// - `output`: Destination storage to fill.
110 ///
111 /// # Returns
112 /// The number of items written to `output`.
113 ///
114 /// # Errors
115 /// Returns the first non-[`ErrorKind::Interrupted`] error reported by the
116 /// input. Interrupted reads are retried. Returns [`ErrorKind::InvalidData`]
117 /// if the input reports more items than requested.
118 fn read_exact_or_eof(
119 &mut self,
120 output: &mut [Self::Item],
121 ) -> Result<usize> {
122 let mut total = 0;
123 while total < output.len() {
124 let remaining = output.len() - total;
125 // SAFETY: `total..output.len()` is a valid suffix of `output`.
126 match unsafe { self.read_unchecked(output, total, remaining) } {
127 Ok(0) => break,
128 Ok(read) => {
129 validate_read_count(read, remaining)?;
130 total += read;
131 }
132 Err(error) if error.kind() == ErrorKind::Interrupted => {}
133 Err(error) => return Err(error),
134 }
135 }
136 Ok(total)
137 }
138
139 /// Reads items into an indexed output range until that range is full or
140 /// EOF is reached, without checking the range bounds in release builds.
141 ///
142 /// This method has the same EOF and retry behavior as
143 /// [`InputExt::read_exact_or_eof`], but writes into
144 /// `output[index..index + count]` using indexed unchecked reads.
145 ///
146 /// # Parameters
147 /// - `output`: Destination storage.
148 /// - `index`: Start index inside `output`.
149 /// - `count`: Number of items to try to read.
150 ///
151 /// # Returns
152 /// The number of items written into `output[index..index + count]`. The
153 /// value is in `0..=count`.
154 ///
155 /// # Errors
156 /// Returns the first non-[`ErrorKind::Interrupted`] error reported by the
157 /// input. Interrupted reads are retried. Returns [`ErrorKind::InvalidData`]
158 /// if the input reports more items than requested.
159 ///
160 /// # Safety
161 /// The caller must guarantee that `index..index + count` is a valid range
162 /// inside `output` and that the addition does not overflow.
163 unsafe fn read_exact_or_eof_unchecked(
164 &mut self,
165 output: &mut [Self::Item],
166 index: usize,
167 count: usize,
168 ) -> Result<usize> {
169 debug_assert!(
170 UncheckedSlice::range_fits(output.len(), index, count),
171 "unchecked read range exceeds output buffer"
172 );
173 let mut total = 0;
174 while total < count {
175 let remaining = count - total;
176 // SAFETY: The caller guarantees the original destination range is
177 // valid; `total < count`, so this suffix remains inside it.
178 match unsafe {
179 self.read_unchecked(output, index + total, remaining)
180 } {
181 Ok(0) => break,
182 Ok(read) => {
183 validate_read_count(read, remaining)?;
184 total += read;
185 }
186 Err(error) if error.kind() == ErrorKind::Interrupted => {}
187 Err(error) => return Err(error),
188 }
189 }
190 Ok(total)
191 }
192
193 /// Copies all remaining items from this input into `output`.
194 ///
195 /// The method allocates a reusable heap buffer and copies until EOF. It
196 /// does not close or flush the output.
197 ///
198 /// # Parameters
199 /// - `output`: Destination output.
200 ///
201 /// # Returns
202 /// The number of items copied.
203 ///
204 /// # Errors
205 /// Returns the first non-[`ErrorKind::Interrupted`] read error or output
206 /// error reported by the underlying streams. Interrupted reads are retried.
207 /// Returns [`ErrorKind::InvalidData`] if the input reports more items than
208 /// requested.
209 fn copy_to<O>(&mut self, output: &mut O) -> Result<u64>
210 where
211 O: Output<Item = Self::Item> + ?Sized,
212 Self::Item: Copy + Default,
213 {
214 let mut buffer =
215 create_vec(DEFAULT_BUFFER_CAPACITY, Self::Item::default())?;
216 let mut copied = 0_u64;
217 loop {
218 let requested = buffer.len();
219 let read = read_retrying_interrupted_limited(
220 self,
221 &mut buffer,
222 requested,
223 )?;
224 if read == 0 {
225 return Ok(copied);
226 }
227 // SAFETY: `read` has been validated against `buffer.len()`.
228 unsafe {
229 output.write_all_unchecked(&buffer, 0, read)?;
230 }
231 copied = add_copied(copied, read)?;
232 }
233 }
234
235 /// Copies at most `max_units` items from this input into `output`.
236 ///
237 /// This method stops successfully when either EOF is reached or
238 /// `max_units` items have been copied. It does not close or flush the
239 /// output.
240 ///
241 /// # Parameters
242 /// - `output`: Destination output.
243 /// - `max_units`: Maximum number of items to copy.
244 ///
245 /// # Returns
246 /// The number of items copied.
247 ///
248 /// # Errors
249 /// Returns the first non-[`ErrorKind::Interrupted`] read error or output
250 /// error reported by the underlying streams. Interrupted reads are retried.
251 /// Returns [`ErrorKind::InvalidData`] if the input reports more items than
252 /// requested.
253 fn copy_to_at_most<O>(
254 &mut self,
255 output: &mut O,
256 max_units: u64,
257 ) -> Result<u64>
258 where
259 O: Output<Item = Self::Item> + ?Sized,
260 Self::Item: Copy + Default,
261 {
262 if max_units == 0 {
263 return Ok(0);
264 }
265 let mut buffer =
266 create_vec(DEFAULT_BUFFER_CAPACITY, Self::Item::default())?;
267 let mut remaining = max_units;
268 let mut copied = 0_u64;
269 while remaining > 0 {
270 let requested = remaining.min(buffer.len() as u64) as usize;
271 let read = read_retrying_interrupted_limited(
272 self,
273 &mut buffer,
274 requested,
275 )?;
276 if read == 0 {
277 break;
278 }
279 // SAFETY: `read` has been validated against the requested prefix.
280 unsafe {
281 output.write_all_unchecked(&buffer, 0, read)?;
282 }
283 let read = read as u64;
284 remaining -= read;
285 copied += read;
286 }
287 Ok(copied)
288 }
289
290 /// Copies the remaining input if its total length is at most `max_units`.
291 ///
292 /// This method copies from the current input position until EOF. If EOF is
293 /// not reached within `max_units` items, it returns
294 /// [`ErrorKind::InvalidData`]. Detecting oversized input consumes one
295 /// excess item from this input; that excess item is not written to
296 /// `output`.
297 ///
298 /// Oversized input, read errors, and allocation failures before output
299 /// flushing leave `output` unchanged. Once EOF is reached and collected
300 /// items are written to `output`, a write error may leave partial items
301 /// accepted by `output` because [`Output`] has no rollback operation.
302 ///
303 /// # Parameters
304 /// - `output`: Destination output.
305 /// - `max_units`: Maximum accepted number of remaining input items.
306 ///
307 /// # Returns
308 /// The number of items copied when EOF is reached within the limit.
309 ///
310 /// # Errors
311 /// Returns [`ErrorKind::InvalidData`] when the remaining input is longer
312 /// than `max_units`. Returns the first non-[`ErrorKind::Interrupted`] read
313 /// error or output error reported by the underlying streams. Interrupted
314 /// reads are retried. Returns [`ErrorKind::InvalidData`] if the input
315 /// reports more items than requested.
316 fn copy_to_end_limited<O>(
317 &mut self,
318 output: &mut O,
319 max_units: u64,
320 ) -> Result<u64>
321 where
322 O: Output<Item = Self::Item> + ?Sized,
323 Self::Item: Copy + Default,
324 {
325 let mut buffer =
326 create_vec(DEFAULT_BUFFER_CAPACITY, Self::Item::default())?;
327 let mut collected = Vec::new();
328 let mut remaining = max_units;
329 let mut copied = 0_u64;
330 loop {
331 let requested =
332 remaining.saturating_add(1).min(buffer.len() as u64) as usize;
333 let read = read_retrying_interrupted_limited(
334 self,
335 &mut buffer,
336 requested,
337 )?;
338 if read == 0 {
339 if !collected.is_empty() {
340 // SAFETY: `collected` contains exactly the items validated
341 // below.
342 unsafe {
343 output.write_all_unchecked(
344 &collected,
345 0,
346 collected.len(),
347 )?;
348 }
349 }
350 return Ok(copied);
351 }
352 if (read as u64) > remaining {
353 return Err(Error::new(
354 ErrorKind::InvalidData,
355 format!(
356 "input exceeds maximum length of {max_units} items"
357 ),
358 ));
359 }
360 try_reserve_vec(&mut collected, read)?;
361 collected.extend_from_slice(&buffer[..read]);
362 let read = read as u64;
363 remaining -= read;
364 copied = add_copied(copied, read as usize)?;
365 }
366 }
367}
368
369impl<T> InputExt for T where T: Input + ?Sized {}
370
371/// Reads into a buffer prefix while retrying interrupted reads.
372///
373/// # Parameters
374/// - `input`: Source input.
375/// - `buffer`: Destination buffer.
376/// - `requested`: Number of items to request.
377///
378/// # Returns
379/// The number of items read.
380///
381/// # Errors
382/// Returns the first non-interrupted input error. Returns
383/// [`ErrorKind::InvalidData`] if the input reports more items than requested.
384fn read_retrying_interrupted_limited<I>(
385 input: &mut I,
386 buffer: &mut [I::Item],
387 requested: usize,
388) -> Result<usize>
389where
390 I: Input + ?Sized,
391{
392 loop {
393 // SAFETY: Callers pass a valid prefix length within `buffer`.
394 match unsafe { input.read_unchecked(buffer, 0, requested) } {
395 Ok(read) => {
396 validate_read_count(read, requested)?;
397 return Ok(read);
398 }
399 Err(error) if error.kind() == ErrorKind::Interrupted => {}
400 Err(error) => return Err(error),
401 }
402 }
403}
404
405#[cfg(coverage)]
406thread_local! {
407 static COVERAGE_FAIL_NEXT_ADD_COPIED: Cell<bool> = const { Cell::new(false) };
408}
409
410/// Makes the next [`add_copied`] call fail.
411///
412/// Coverage-only helper for exercising overflow propagation inside copy loops.
413#[cfg(coverage)]
414#[doc(hidden)]
415pub fn coverage_fail_next_add_copied() {
416 COVERAGE_FAIL_NEXT_ADD_COPIED.with(|state| state.set(true));
417}
418
419/// Clears coverage-only add_copied hooks between tests.
420#[cfg(coverage)]
421#[doc(hidden)]
422pub fn coverage_reset_add_copied_hooks() {
423 COVERAGE_FAIL_NEXT_ADD_COPIED.with(|state| state.set(false));
424}
425
426#[cfg(coverage)]
427#[doc(hidden)]
428/// Triggers natural add_copied overflow for coverage assertions.
429pub fn coverage_natural_add_copied_overflow() -> Result<u64> {
430 add_copied(u64::MAX, 1)
431}
432
433/// Adds a copied item count to an accumulated total.
434///
435/// # Parameters
436/// - `copied`: Existing copied item count.
437/// - `read`: Newly copied item count.
438///
439/// # Returns
440/// The updated copied item count.
441///
442/// # Errors
443/// Returns [`ErrorKind::InvalidData`] if the count overflows `u64`.
444#[inline(always)]
445fn add_copied(copied: u64, read: usize) -> Result<u64> {
446 #[cfg(coverage)]
447 if COVERAGE_FAIL_NEXT_ADD_COPIED.with(|state| {
448 let fail = state.get();
449 if fail {
450 state.set(false);
451 }
452 fail
453 }) {
454 return Err(Error::new(
455 ErrorKind::InvalidData,
456 "copied item count overflows u64",
457 ));
458 }
459 copied.checked_add(read as u64).ok_or_else(|| {
460 Error::new(ErrorKind::InvalidData, "copied item count overflows u64")
461 })
462}