# nod [![Build Status]][actions] [![Latest Version]][crates.io] [![Api Rustdoc]][rustdoc] ![Rust Version] [Build Status]: https://github.com/encounter/nod/actions/workflows/build.yaml/badge.svg [actions]: https://github.com/encounter/nod/actions [Latest Version]: https://img.shields.io/crates/v/nod.svg [crates.io]: https://crates.io/crates/nod [Api Rustdoc]: https://img.shields.io/badge/api-rustdoc-blue.svg [rustdoc]: https://docs.rs/nod [Rust Version]: https://img.shields.io/badge/rust-1.85+-blue.svg?maxAge=3600 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 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, read-only) - GCZ - TGC ## CLI tool This crate includes a command-line tool called `nodtool`. Download the latest release from the [releases page](https://github.com/encounter/nod-rs/releases), or install it using Cargo: ```shell cargo install --locked nodtool ``` ### info Displays information about a disc image. ```shell nodtool info /path/to/game.iso ``` ### extract Extracts the contents of a disc image to a directory. ```shell nodtool extract /path/to/game.iso [outdir] ``` For Wii U VC titles, use `content/hif_000000.nfs`: ```shell nodtool extract /path/to/game/content/hif_000000.nfs [outdir] ``` ### convert Converts a disc image to any supported format. See `nodtool convert --help` for more information. ```shell nodtool convert /path/to/game.iso /path/to/game.rvz ``` ### verify Verifies a disc image against an internal Redump database. ```shell nodtool verify /path/to/game.iso ``` ## Library example 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 = 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"); // Find a file by path and read it into a string. if let Some((_, node)) = fst.find("/MP3/Worlds.txt") { let mut s = String::new(); partition .open_file(node) .expect("Failed to open file stream") .read_to_string(&mut s) .expect("Failed to read file"); println!("{}", s); } ``` Converting a disc image to raw ISO: ```rust use nod::read::{DiscOptions, DiscReader, PartitionEncryption}; 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"); 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 Licensed under either of - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.