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}