From 4b4564207a9a7f6246af263ca56e52ce0eef03d8 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 31 Mar 2025 23:33:07 -0600 Subject: [PATCH] Documentation updates --- README.md | 116 +++++++++++++++++++++++++++++++++++------ nod/src/disc/writer.rs | 10 +--- nod/src/io/ciso.rs | 6 +-- nod/src/io/gcz.rs | 4 +- nod/src/io/iso.rs | 8 +-- nod/src/io/tgc.rs | 4 +- nod/src/io/wbfs.rs | 6 +-- nod/src/io/wia.rs | 4 +- nod/src/lib.rs | 56 ++++++++++---------- nod/src/read.rs | 35 ++++++------- nod/src/util/mod.rs | 2 +- nod/src/write.rs | 27 +++++++--- nodtool/src/cmd/gen.rs | 24 +-------- 13 files changed, 184 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index c0a5a45..6853c9c 100644 --- a/README.md +++ b/README.md @@ -81,17 +81,21 @@ Opening a disc image and reading a file: ```rust use std::io::Read; +use nod::{ + common::PartitionKind, + read::{DiscOptions, DiscReader, PartitionOptions}, +}; + // Open a disc image and the first data partition. -let disc = nod::Disc::new("path/to/file.iso") - .expect("Failed to open disc"); -let mut partition = disc.open_partition_kind(nod::PartitionKind::Data) +let disc = + DiscReader::new("path/to/file.iso", &DiscOptions::default()).expect("Failed to open disc"); +let mut partition = disc + .open_partition_kind(PartitionKind::Data, &PartitionOptions::default()) .expect("Failed to open data partition"); // Read partition metadata and the file system table. -let meta = partition.meta() - .expect("Failed to read partition metadata"); -let fst = meta.fst() - .expect("File system table is invalid"); +let meta = partition.meta().expect("Failed to read partition metadata"); +let fst = meta.fst().expect("File system table is invalid"); // Find a file by path and read it into a string. if let Some((_, node)) = fst.find("/MP3/Worlds.txt") { @@ -108,16 +112,98 @@ if let Some((_, node)) = fst.find("/MP3/Worlds.txt") { Converting a disc image to raw ISO: ```rust -// Enable `rebuild_encryption` to ensure the output is a valid ISO. -let options = nod::OpenOptions { rebuild_encryption: true, ..Default::default() }; -let mut disc = nod::Disc::new_with_options("path/to/file.rvz", &options) - .expect("Failed to open disc"); +use nod::read::{DiscOptions, DiscReader, PartitionEncryption}; -// Read directly from the open disc and write to the output file. -let mut out = std::fs::File::create("output.iso") +let options = DiscOptions { + partition_encryption: PartitionEncryption::Original, + // Use 4 threads to preload data as the disc is read. This can speed up sequential reads, + // especially when the disc image format uses compression. + preloader_threads: 4, +}; +// Open a disc image. +let mut disc = DiscReader::new("path/to/file.rvz", &options).expect("Failed to open disc"); + +// Create a new output file. +let mut out = std::fs::File::create("output.iso").expect("Failed to create output file"); +// Read directly from the DiscReader and write to the output file. +// NOTE: Any copy method that accepts `Read` and `Write` can be used here, +// such as `std::io::copy`. This example utilizes `BufRead` for efficiency, +// since `DiscReader` has its own internal buffer. +nod::util::buf_copy(&mut disc, &mut out).expect("Failed to write data"); +``` + +Converting a disc image to RVZ: + +```rust +use std::fs::File; +use std::io::{Seek, Write}; +use nod::common::{Compression, Format}; +use nod::read::{DiscOptions, DiscReader, PartitionEncryption}; +use nod::write::{DiscWriter, DiscWriterWeight, FormatOptions, ProcessOptions}; + +let open_options = DiscOptions { + partition_encryption: PartitionEncryption::Original, + // Use 4 threads to preload data as the disc is read. This can speed up sequential reads, + // especially when the disc image format uses compression. + preloader_threads: 4, +}; +// Open a disc image. +let disc = DiscReader::new("path/to/file.iso", &open_options) + .expect("Failed to open disc"); +// Create a new output file. +let mut output_file = File::create("output.rvz") .expect("Failed to create output file"); -std::io::copy(&mut disc, &mut out) - .expect("Failed to write data"); + +let options = FormatOptions { + format: Format::Rvz, + compression: Compression::Zstandard(19), + block_size: Format::Rvz.default_block_size(), +}; +// Create a disc writer with the desired output format. +let mut writer = DiscWriter::new(disc, &options) + .expect("Failed to create writer"); + +// Ideally we'd base this on the actual number of CPUs available. +// This is just an example. +let num_threads = match writer.weight() { + DiscWriterWeight::Light => 0, + DiscWriterWeight::Medium => 4, + DiscWriterWeight::Heavy => 12, +}; +let process_options = ProcessOptions { + processor_threads: num_threads, + // Enable checksum calculation for the _original_ disc data. + // Digests will be stored in the output file for verification, if supported. + // They will also be returned in the finalization result. + digest_crc32: true, + digest_md5: false, // MD5 is slow, skip it + digest_sha1: true, + digest_xxh64: true, +}; +// Start processing the disc image. +let finalization = writer.process( + |data, _progress, _total| { + output_file.write_all(data.as_ref())?; + // One could display progress here, if desired. + Ok(()) + }, + &process_options +) +.expect("Failed to process disc image"); + +// Some disc writers calculate data during processing. +// If the finalization returns header data, seek to the beginning of the file and write it. +if !finalization.header.is_empty() { + output_file.rewind() + .expect("Failed to seek"); + output_file.write_all(finalization.header.as_ref()) + .expect("Failed to write header"); +} +output_file.flush().expect("Failed to flush output file"); + +// Display the calculated digests. +println!("CRC32: {:08X}", finalization.crc32.unwrap()); +// ... ``` ## License diff --git a/nod/src/disc/writer.rs b/nod/src/disc/writer.rs index 1899f79..bbf76d6 100644 --- a/nod/src/disc/writer.rs +++ b/nod/src/disc/writer.rs @@ -16,17 +16,9 @@ use crate::{ wii::{HASHES_SIZE, SECTOR_DATA_SIZE}, }, util::{aes::decrypt_sector_b2b, array_ref, array_ref_mut, lfg::LaggedFibonacci}, - write::{DiscFinalization, DiscWriterWeight, ProcessOptions}, + write::{DataCallback, DiscFinalization, DiscWriterWeight, ProcessOptions}, }; -/// A callback for writing disc data. -/// -/// The callback should write all data to the output stream before returning, or return an error if -/// writing fails. The second and third arguments are the current bytes processed and the total -/// bytes to process, respectively. For most formats, this has no relation to the written disc size, -/// but can be used to display progress. -pub type DataCallback<'a> = dyn FnMut(Bytes, u64, u64) -> io::Result<()> + 'a; - /// A trait for writing disc images. pub trait DiscWriter: DynClone { /// Processes the disc writer to completion. diff --git a/nod/src/io/ciso.rs b/nod/src/io/ciso.rs index 4317556..2da458d 100644 --- a/nod/src/io/ciso.rs +++ b/nod/src/io/ciso.rs @@ -15,8 +15,8 @@ use crate::{ SECTOR_SIZE, reader::DiscReader, writer::{ - BlockProcessor, BlockResult, CheckBlockResult, DataCallback, DiscWriter, check_block, - par_process, read_block, + BlockProcessor, BlockResult, CheckBlockResult, DiscWriter, check_block, par_process, + read_block, }, }, io::{ @@ -31,7 +31,7 @@ use crate::{ read::{box_to_bytes, read_arc_at}, static_assert, }, - write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, + write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, }; pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8; diff --git a/nod/src/io/gcz.rs b/nod/src/io/gcz.rs index 52fa988..eb00033 100644 --- a/nod/src/io/gcz.rs +++ b/nod/src/io/gcz.rs @@ -15,7 +15,7 @@ use crate::{ disc::{ SECTOR_SIZE, reader::DiscReader, - writer::{BlockProcessor, BlockResult, DataCallback, DiscWriter, par_process, read_block}, + writer::{BlockProcessor, BlockResult, DiscWriter, par_process, read_block}, }, io::block::{Block, BlockKind, BlockReader, GCZ_MAGIC}, read::{DiscMeta, DiscStream}, @@ -25,7 +25,7 @@ use crate::{ read::{read_arc_slice_at, read_at}, static_assert, }, - write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, + write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, }; /// GCZ header (little endian) diff --git a/nod/src/io/iso.rs b/nod/src/io/iso.rs index 13cb704..7e14172 100644 --- a/nod/src/io/iso.rs +++ b/nod/src/io/iso.rs @@ -3,15 +3,11 @@ use std::{io, io::BufRead}; use crate::{ Result, ResultContext, common::Format, - disc::{ - SECTOR_SIZE, - reader::DiscReader, - writer::{DataCallback, DiscWriter}, - }, + disc::{SECTOR_SIZE, reader::DiscReader, writer::DiscWriter}, io::block::{Block, BlockKind, BlockReader}, read::{DiscMeta, DiscStream}, util::digest::DigestManager, - write::{DiscFinalization, DiscWriterWeight, ProcessOptions}, + write::{DataCallback, DiscFinalization, DiscWriterWeight, ProcessOptions}, }; #[derive(Clone)] diff --git a/nod/src/io/tgc.rs b/nod/src/io/tgc.rs index 8abc53f..1b117dc 100644 --- a/nod/src/io/tgc.rs +++ b/nod/src/io/tgc.rs @@ -16,7 +16,7 @@ use crate::{ fst::Fst, gcn::{read_dol, read_fst}, reader::DiscReader, - writer::{DataCallback, DiscWriter}, + writer::DiscWriter, }, io::block::{Block, BlockKind, BlockReader, TGC_MAGIC}, read::{DiscMeta, DiscStream, PartitionOptions, PartitionReader}, @@ -25,7 +25,7 @@ use crate::{ read::{read_arc_at, read_arc_slice_at, read_at, read_with_zero_fill}, static_assert, }, - write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, + write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, }; /// TGC header (big endian) diff --git a/nod/src/io/wbfs.rs b/nod/src/io/wbfs.rs index 98cc367..94766f4 100644 --- a/nod/src/io/wbfs.rs +++ b/nod/src/io/wbfs.rs @@ -15,8 +15,8 @@ use crate::{ SECTOR_SIZE, reader::DiscReader, writer::{ - BlockProcessor, BlockResult, CheckBlockResult, DataCallback, DiscWriter, check_block, - par_process, read_block, + BlockProcessor, BlockResult, CheckBlockResult, DiscWriter, check_block, par_process, + read_block, }, }, io::{ @@ -30,7 +30,7 @@ use crate::{ lfg::LaggedFibonacci, read::{read_arc_slice_at, read_at, read_box_slice_at}, }, - write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, + write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, }; #[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] diff --git a/nod/src/io/wia.rs b/nod/src/io/wia.rs index db90aa4..33575f3 100644 --- a/nod/src/io/wia.rs +++ b/nod/src/io/wia.rs @@ -20,7 +20,7 @@ use crate::{ fst::Fst, reader::DiscReader, wii::{HASHES_SIZE, SECTOR_DATA_SIZE}, - writer::{BlockProcessor, BlockResult, DataCallback, DiscWriter, par_process, read_block}, + writer::{BlockProcessor, BlockResult, DiscWriter, par_process, read_block}, }, io::{ block::{Block, BlockKind, BlockReader, RVZ_MAGIC, WIA_MAGIC}, @@ -40,7 +40,7 @@ use crate::{ }, static_assert, }, - write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, + write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, }; const WIA_VERSION: u32 = 0x01000000; diff --git a/nod/src/lib.rs b/nod/src/lib.rs index 29c92a3..5a6841e 100644 --- a/nod/src/lib.rs +++ b/nod/src/lib.rs @@ -1,16 +1,16 @@ #![allow(clippy::new_ret_no_self)] #![warn(missing_docs)] -//! Library for traversing & reading Nintendo Optical Disc (GameCube and Wii) images. +//! Library for reading and writing Nintendo Optical Disc (GameCube and Wii) images. //! //! Originally based on the C++ library [nod](https://github.com/AxioDL/nod), -//! but does not currently support authoring. +//! but with extended format support and many additional features. //! //! Currently supported file formats: //! - ISO (GCM) //! - WIA / RVZ //! - WBFS (+ NKit 2 lossless) //! - CISO (+ NKit 2 lossless) -//! - NFS (Wii U VC) +//! - NFS (Wii U VC, read-only) //! - GCZ //! - TGC //! @@ -75,11 +75,16 @@ //! Converting a disc image to RVZ: //! //! ```no_run -//! use std::fs::File; -//! use std::io::{Seek, Write}; -//! use nod::common::{Compression, Format}; -//! use nod::read::{DiscOptions, DiscReader, PartitionEncryption}; -//! use nod::write::{DiscWriter, DiscWriterWeight, FormatOptions, ProcessOptions}; +//! use std::{ +//! fs::File, +//! io::{Seek, Write}, +//! }; +//! +//! use nod::{ +//! common::{Compression, Format}, +//! read::{DiscOptions, DiscReader, PartitionEncryption}, +//! write::{DiscWriter, DiscWriterWeight, FormatOptions, ProcessOptions}, +//! }; //! //! let open_options = DiscOptions { //! partition_encryption: PartitionEncryption::Original, @@ -88,11 +93,9 @@ //! preloader_threads: 4, //! }; //! // Open a disc image. -//! let disc = DiscReader::new("path/to/file.iso", &open_options) -//! .expect("Failed to open disc"); +//! let disc = DiscReader::new("path/to/file.iso", &open_options).expect("Failed to open disc"); //! // Create a new output file. -//! let mut output_file = File::create("output.rvz") -//! .expect("Failed to create output file"); +//! let mut output_file = File::create("output.rvz").expect("Failed to create output file"); //! //! let options = FormatOptions { //! format: Format::Rvz, @@ -100,8 +103,7 @@ //! block_size: Format::Rvz.default_block_size(), //! }; //! // Create a disc writer with the desired output format. -//! let mut writer = DiscWriter::new(disc, &options) -//! .expect("Failed to create writer"); +//! let mut writer = DiscWriter::new(disc, &options).expect("Failed to create writer"); //! //! // Ideally we'd base this on the actual number of CPUs available. //! // This is just an example. @@ -121,29 +123,29 @@ //! digest_xxh64: true, //! }; //! // Start processing the disc image. -//! let finalization = writer.process( -//! |data, _progress, _total| { -//! output_file.write_all(data.as_ref())?; -//! // One could display progress here, if desired. -//! Ok(()) -//! }, -//! &process_options -//! ) -//! .expect("Failed to process disc image"); +//! let finalization = writer +//! .process( +//! |data, _progress, _total| { +//! output_file.write_all(data.as_ref())?; +//! // One could display progress here, if desired. +//! Ok(()) +//! }, +//! &process_options, +//! ) +//! .expect("Failed to process disc image"); //! //! // Some disc writers calculate data during processing. //! // If the finalization returns header data, seek to the beginning of the file and write it. //! if !finalization.header.is_empty() { -//! output_file.rewind() -//! .expect("Failed to seek"); -//! output_file.write_all(finalization.header.as_ref()) -//! .expect("Failed to write header"); +//! output_file.rewind().expect("Failed to seek"); +//! output_file.write_all(finalization.header.as_ref()).expect("Failed to write header"); //! } //! output_file.flush().expect("Failed to flush output file"); //! //! // Display the calculated digests. //! println!("CRC32: {:08X}", finalization.crc32.unwrap()); //! // ... +//! ``` pub mod build; pub mod common; diff --git a/nod/src/read.rs b/nod/src/read.rs index f0fc25e..c833b3f 100644 --- a/nod/src/read.rs +++ b/nod/src/read.rs @@ -167,16 +167,15 @@ where T: Read + Seek + Send /// /// This is the primary entry point for reading disc images. #[derive(Clone)] -pub struct DiscReader { - inner: disc::reader::DiscReader, -} +#[repr(transparent)] +pub struct DiscReader(disc::reader::DiscReader); impl DiscReader { /// Opens a disc image from a file path. pub fn new>(path: P, options: &DiscOptions) -> Result { let io = block::open(path.as_ref())?; let inner = disc::reader::DiscReader::new(io, options)?; - Ok(DiscReader { inner }) + Ok(DiscReader(inner)) } /// Opens a disc image from a [`DiscStream`]. This allows low-overhead, multithreaded @@ -185,8 +184,8 @@ impl DiscReader { /// See [`DiscStream`] for more information. pub fn new_stream(stream: Box, options: &DiscOptions) -> Result { let io = block::new(stream)?; - let reader = disc::reader::DiscReader::new(io, options)?; - Ok(DiscReader { inner: reader }) + let inner = disc::reader::DiscReader::new(io, options)?; + Ok(DiscReader(inner)) } /// Opens a disc image from a [`Read`] + [`Seek`] stream that can be cloned. @@ -217,27 +216,27 @@ impl DiscReader { /// The disc's primary header. #[inline] - pub fn header(&self) -> &DiscHeader { self.inner.header() } + pub fn header(&self) -> &DiscHeader { self.0.header() } /// The Wii disc's region information. /// /// **GameCube**: This will return `None`. #[inline] - pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.inner.region() } + pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.0.region() } /// Returns extra metadata included in the disc file format, if any. #[inline] - pub fn meta(&self) -> DiscMeta { self.inner.meta() } + pub fn meta(&self) -> DiscMeta { self.0.meta() } /// The disc's size in bytes, or an estimate if not stored by the format. #[inline] - pub fn disc_size(&self) -> u64 { self.inner.disc_size() } + pub fn disc_size(&self) -> u64 { self.0.disc_size() } /// A list of Wii partitions on the disc. /// /// **GameCube**: This will return an empty slice. #[inline] - pub fn partitions(&self) -> &[PartitionInfo] { self.inner.partitions() } + pub fn partitions(&self) -> &[PartitionInfo] { self.0.partitions() } /// Opens a decrypted partition read stream for the specified partition index. /// @@ -248,7 +247,7 @@ impl DiscReader { index: usize, options: &PartitionOptions, ) -> Result> { - self.inner.open_partition(index, options) + self.0.open_partition(index, options) } /// Opens a decrypted partition read stream for the first partition matching @@ -261,28 +260,28 @@ impl DiscReader { kind: PartitionKind, options: &PartitionOptions, ) -> Result> { - self.inner.open_partition_kind(kind, options) + self.0.open_partition_kind(kind, options) } - pub(crate) fn into_inner(self) -> disc::reader::DiscReader { self.inner } + pub(crate) fn into_inner(self) -> disc::reader::DiscReader { self.0 } } impl BufRead for DiscReader { #[inline] - fn fill_buf(&mut self) -> io::Result<&[u8]> { self.inner.fill_buf() } + fn fill_buf(&mut self) -> io::Result<&[u8]> { self.0.fill_buf() } #[inline] - fn consume(&mut self, amt: usize) { self.inner.consume(amt) } + fn consume(&mut self, amt: usize) { self.0.consume(amt) } } impl Read for DiscReader { #[inline] - fn read(&mut self, buf: &mut [u8]) -> io::Result { self.inner.read(buf) } + fn read(&mut self, buf: &mut [u8]) -> io::Result { self.0.read(buf) } } impl Seek for DiscReader { #[inline] - fn seek(&mut self, pos: io::SeekFrom) -> io::Result { self.inner.seek(pos) } + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { self.0.seek(pos) } } /// Extra metadata about the underlying disc file format. diff --git a/nod/src/util/mod.rs b/nod/src/util/mod.rs index 3971f17..80745b9 100644 --- a/nod/src/util/mod.rs +++ b/nod/src/util/mod.rs @@ -14,7 +14,7 @@ pub(crate) mod digest; pub mod lfg; pub(crate) mod read; -/// Copies from a buffered reader to a writer without extra allocations. +/// Copies from a [`BufRead`] to a [`Write`] without allocating a buffer. pub fn buf_copy(reader: &mut R, writer: &mut W) -> io::Result where R: BufRead + ?Sized, diff --git a/nod/src/write.rs b/nod/src/write.rs index 92c80d9..21a8972 100644 --- a/nod/src/write.rs +++ b/nod/src/write.rs @@ -1,5 +1,7 @@ //! [`DiscWriter`] and associated types. +use std::io; + use bytes::Bytes; use crate::{ @@ -70,13 +72,20 @@ pub struct ProcessOptions { pub digest_xxh64: bool, } +/// A callback for writing disc data. +/// +/// The callback should write all data to the output stream before returning, or return an error if +/// writing fails. The second and third arguments are the current bytes processed and the total +/// bytes to process, respectively. For most formats, this has no relation to the written disc size, +/// but can be used to display progress. +pub type DataCallback<'a> = dyn FnMut(Bytes, u64, u64) -> io::Result<()> + 'a; + /// A constructed disc writer. /// /// This is the primary entry point for writing disc images. #[derive(Clone)] -pub struct DiscWriter { - inner: Box, -} +#[repr(transparent)] +pub struct DiscWriter(Box); impl DiscWriter { /// Creates a new disc writer with the specified format options. @@ -101,31 +110,33 @@ impl DiscWriter { Format::Wia | Format::Rvz => crate::io::wia::DiscWriterWIA::new(reader, &options)?, format => return Err(Error::Other(format!("Unsupported write format: {format}"))), }; - Ok(DiscWriter { inner }) + Ok(DiscWriter(inner)) } /// Processes the disc writer to completion, calling the data callback, in order, for each block /// of data to write to the output file. The callback should write all data before returning, or /// return an error if writing fails. + /// + /// See [`DataCallback`] for more information. #[inline] pub fn process( &self, - mut data_callback: impl FnMut(Bytes, u64, u64) -> std::io::Result<()>, + mut data_callback: impl FnMut(Bytes, u64, u64) -> io::Result<()>, options: &ProcessOptions, ) -> Result { - self.inner.process(&mut data_callback, options) + self.0.process(&mut data_callback, options) } /// Returns the progress upper bound for the disc writer. For most formats, this has no /// relation to the written disc size, but can be used to display progress. #[inline] - pub fn progress_bound(&self) -> u64 { self.inner.progress_bound() } + pub fn progress_bound(&self) -> u64 { self.0.progress_bound() } /// Returns the weight of the disc writer, which can help determine the number of threads to /// dedicate for output processing. This may depend on the format's configuration, such as /// whether compression is enabled. #[inline] - pub fn weight(&self) -> DiscWriterWeight { self.inner.weight() } + pub fn weight(&self) -> DiscWriterWeight { self.0.weight() } } /// Data returned by the disc writer after processing. diff --git a/nodtool/src/cmd/gen.rs b/nodtool/src/cmd/gen.rs index 40cd501..5aecc10 100644 --- a/nodtool/src/cmd/gen.rs +++ b/nodtool/src/cmd/gen.rs @@ -2,7 +2,7 @@ use std::{ fs, fs::File, io, - io::{BufRead, Read, Seek, SeekFrom, Write}, + io::{Read, Seek, SeekFrom, Write}, path::{Path, PathBuf}, str::from_utf8, time::Instant, @@ -21,7 +21,7 @@ use nod::{ DiscOptions, DiscReader, PartitionEncryption, PartitionMeta, PartitionOptions, PartitionReader, }, - util::lfg::LaggedFibonacci, + util::{buf_copy, lfg::LaggedFibonacci}, write::{DiscWriter, FormatOptions, ProcessOptions}, }; use tracing::{debug, error, info, warn}; @@ -745,23 +745,3 @@ where W: Write fn stream_position(&mut self) -> io::Result { Ok(self.position) } } - -/// Copies from a buffered reader to a writer without extra allocations. -fn buf_copy(reader: &mut R, writer: &mut W) -> io::Result -where - R: BufRead + ?Sized, - W: Write + ?Sized, -{ - let mut copied = 0; - loop { - let buf = reader.fill_buf()?; - let len = buf.len(); - if len == 0 { - break; - } - writer.write_all(buf)?; - reader.consume(len); - copied += len as u64; - } - Ok(copied) -}