Skip to main content

qubit_io/util/
streams.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#[cfg(coverage)]
10use std::cell::Cell;
11use std::cmp::Ordering;
12use std::io::{
13    Error,
14    ErrorKind,
15    Read,
16    Result,
17    Write,
18};
19
20use crate::ReadExt;
21use crate::capacity_const::{
22    DEFAULT_BUFFER_CAPACITY,
23    DEFAULT_COMPARE_BUFFER_SIZE,
24    DEFAULT_COPY_BUFFER_SIZE,
25};
26use crate::traits::validate_read_count;
27use crate::util::{
28    create_vec,
29    try_reserve_vec,
30};
31use crate::{
32    Input,
33    Output,
34};
35
36/// Stream utility namespace.
37///
38/// This type is an uninstantiable namespace for operations involving one or
39/// more [`Read`] or [`Write`] values. The methods do not close or flush the
40/// supplied streams unless the underlying standard-library operation documents
41/// otherwise.
42///
43/// # Examples
44/// ```
45/// use qubit_io::Streams;
46/// use std::io::Cursor;
47///
48/// let mut input = Cursor::new(b"abcdef".to_vec());
49/// let mut output = Vec::new();
50///
51/// let copied = Streams::copy_at_most(&mut input, &mut output, 4)?;
52///
53/// assert_eq!(4, copied);
54/// assert_eq!(b"abcd", output.as_slice());
55/// # Ok::<(), std::io::Error>(())
56/// ```
57pub enum Streams {}
58
59impl Streams {
60    /// Copies all remaining bytes from `reader` to `writer`.
61    ///
62    /// This is a namespace-style wrapper around [`std::io::copy`]. It preserves
63    /// the standard-library behavior, including platform-specific optimized
64    /// copy paths when available.
65    ///
66    /// # Parameters
67    /// - `reader`: Source reader.
68    /// - `writer`: Destination writer.
69    ///
70    /// # Returns
71    /// The number of bytes copied.
72    ///
73    /// # Errors
74    /// Returns the first read or write error reported by the underlying
75    /// streams, using the same error behavior as [`std::io::copy`].
76    #[inline]
77    pub fn copy<R, W>(reader: &mut R, writer: &mut W) -> Result<u64>
78    where
79        R: Read + ?Sized,
80        W: Write + ?Sized,
81    {
82        std::io::copy(reader, writer)
83    }
84
85    /// Copies at most `max_bytes` bytes from `reader` to `writer`.
86    ///
87    /// This method stops successfully when either EOF is reached or
88    /// `max_bytes` bytes have been copied. It does not close or flush either
89    /// stream.
90    ///
91    /// # Parameters
92    /// - `reader`: Source reader.
93    /// - `writer`: Destination writer.
94    /// - `max_bytes`: Maximum number of bytes to copy.
95    ///
96    /// # Returns
97    /// The number of bytes copied.
98    ///
99    /// # Errors
100    /// Returns the first non-interrupted read error or write error reported by
101    /// the underlying streams. Interrupted reads are retried.
102    #[inline]
103    pub fn copy_at_most<R, W>(
104        reader: &mut R,
105        writer: &mut W,
106        max_bytes: u64,
107    ) -> Result<u64>
108    where
109        R: Read + ?Sized,
110        W: Write + ?Sized,
111    {
112        let mut reader = reader;
113        let mut writer = writer;
114        copy_at_most_impl(
115            &mut reader,
116            &mut writer,
117            max_bytes,
118            DEFAULT_COPY_BUFFER_SIZE,
119        )
120    }
121
122    /// Copies at most `max_bytes` bytes from `reader` to `writer` using a
123    /// caller-selected heap buffer.
124    ///
125    /// This method has the same copy semantics as [`Self::copy_at_most`], but
126    /// allocates a buffer on the heap with `buffer_size` bytes. Use it when
127    /// the default chunk size is too large for the caller's stack budget or
128    /// when a smaller copy window is desirable.
129    ///
130    /// # Parameters
131    /// - `reader`: Source reader.
132    /// - `writer`: Destination writer.
133    /// - `max_bytes`: Maximum number of bytes to copy.
134    /// - `buffer_size`: Number of bytes in the copy buffer.
135    ///
136    /// # Returns
137    /// The number of bytes copied.
138    ///
139    /// # Errors
140    /// Returns [`ErrorKind::InvalidInput`] when `buffer_size == 0`. Returns an
141    /// allocation error if the copy buffer cannot be allocated. Returns the
142    /// first non-interrupted read error or write error reported by the
143    /// underlying streams. Interrupted reads are retried.
144    #[inline]
145    pub fn copy_at_most_with_buffer_size<R, W>(
146        reader: &mut R,
147        writer: &mut W,
148        max_bytes: u64,
149        buffer_size: usize,
150    ) -> Result<u64>
151    where
152        R: Read + ?Sized,
153        W: Write + ?Sized,
154    {
155        let mut reader = reader;
156        let mut writer = writer;
157        copy_at_most_impl(&mut reader, &mut writer, max_bytes, buffer_size)
158    }
159
160    /// Copies the remaining input if its total length is at most `max_bytes`.
161    ///
162    /// This method copies from the current reader position until EOF. If EOF is
163    /// not reached within `max_bytes` bytes, it returns
164    /// [`std::io::ErrorKind::InvalidData`]. Detecting oversized input consumes
165    /// one excess byte from `reader`; that excess byte is not written to
166    /// `writer`.
167    ///
168    /// Unlike bounded reads into in-memory collections, this method cannot roll
169    /// back bytes already accepted by `writer` when the limit is exceeded
170    /// because [`Write`] does not provide truncation. On
171    /// [`std::io::ErrorKind::InvalidData`], up to `max_bytes` bytes may remain
172    /// in `writer`.
173    ///
174    /// # Parameters
175    /// - `reader`: Source reader.
176    /// - `writer`: Destination writer.
177    /// - `max_bytes`: Maximum accepted number of bytes in the remaining input.
178    ///
179    /// # Returns
180    /// The number of bytes copied when EOF is reached within the limit.
181    ///
182    /// # Errors
183    /// Returns [`std::io::ErrorKind::InvalidData`] when the remaining input is
184    /// longer than `max_bytes`. Returns the first non-interrupted read error or
185    /// write error reported by the underlying streams. Interrupted reads are
186    /// retried.
187    #[inline]
188    pub fn copy_to_end_limited<R, W>(
189        reader: &mut R,
190        writer: &mut W,
191        max_bytes: u64,
192    ) -> Result<u64>
193    where
194        R: Read + ?Sized,
195        W: Write + ?Sized,
196    {
197        let mut reader = reader;
198        let mut writer = writer;
199        let copied = copy_at_most_impl(
200            &mut reader,
201            &mut writer,
202            max_bytes,
203            DEFAULT_COPY_BUFFER_SIZE,
204        )?;
205        if copied < max_bytes {
206            return Ok(copied);
207        }
208        let mut byte = [0];
209        loop {
210            match reader.read(&mut byte) {
211                Ok(0) => return Ok(copied),
212                Ok(_) => {
213                    return Err(Error::new(
214                        ErrorKind::InvalidData,
215                        format!(
216                            "input exceeds maximum length of {max_bytes} bytes"
217                        ),
218                    ));
219                }
220                Err(error) => {
221                    if error.kind() == ErrorKind::Interrupted {
222                        continue;
223                    }
224                    return Err(error);
225                }
226            }
227        }
228    }
229
230    /// Copies all remaining items from `input` to `output`.
231    ///
232    /// This method allocates a reusable item buffer and copies until EOF. It
233    /// does not close or flush `output`.
234    ///
235    /// # Parameters
236    /// - `input`: Source item input.
237    /// - `output`: Destination item output.
238    ///
239    /// # Returns
240    /// The number of items copied.
241    ///
242    /// # Errors
243    /// Returns the first non-interrupted read error or output error reported by
244    /// the underlying streams. Returns [`ErrorKind::InvalidData`] if an input
245    /// or output reports an impossible item count.
246    pub fn copy_input_to_output<I, O>(
247        input: &mut I,
248        output: &mut O,
249    ) -> Result<u64>
250    where
251        I: Input + ?Sized,
252        O: Output<Item = I::Item> + ?Sized,
253        I::Item: Copy + Default,
254    {
255        let mut buffer =
256            create_vec(DEFAULT_BUFFER_CAPACITY, I::Item::default())?;
257        let mut copied = 0_u64;
258        loop {
259            let read = input.read_fully(&mut buffer)?;
260            validate_read_count(read, buffer.len())?;
261            if read == 0 {
262                return Ok(copied);
263            }
264            // SAFETY: `read` is bounded by `buffer.len()`.
265            unsafe {
266                output.write_fully_unchecked(&buffer, 0, read)?;
267            }
268            copied = add_item_count(copied, read)?;
269        }
270    }
271
272    /// Copies at most `max_items` items from `input` to `output`.
273    ///
274    /// This method stops successfully when either EOF is reached or `max_items`
275    /// items have been copied. It does not close or flush `output`.
276    ///
277    /// # Parameters
278    /// - `input`: Source item input.
279    /// - `output`: Destination item output.
280    /// - `max_items`: Maximum number of items to copy.
281    ///
282    /// # Returns
283    /// The number of items copied.
284    ///
285    /// # Errors
286    /// Returns the first non-interrupted read error or output error reported by
287    /// the underlying streams. Returns [`ErrorKind::InvalidData`] if an input
288    /// or output reports an impossible item count.
289    pub fn copy_input_to_output_at_most<I, O>(
290        input: &mut I,
291        output: &mut O,
292        max_items: u64,
293    ) -> Result<u64>
294    where
295        I: Input + ?Sized,
296        O: Output<Item = I::Item> + ?Sized,
297        I::Item: Copy + Default,
298    {
299        if max_items == 0 {
300            return Ok(0);
301        }
302        let mut buffer =
303            create_vec(DEFAULT_BUFFER_CAPACITY, I::Item::default())?;
304        let mut remaining = max_items;
305        let mut copied = 0_u64;
306        while remaining > 0 {
307            let requested = remaining.min(buffer.len() as u64) as usize;
308            // SAFETY: `requested` is a valid prefix length inside `buffer`.
309            let read = unsafe {
310                input.read_fully_unchecked(&mut buffer, 0, requested)?
311            };
312            validate_read_count(read, requested)?;
313            if read == 0 {
314                break;
315            }
316            // SAFETY: `read` is bounded by the requested prefix.
317            unsafe {
318                output.write_fully_unchecked(&buffer, 0, read)?;
319            }
320            let read = read as u64;
321            remaining -= read;
322            copied = add_item_count(copied, read as usize)?;
323        }
324        Ok(copied)
325    }
326
327    /// Copies the remaining input if its total length is at most `max_items`.
328    ///
329    /// This method copies from the current input position until EOF. If EOF is
330    /// not reached within `max_items` items, it returns
331    /// [`ErrorKind::InvalidData`]. Detecting oversized input consumes one
332    /// excess item from `input`; that excess item is not written to
333    /// `output`.
334    ///
335    /// Oversized input, read errors, and allocation failures before output
336    /// flushing leave `output` unchanged. Once EOF is reached and collected
337    /// items are written to `output`, a write error may leave partial items
338    /// accepted by `output` because [`Output`] has no rollback operation.
339    ///
340    /// # Parameters
341    /// - `input`: Source item input.
342    /// - `output`: Destination item output.
343    /// - `max_items`: Maximum accepted number of remaining input items.
344    ///
345    /// # Returns
346    /// The number of items copied when EOF is reached within the limit.
347    ///
348    /// # Errors
349    /// Returns [`ErrorKind::InvalidData`] when the remaining input is longer
350    /// than `max_items`, or when an input reports an impossible item count.
351    /// Returns the first non-interrupted read error or output error reported by
352    /// the underlying streams.
353    pub fn copy_input_to_output_end_limited<I, O>(
354        input: &mut I,
355        output: &mut O,
356        max_items: u64,
357    ) -> Result<u64>
358    where
359        I: Input + ?Sized,
360        O: Output<Item = I::Item> + ?Sized,
361        I::Item: Copy + Default,
362    {
363        let mut buffer =
364            create_vec(DEFAULT_BUFFER_CAPACITY, I::Item::default())?;
365        let mut collected = Vec::new();
366        let mut remaining = max_items;
367        let mut copied = 0_u64;
368        loop {
369            let requested =
370                remaining.saturating_add(1).min(buffer.len() as u64) as usize;
371            // SAFETY: `requested` is a valid prefix length inside `buffer`.
372            let read = unsafe {
373                input.read_fully_unchecked(&mut buffer, 0, requested)?
374            };
375            validate_read_count(read, requested)?;
376            if read == 0 {
377                let count = collected.len();
378                if count == 0 {
379                    return Ok(copied);
380                }
381                // SAFETY: The full collected range is valid.
382                unsafe {
383                    output.write_fully_unchecked(&collected, 0, count)?;
384                }
385                return Ok(copied);
386            }
387            if (read as u64) > remaining {
388                return Err(Error::new(
389                    ErrorKind::InvalidData,
390                    format!(
391                        "input exceeds maximum length of {max_items} items"
392                    ),
393                ));
394            }
395            try_reserve_vec(&mut collected, read)?;
396            collected.extend_from_slice(&buffer[..read]);
397            let read = read as u64;
398            remaining -= read;
399            copied = add_item_count(copied, read as usize)?;
400        }
401    }
402
403    /// Tests whether two readable streams have equal remaining contents.
404    ///
405    /// The comparison starts at each reader's current position and reads both
406    /// streams in fixed-size chunks. A mismatch stops comparison immediately
407    /// after the differing chunks are read, so each reader may have advanced
408    /// past the first differing byte within that chunk.
409    ///
410    /// # Parameters
411    /// - `left`: First stream.
412    /// - `right`: Second stream.
413    ///
414    /// # Returns
415    /// `true` when both streams produce the same bytes until EOF.
416    ///
417    /// # Errors
418    /// Returns the first read error reported by either stream.
419    #[inline]
420    pub fn content_eq(
421        left: &mut dyn Read,
422        right: &mut dyn Read,
423    ) -> Result<bool> {
424        Ok(Self::compare_content(left, right)? == Ordering::Equal)
425    }
426
427    /// Lexicographically compares the remaining contents of two readable
428    /// streams.
429    ///
430    /// The comparison starts at each reader's current position and reads both
431    /// streams in fixed-size chunks. A mismatch stops comparison immediately
432    /// after the differing chunks are read, so each reader may have advanced
433    /// past the first differing byte within that chunk.
434    ///
435    /// # Parameters
436    /// - `left`: First stream.
437    /// - `right`: Second stream.
438    ///
439    /// # Returns
440    /// The lexicographic ordering of the remaining bytes.
441    ///
442    /// # Errors
443    /// Returns the first read error reported by either stream.
444    pub fn compare_content(
445        left: &mut dyn Read,
446        right: &mut dyn Read,
447    ) -> Result<Ordering> {
448        Self::compare_content_with_buffer_size(
449            left,
450            right,
451            DEFAULT_COMPARE_BUFFER_SIZE,
452        )
453    }
454
455    /// Lexicographically compares the remaining contents of two readable
456    /// streams using caller-selected heap buffers.
457    ///
458    /// This method has the same comparison and stream-advance semantics as
459    /// [`Self::compare_content`], but allocates two buffers on the heap with
460    /// `buffer_size` bytes each. Use it when the default chunk size is too
461    /// large for the caller's stack budget or when a smaller comparison window
462    /// is desirable.
463    ///
464    /// # Parameters
465    /// - `left`: First stream.
466    /// - `right`: Second stream.
467    /// - `buffer_size`: Number of bytes in each comparison buffer.
468    ///
469    /// # Returns
470    /// The lexicographic ordering of the remaining bytes.
471    ///
472    /// # Errors
473    /// Returns [`ErrorKind::InvalidInput`] when `buffer_size == 0`. Returns an
474    /// allocation error if the comparison buffers cannot be allocated. Returns
475    /// the first read error reported by either stream.
476    pub fn compare_content_with_buffer_size(
477        left: &mut dyn Read,
478        right: &mut dyn Read,
479        buffer_size: usize,
480    ) -> Result<Ordering> {
481        if buffer_size == 0 {
482            return Err(Error::new(
483                ErrorKind::InvalidInput,
484                "compare buffer size must be greater than zero",
485            ));
486        }
487        let mut left_buffer = create_vec(buffer_size, 0)?;
488        let mut right_buffer = create_vec(buffer_size, 0)?;
489        debug_assert_eq!(
490            left_buffer.len(),
491            right_buffer.len(),
492            "compare buffers must have identical lengths",
493        );
494        debug_assert!(
495            !left_buffer.is_empty(),
496            "compare buffers must not be empty",
497        );
498        loop {
499            let left_count = left.read_exact_or_eof(&mut left_buffer)?;
500            let right_count = right.read_exact_or_eof(&mut right_buffer)?;
501            let n = left_count.min(right_count);
502            for index in 0..n {
503                match left_buffer[index].cmp(&right_buffer[index]) {
504                    Ordering::Equal => {}
505                    ordering => return Ok(ordering),
506                }
507            }
508            match left_count.cmp(&right_count) {
509                Ordering::Equal if left_count == 0 => {
510                    return Ok(Ordering::Equal);
511                }
512                Ordering::Equal => {}
513                ordering => return Ok(ordering),
514            }
515        }
516    }
517}
518
519/// Copies at most `max_bytes` bytes using trait-object I/O endpoints.
520///
521/// # Parameters
522/// - `reader`: Source reader.
523/// - `writer`: Destination writer.
524/// - `max_bytes`: Maximum number of bytes to copy.
525/// - `buffer_size`: Number of bytes in the copy buffer.
526///
527/// # Returns
528/// The number of bytes copied.
529///
530/// # Errors
531/// Returns [`ErrorKind::InvalidInput`] when `buffer_size == 0`. Returns an
532/// allocation error if the copy buffer cannot be allocated. Returns the first
533/// non-interrupted read error or write error reported by the underlying
534/// streams. Interrupted reads are retried.
535fn copy_at_most_impl(
536    reader: &mut dyn Read,
537    writer: &mut dyn Write,
538    max_bytes: u64,
539    buffer_size: usize,
540) -> Result<u64> {
541    if buffer_size == 0 {
542        return Err(Error::new(
543            ErrorKind::InvalidInput,
544            "copy buffer size must be greater than zero",
545        ));
546    }
547    let mut buffer = create_vec(buffer_size, 0)?;
548    let mut remaining = max_bytes;
549    let mut copied = 0;
550    while remaining > 0 {
551        let requested = remaining.min(buffer_size as u64) as usize;
552        match reader.read(&mut buffer[..requested]) {
553            Ok(0) => break,
554            Ok(count) => {
555                writer.write_all(&buffer[..count])?;
556                let count = count as u64;
557                remaining -= count;
558                copied += count;
559            }
560            Err(error) => {
561                if error.kind() == ErrorKind::Interrupted {
562                    continue;
563                }
564                return Err(error);
565            }
566        }
567    }
568    Ok(copied)
569}
570
571#[cfg(coverage)]
572thread_local! {
573    static COVERAGE_FAIL_NEXT_ADD_ITEM_COUNT: Cell<bool> = const { Cell::new(false) };
574}
575
576/// Makes the next [`add_item_count`] call fail.
577///
578/// Coverage-only helper for exercising overflow propagation inside copy loops.
579#[cfg(coverage)]
580#[doc(hidden)]
581pub fn coverage_fail_next_add_item_count() {
582    COVERAGE_FAIL_NEXT_ADD_ITEM_COUNT.with(|state| state.set(true));
583}
584
585/// Clears coverage-only [`add_item_count`] hooks between tests.
586#[cfg(coverage)]
587#[doc(hidden)]
588pub fn coverage_reset_add_item_count_hooks() {
589    COVERAGE_FAIL_NEXT_ADD_ITEM_COUNT.with(|state| state.set(false));
590}
591
592/// Adds a copied item count to an accumulated total.
593///
594/// # Parameters
595/// - `copied`: Existing copied item count.
596/// - `count`: Newly copied item count.
597///
598/// # Returns
599/// The updated copied item count.
600///
601/// # Errors
602/// Returns [`ErrorKind::InvalidData`] if the count overflows `u64`.
603#[inline(always)]
604fn add_item_count(copied: u64, count: usize) -> Result<u64> {
605    #[cfg(coverage)]
606    if COVERAGE_FAIL_NEXT_ADD_ITEM_COUNT.with(|state| {
607        let fail = state.get();
608        if fail {
609            state.set(false);
610        }
611        fail
612    }) {
613        return Err(Error::new(
614            ErrorKind::InvalidData,
615            "copied item count overflows u64",
616        ));
617    }
618    copied.checked_add(count as u64).ok_or_else(|| {
619        Error::new(ErrorKind::InvalidData, "copied item count overflows u64")
620    })
621}
622
623/// Exercises the copied-item overflow branch in coverage builds.
624#[cfg(coverage)]
625#[doc(hidden)]
626pub fn coverage_add_item_count_overflow() -> Result<u64> {
627    add_item_count(u64::MAX, 1)
628}