nod/
lib.rs

1#![warn(missing_docs, clippy::missing_inline_in_public_items)]
2//! Library for traversing & reading Nintendo Optical Disc (GameCube and Wii) images.
3//!
4//! Originally based on the C++ library [nod](https://github.com/AxioDL/nod),
5//! but does not currently support authoring.
6//!
7//! Currently supported file formats:
8//! - ISO (GCM)
9//! - WIA / RVZ
10//! - WBFS (+ NKit 2 lossless)
11//! - CISO (+ NKit 2 lossless)
12//! - NFS (Wii U VC)
13//! - GCZ
14//!
15//! # Examples
16//!
17//! Opening a disc image and reading a file:
18//!
19//! ```no_run
20//! use std::io::Read;
21//!
22//! // Open a disc image and the first data partition.
23//! let disc = nod::Disc::new("path/to/file.iso")
24//!     .expect("Failed to open disc");
25//! let mut partition = disc.open_partition_kind(nod::PartitionKind::Data)
26//!     .expect("Failed to open data partition");
27//!
28//! // Read partition metadata and the file system table.
29//! let meta = partition.meta()
30//!     .expect("Failed to read partition metadata");
31//! let fst = meta.fst()
32//!     .expect("File system table is invalid");
33//!
34//! // Find a file by path and read it into a string.
35//! if let Some((_, node)) = fst.find("/MP3/Worlds.txt") {
36//!     let mut s = String::new();
37//!     partition
38//!         .open_file(node)
39//!         .expect("Failed to open file stream")
40//!         .read_to_string(&mut s)
41//!         .expect("Failed to read file");
42//!     println!("{}", s);
43//! }
44//! ```
45//!
46//! Converting a disc image to raw ISO:
47//!
48//! ```no_run
49//! // Enable `rebuild_encryption` to ensure the output is a valid ISO.
50//! let options = nod::OpenOptions { rebuild_encryption: true, ..Default::default() };
51//! let mut disc = nod::Disc::new_with_options("path/to/file.rvz", &options)
52//!     .expect("Failed to open disc");
53//!
54//! // Read directly from the open disc and write to the output file.
55//! let mut out = std::fs::File::create("output.iso")
56//!     .expect("Failed to create output file");
57//! std::io::copy(&mut disc, &mut out)
58//!     .expect("Failed to write data");
59//! ```
60
61use std::{
62    io::{BufRead, Read, Seek},
63    path::Path,
64};
65
66pub use disc::{
67    ApploaderHeader, DiscHeader, DolHeader, FileStream, Fst, Node, NodeKind, OwnedFileStream,
68    PartitionBase, PartitionHeader, PartitionKind, PartitionMeta, SignedHeader, Ticket,
69    TicketLimit, TmdHeader, WindowedStream, BI2_SIZE, BOOT_SIZE, DL_DVD_SIZE, GCN_MAGIC,
70    MINI_DVD_SIZE, REGION_SIZE, SECTOR_SIZE, SL_DVD_SIZE, WII_MAGIC,
71};
72pub use io::{
73    block::{DiscStream, PartitionInfo},
74    Compression, DiscMeta, Format, KeyBytes, MagicBytes,
75};
76pub use util::lfg::LaggedFibonacci;
77
78mod disc;
79mod io;
80mod util;
81
82/// Error types for nod.
83#[derive(thiserror::Error, Debug)]
84pub enum Error {
85    /// An error for disc format related issues.
86    #[error("disc format error: {0}")]
87    DiscFormat(String),
88    /// A general I/O error.
89    #[error("I/O error: {0}")]
90    Io(String, #[source] std::io::Error),
91    /// An unknown error.
92    #[error("error: {0}")]
93    Other(String),
94    /// An error occurred while allocating memory.
95    #[error("allocation failed")]
96    Alloc(zerocopy::AllocError),
97}
98
99impl From<&str> for Error {
100    #[inline]
101    fn from(s: &str) -> Error { Error::Other(s.to_string()) }
102}
103
104impl From<String> for Error {
105    #[inline]
106    fn from(s: String) -> Error { Error::Other(s) }
107}
108
109impl From<zerocopy::AllocError> for Error {
110    #[inline]
111    fn from(e: zerocopy::AllocError) -> Error { Error::Alloc(e) }
112}
113
114/// Helper result type for [`Error`].
115pub type Result<T, E = Error> = core::result::Result<T, E>;
116
117/// Helper trait for adding context to errors.
118pub trait ErrorContext {
119    /// Adds context to an error.
120    fn context(self, context: impl Into<String>) -> Error;
121}
122
123impl ErrorContext for std::io::Error {
124    #[inline]
125    fn context(self, context: impl Into<String>) -> Error { Error::Io(context.into(), self) }
126}
127
128/// Helper trait for adding context to result errors.
129pub trait ResultContext<T> {
130    /// Adds context to a result error.
131    fn context(self, context: impl Into<String>) -> Result<T>;
132
133    /// Adds context to a result error using a closure.
134    fn with_context<F>(self, f: F) -> Result<T>
135    where F: FnOnce() -> String;
136}
137
138impl<T, E> ResultContext<T> for Result<T, E>
139where E: ErrorContext
140{
141    #[inline]
142    fn context(self, context: impl Into<String>) -> Result<T> {
143        self.map_err(|e| e.context(context))
144    }
145
146    #[inline]
147    fn with_context<F>(self, f: F) -> Result<T>
148    where F: FnOnce() -> String {
149        self.map_err(|e| e.context(f()))
150    }
151}
152
153/// Options for opening a disc image.
154#[derive(Default, Debug, Clone)]
155pub struct OpenOptions {
156    /// Wii: Rebuild partition data encryption and hashes if the underlying format stores data
157    /// decrypted or with hashes removed. (e.g. WIA/RVZ, NFS)
158    pub rebuild_encryption: bool,
159    /// Wii: Validate partition data hashes while reading the disc image.
160    pub validate_hashes: bool,
161}
162
163/// An open disc image and read stream.
164///
165/// This is the primary entry point for reading disc images.
166pub struct Disc {
167    reader: disc::reader::DiscReader,
168    options: OpenOptions,
169}
170
171impl Disc {
172    /// Opens a disc image from a file path.
173    #[inline]
174    pub fn new<P: AsRef<Path>>(path: P) -> Result<Disc> {
175        Disc::new_with_options(path, &OpenOptions::default())
176    }
177
178    /// Opens a disc image from a file path with custom options.
179    #[inline]
180    pub fn new_with_options<P: AsRef<Path>>(path: P, options: &OpenOptions) -> Result<Disc> {
181        let io = io::block::open(path.as_ref())?;
182        let reader = disc::reader::DiscReader::new(io, options)?;
183        Ok(Disc { reader, options: options.clone() })
184    }
185
186    /// Opens a disc image from a read stream.
187    #[inline]
188    pub fn new_stream(stream: Box<dyn DiscStream>) -> Result<Disc> {
189        Disc::new_stream_with_options(stream, &OpenOptions::default())
190    }
191
192    /// Opens a disc image from a read stream with custom options.
193    #[inline]
194    pub fn new_stream_with_options(
195        stream: Box<dyn DiscStream>,
196        options: &OpenOptions,
197    ) -> Result<Disc> {
198        let io = io::block::new(stream)?;
199        let reader = disc::reader::DiscReader::new(io, options)?;
200        Ok(Disc { reader, options: options.clone() })
201    }
202
203    /// Detects the format of a disc image from a read stream.
204    #[inline]
205    pub fn detect<R>(stream: &mut R) -> std::io::Result<Option<Format>>
206    where R: Read + ?Sized {
207        io::block::detect(stream)
208    }
209
210    /// The disc's primary header.
211    #[inline]
212    pub fn header(&self) -> &DiscHeader { self.reader.header() }
213
214    /// The Wii disc's region information.
215    ///
216    /// **GameCube**: This will return `None`.
217    #[inline]
218    pub fn region(&self) -> Option<&[u8; REGION_SIZE]> { self.reader.region() }
219
220    /// Returns extra metadata included in the disc file format, if any.
221    #[inline]
222    pub fn meta(&self) -> DiscMeta { self.reader.meta() }
223
224    /// The disc's size in bytes, or an estimate if not stored by the format.
225    #[inline]
226    pub fn disc_size(&self) -> u64 { self.reader.disc_size() }
227
228    /// A list of Wii partitions on the disc.
229    ///
230    /// **GameCube**: This will return an empty slice.
231    #[inline]
232    pub fn partitions(&self) -> &[PartitionInfo] { self.reader.partitions() }
233
234    /// Opens a decrypted partition read stream for the specified partition index.
235    ///
236    /// **GameCube**: `index` must always be 0.
237    #[inline]
238    pub fn open_partition(&self, index: usize) -> Result<Box<dyn PartitionBase>> {
239        self.reader.open_partition(index, &self.options)
240    }
241
242    /// Opens a decrypted partition read stream for the first partition matching
243    /// the specified kind.
244    ///
245    /// **GameCube**: `kind` must always be [`PartitionKind::Data`].
246    #[inline]
247    pub fn open_partition_kind(&self, kind: PartitionKind) -> Result<Box<dyn PartitionBase>> {
248        self.reader.open_partition_kind(kind, &self.options)
249    }
250}
251
252impl BufRead for Disc {
253    #[inline]
254    fn fill_buf(&mut self) -> std::io::Result<&[u8]> { self.reader.fill_buf() }
255
256    #[inline]
257    fn consume(&mut self, amt: usize) { self.reader.consume(amt) }
258}
259
260impl Read for Disc {
261    #[inline]
262    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { self.reader.read(buf) }
263}
264
265impl Seek for Disc {
266    #[inline]
267    fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { self.reader.seek(pos) }
268}