Documentation updates

This commit is contained in:
Luke Street 2025-03-31 23:33:07 -06:00
parent 9d8cd980b8
commit 4b4564207a
13 changed files with 184 additions and 118 deletions

116
README.md
View File

@ -81,17 +81,21 @@ Opening a disc image and reading a file:
```rust ```rust
use std::io::Read; use std::io::Read;
use nod::{
common::PartitionKind,
read::{DiscOptions, DiscReader, PartitionOptions},
};
// Open a disc image and the first data partition. // Open a disc image and the first data partition.
let disc = nod::Disc::new("path/to/file.iso") let disc =
.expect("Failed to open disc"); DiscReader::new("path/to/file.iso", &DiscOptions::default()).expect("Failed to open disc");
let mut partition = disc.open_partition_kind(nod::PartitionKind::Data) let mut partition = disc
.open_partition_kind(PartitionKind::Data, &PartitionOptions::default())
.expect("Failed to open data partition"); .expect("Failed to open data partition");
// Read partition metadata and the file system table. // Read partition metadata and the file system table.
let meta = partition.meta() let meta = partition.meta().expect("Failed to read partition metadata");
.expect("Failed to read partition metadata"); let fst = meta.fst().expect("File system table is invalid");
let fst = meta.fst()
.expect("File system table is invalid");
// Find a file by path and read it into a string. // Find a file by path and read it into a string.
if let Some((_, node)) = fst.find("/MP3/Worlds.txt") { 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: Converting a disc image to raw ISO:
```rust ```rust
// Enable `rebuild_encryption` to ensure the output is a valid ISO. use nod::read::{DiscOptions, DiscReader, PartitionEncryption};
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");
// Read directly from the open disc and write to the output file. let options = DiscOptions {
let mut out = std::fs::File::create("output.iso") 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"); .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 ## License

View File

@ -16,17 +16,9 @@ use crate::{
wii::{HASHES_SIZE, SECTOR_DATA_SIZE}, wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
}, },
util::{aes::decrypt_sector_b2b, array_ref, array_ref_mut, lfg::LaggedFibonacci}, 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. /// A trait for writing disc images.
pub trait DiscWriter: DynClone { pub trait DiscWriter: DynClone {
/// Processes the disc writer to completion. /// Processes the disc writer to completion.

View File

@ -15,8 +15,8 @@ use crate::{
SECTOR_SIZE, SECTOR_SIZE,
reader::DiscReader, reader::DiscReader,
writer::{ writer::{
BlockProcessor, BlockResult, CheckBlockResult, DataCallback, DiscWriter, check_block, BlockProcessor, BlockResult, CheckBlockResult, DiscWriter, check_block, par_process,
par_process, read_block, read_block,
}, },
}, },
io::{ io::{
@ -31,7 +31,7 @@ use crate::{
read::{box_to_bytes, read_arc_at}, read::{box_to_bytes, read_arc_at},
static_assert, static_assert,
}, },
write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions},
}; };
pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8; pub const CISO_MAP_SIZE: usize = SECTOR_SIZE - 8;

View File

@ -15,7 +15,7 @@ use crate::{
disc::{ disc::{
SECTOR_SIZE, SECTOR_SIZE,
reader::DiscReader, 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}, io::block::{Block, BlockKind, BlockReader, GCZ_MAGIC},
read::{DiscMeta, DiscStream}, read::{DiscMeta, DiscStream},
@ -25,7 +25,7 @@ use crate::{
read::{read_arc_slice_at, read_at}, read::{read_arc_slice_at, read_at},
static_assert, static_assert,
}, },
write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions},
}; };
/// GCZ header (little endian) /// GCZ header (little endian)

View File

@ -3,15 +3,11 @@ use std::{io, io::BufRead};
use crate::{ use crate::{
Result, ResultContext, Result, ResultContext,
common::Format, common::Format,
disc::{ disc::{SECTOR_SIZE, reader::DiscReader, writer::DiscWriter},
SECTOR_SIZE,
reader::DiscReader,
writer::{DataCallback, DiscWriter},
},
io::block::{Block, BlockKind, BlockReader}, io::block::{Block, BlockKind, BlockReader},
read::{DiscMeta, DiscStream}, read::{DiscMeta, DiscStream},
util::digest::DigestManager, util::digest::DigestManager,
write::{DiscFinalization, DiscWriterWeight, ProcessOptions}, write::{DataCallback, DiscFinalization, DiscWriterWeight, ProcessOptions},
}; };
#[derive(Clone)] #[derive(Clone)]

View File

@ -16,7 +16,7 @@ use crate::{
fst::Fst, fst::Fst,
gcn::{read_dol, read_fst}, gcn::{read_dol, read_fst},
reader::DiscReader, reader::DiscReader,
writer::{DataCallback, DiscWriter}, writer::DiscWriter,
}, },
io::block::{Block, BlockKind, BlockReader, TGC_MAGIC}, io::block::{Block, BlockKind, BlockReader, TGC_MAGIC},
read::{DiscMeta, DiscStream, PartitionOptions, PartitionReader}, 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}, read::{read_arc_at, read_arc_slice_at, read_at, read_with_zero_fill},
static_assert, static_assert,
}, },
write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions},
}; };
/// TGC header (big endian) /// TGC header (big endian)

View File

@ -15,8 +15,8 @@ use crate::{
SECTOR_SIZE, SECTOR_SIZE,
reader::DiscReader, reader::DiscReader,
writer::{ writer::{
BlockProcessor, BlockResult, CheckBlockResult, DataCallback, DiscWriter, check_block, BlockProcessor, BlockResult, CheckBlockResult, DiscWriter, check_block, par_process,
par_process, read_block, read_block,
}, },
}, },
io::{ io::{
@ -30,7 +30,7 @@ use crate::{
lfg::LaggedFibonacci, lfg::LaggedFibonacci,
read::{read_arc_slice_at, read_at, read_box_slice_at}, 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)] #[derive(Debug, Clone, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]

View File

@ -20,7 +20,7 @@ use crate::{
fst::Fst, fst::Fst,
reader::DiscReader, reader::DiscReader,
wii::{HASHES_SIZE, SECTOR_DATA_SIZE}, wii::{HASHES_SIZE, SECTOR_DATA_SIZE},
writer::{BlockProcessor, BlockResult, DataCallback, DiscWriter, par_process, read_block}, writer::{BlockProcessor, BlockResult, DiscWriter, par_process, read_block},
}, },
io::{ io::{
block::{Block, BlockKind, BlockReader, RVZ_MAGIC, WIA_MAGIC}, block::{Block, BlockKind, BlockReader, RVZ_MAGIC, WIA_MAGIC},
@ -40,7 +40,7 @@ use crate::{
}, },
static_assert, static_assert,
}, },
write::{DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions}, write::{DataCallback, DiscFinalization, DiscWriterWeight, FormatOptions, ProcessOptions},
}; };
const WIA_VERSION: u32 = 0x01000000; const WIA_VERSION: u32 = 0x01000000;

View File

@ -1,16 +1,16 @@
#![allow(clippy::new_ret_no_self)] #![allow(clippy::new_ret_no_self)]
#![warn(missing_docs)] #![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), //! 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: //! Currently supported file formats:
//! - ISO (GCM) //! - ISO (GCM)
//! - WIA / RVZ //! - WIA / RVZ
//! - WBFS (+ NKit 2 lossless) //! - WBFS (+ NKit 2 lossless)
//! - CISO (+ NKit 2 lossless) //! - CISO (+ NKit 2 lossless)
//! - NFS (Wii U VC) //! - NFS (Wii U VC, read-only)
//! - GCZ //! - GCZ
//! - TGC //! - TGC
//! //!
@ -75,11 +75,16 @@
//! Converting a disc image to RVZ: //! Converting a disc image to RVZ:
//! //!
//! ```no_run //! ```no_run
//! use std::fs::File; //! use std::{
//! use std::io::{Seek, Write}; //! fs::File,
//! use nod::common::{Compression, Format}; //! io::{Seek, Write},
//! use nod::read::{DiscOptions, DiscReader, PartitionEncryption}; //! };
//! use nod::write::{DiscWriter, DiscWriterWeight, FormatOptions, ProcessOptions}; //!
//! use nod::{
//! common::{Compression, Format},
//! read::{DiscOptions, DiscReader, PartitionEncryption},
//! write::{DiscWriter, DiscWriterWeight, FormatOptions, ProcessOptions},
//! };
//! //!
//! let open_options = DiscOptions { //! let open_options = DiscOptions {
//! partition_encryption: PartitionEncryption::Original, //! partition_encryption: PartitionEncryption::Original,
@ -88,11 +93,9 @@
//! preloader_threads: 4, //! preloader_threads: 4,
//! }; //! };
//! // Open a disc image. //! // Open a disc image.
//! let disc = DiscReader::new("path/to/file.iso", &open_options) //! let disc = DiscReader::new("path/to/file.iso", &open_options).expect("Failed to open disc");
//! .expect("Failed to open disc");
//! // Create a new output file. //! // Create a new output file.
//! let mut output_file = File::create("output.rvz") //! let mut output_file = File::create("output.rvz").expect("Failed to create output file");
//! .expect("Failed to create output file");
//! //!
//! let options = FormatOptions { //! let options = FormatOptions {
//! format: Format::Rvz, //! format: Format::Rvz,
@ -100,8 +103,7 @@
//! block_size: Format::Rvz.default_block_size(), //! block_size: Format::Rvz.default_block_size(),
//! }; //! };
//! // Create a disc writer with the desired output format. //! // Create a disc writer with the desired output format.
//! let mut writer = DiscWriter::new(disc, &options) //! let mut writer = DiscWriter::new(disc, &options).expect("Failed to create writer");
//! .expect("Failed to create writer");
//! //!
//! // Ideally we'd base this on the actual number of CPUs available. //! // Ideally we'd base this on the actual number of CPUs available.
//! // This is just an example. //! // This is just an example.
@ -121,29 +123,29 @@
//! digest_xxh64: true, //! digest_xxh64: true,
//! }; //! };
//! // Start processing the disc image. //! // Start processing the disc image.
//! let finalization = writer.process( //! let finalization = writer
//! |data, _progress, _total| { //! .process(
//! output_file.write_all(data.as_ref())?; //! |data, _progress, _total| {
//! // One could display progress here, if desired. //! output_file.write_all(data.as_ref())?;
//! Ok(()) //! // One could display progress here, if desired.
//! }, //! Ok(())
//! &process_options //! },
//! ) //! &process_options,
//! .expect("Failed to process disc image"); //! )
//! .expect("Failed to process disc image");
//! //!
//! // Some disc writers calculate data during processing. //! // Some disc writers calculate data during processing.
//! // If the finalization returns header data, seek to the beginning of the file and write it. //! // If the finalization returns header data, seek to the beginning of the file and write it.
//! if !finalization.header.is_empty() { //! if !finalization.header.is_empty() {
//! output_file.rewind() //! output_file.rewind().expect("Failed to seek");
//! .expect("Failed to seek"); //! output_file.write_all(finalization.header.as_ref()).expect("Failed to write header");
//! output_file.write_all(finalization.header.as_ref())
//! .expect("Failed to write header");
//! } //! }
//! output_file.flush().expect("Failed to flush output file"); //! output_file.flush().expect("Failed to flush output file");
//! //!
//! // Display the calculated digests. //! // Display the calculated digests.
//! println!("CRC32: {:08X}", finalization.crc32.unwrap()); //! println!("CRC32: {:08X}", finalization.crc32.unwrap());
//! // ... //! // ...
//! ```
pub mod build; pub mod build;
pub mod common; pub mod common;

View File

@ -167,16 +167,15 @@ where T: Read + Seek + Send
/// ///
/// This is the primary entry point for reading disc images. /// This is the primary entry point for reading disc images.
#[derive(Clone)] #[derive(Clone)]
pub struct DiscReader { #[repr(transparent)]
inner: disc::reader::DiscReader, pub struct DiscReader(disc::reader::DiscReader);
}
impl DiscReader { impl DiscReader {
/// Opens a disc image from a file path. /// Opens a disc image from a file path.
pub fn new<P: AsRef<Path>>(path: P, options: &DiscOptions) -> Result<DiscReader> { pub fn new<P: AsRef<Path>>(path: P, options: &DiscOptions) -> Result<DiscReader> {
let io = block::open(path.as_ref())?; let io = block::open(path.as_ref())?;
let inner = disc::reader::DiscReader::new(io, options)?; 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 /// Opens a disc image from a [`DiscStream`]. This allows low-overhead, multithreaded
@ -185,8 +184,8 @@ impl DiscReader {
/// See [`DiscStream`] for more information. /// See [`DiscStream`] for more information.
pub fn new_stream(stream: Box<dyn DiscStream>, options: &DiscOptions) -> Result<DiscReader> { pub fn new_stream(stream: Box<dyn DiscStream>, options: &DiscOptions) -> Result<DiscReader> {
let io = block::new(stream)?; let io = block::new(stream)?;
let reader = disc::reader::DiscReader::new(io, options)?; let inner = disc::reader::DiscReader::new(io, options)?;
Ok(DiscReader { inner: reader }) Ok(DiscReader(inner))
} }
/// Opens a disc image from a [`Read`] + [`Seek`] stream that can be cloned. /// Opens a disc image from a [`Read`] + [`Seek`] stream that can be cloned.
@ -217,27 +216,27 @@ impl DiscReader {
/// The disc's primary header. /// The disc's primary header.
#[inline] #[inline]
pub fn header(&self) -> &DiscHeader { self.inner.header() } pub fn header(&self) -> &DiscHeader { self.0.header() }
/// The Wii disc's region information. /// The Wii disc's region information.
/// ///
/// **GameCube**: This will return `None`. /// **GameCube**: This will return `None`.
#[inline] #[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. /// Returns extra metadata included in the disc file format, if any.
#[inline] #[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. /// The disc's size in bytes, or an estimate if not stored by the format.
#[inline] #[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. /// A list of Wii partitions on the disc.
/// ///
/// **GameCube**: This will return an empty slice. /// **GameCube**: This will return an empty slice.
#[inline] #[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. /// Opens a decrypted partition read stream for the specified partition index.
/// ///
@ -248,7 +247,7 @@ impl DiscReader {
index: usize, index: usize,
options: &PartitionOptions, options: &PartitionOptions,
) -> Result<Box<dyn PartitionReader>> { ) -> Result<Box<dyn PartitionReader>> {
self.inner.open_partition(index, options) self.0.open_partition(index, options)
} }
/// Opens a decrypted partition read stream for the first partition matching /// Opens a decrypted partition read stream for the first partition matching
@ -261,28 +260,28 @@ impl DiscReader {
kind: PartitionKind, kind: PartitionKind,
options: &PartitionOptions, options: &PartitionOptions,
) -> Result<Box<dyn PartitionReader>> { ) -> Result<Box<dyn PartitionReader>> {
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 { impl BufRead for DiscReader {
#[inline] #[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] #[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 { impl Read for DiscReader {
#[inline] #[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { self.inner.read(buf) } fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { self.0.read(buf) }
} }
impl Seek for DiscReader { impl Seek for DiscReader {
#[inline] #[inline]
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> { self.inner.seek(pos) } fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> { self.0.seek(pos) }
} }
/// Extra metadata about the underlying disc file format. /// Extra metadata about the underlying disc file format.

View File

@ -14,7 +14,7 @@ pub(crate) mod digest;
pub mod lfg; pub mod lfg;
pub(crate) mod read; 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<R, W>(reader: &mut R, writer: &mut W) -> io::Result<u64> pub fn buf_copy<R, W>(reader: &mut R, writer: &mut W) -> io::Result<u64>
where where
R: BufRead + ?Sized, R: BufRead + ?Sized,

View File

@ -1,5 +1,7 @@
//! [`DiscWriter`] and associated types. //! [`DiscWriter`] and associated types.
use std::io;
use bytes::Bytes; use bytes::Bytes;
use crate::{ use crate::{
@ -70,13 +72,20 @@ pub struct ProcessOptions {
pub digest_xxh64: bool, 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. /// A constructed disc writer.
/// ///
/// This is the primary entry point for writing disc images. /// This is the primary entry point for writing disc images.
#[derive(Clone)] #[derive(Clone)]
pub struct DiscWriter { #[repr(transparent)]
inner: Box<dyn disc::writer::DiscWriter>, pub struct DiscWriter(Box<dyn disc::writer::DiscWriter>);
}
impl DiscWriter { impl DiscWriter {
/// Creates a new disc writer with the specified format options. /// 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::Wia | Format::Rvz => crate::io::wia::DiscWriterWIA::new(reader, &options)?,
format => return Err(Error::Other(format!("Unsupported write format: {format}"))), 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 /// 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 /// of data to write to the output file. The callback should write all data before returning, or
/// return an error if writing fails. /// return an error if writing fails.
///
/// See [`DataCallback`] for more information.
#[inline] #[inline]
pub fn process( pub fn process(
&self, &self,
mut data_callback: impl FnMut(Bytes, u64, u64) -> std::io::Result<()>, mut data_callback: impl FnMut(Bytes, u64, u64) -> io::Result<()>,
options: &ProcessOptions, options: &ProcessOptions,
) -> Result<DiscFinalization> { ) -> Result<DiscFinalization> {
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 /// 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. /// relation to the written disc size, but can be used to display progress.
#[inline] #[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 /// 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 /// dedicate for output processing. This may depend on the format's configuration, such as
/// whether compression is enabled. /// whether compression is enabled.
#[inline] #[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. /// Data returned by the disc writer after processing.

View File

@ -2,7 +2,7 @@ use std::{
fs, fs,
fs::File, fs::File,
io, io,
io::{BufRead, Read, Seek, SeekFrom, Write}, io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
str::from_utf8, str::from_utf8,
time::Instant, time::Instant,
@ -21,7 +21,7 @@ use nod::{
DiscOptions, DiscReader, PartitionEncryption, PartitionMeta, PartitionOptions, DiscOptions, DiscReader, PartitionEncryption, PartitionMeta, PartitionOptions,
PartitionReader, PartitionReader,
}, },
util::lfg::LaggedFibonacci, util::{buf_copy, lfg::LaggedFibonacci},
write::{DiscWriter, FormatOptions, ProcessOptions}, write::{DiscWriter, FormatOptions, ProcessOptions},
}; };
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
@ -745,23 +745,3 @@ where W: Write
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.position) } fn stream_position(&mut self) -> io::Result<u64> { Ok(self.position) }
} }
/// Copies from a buffered reader to a writer without extra allocations.
fn buf_copy<R, W>(reader: &mut R, writer: &mut W) -> io::Result<u64>
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)
}