Fixes & bump to version 1.0.0

This commit is contained in:
Luke Street 2024-02-22 23:49:28 -07:00
parent 7e6d880792
commit 1895b7df3f
10 changed files with 78 additions and 67 deletions

View File

@ -19,7 +19,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
@ -37,7 +37,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
# We use nightly options in rustfmt.toml # We use nightly options in rustfmt.toml
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@nightly
@ -58,7 +58,7 @@ jobs:
# Prevent new advisories from failing CI # Prevent new advisories from failing CI
continue-on-error: ${{ matrix.checks == 'advisories' }} continue-on-error: ${{ matrix.checks == 'advisories' }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
with: with:
command: check ${{ matrix.checks }} command: check ${{ matrix.checks }}
@ -72,7 +72,7 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cargo test - name: Cargo test
@ -127,7 +127,7 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
if: matrix.packages != '' if: matrix.packages != ''
run: | run: |
@ -143,7 +143,7 @@ jobs:
- name: Cargo build - name: Cargo build
run: cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }} run: cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.name }} name: ${{ matrix.name }}
path: | path: |
@ -160,7 +160,7 @@ jobs:
needs: [ build ] needs: [ build ]
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
path: artifacts path: artifacts
- name: Rename artifacts - name: Rename artifacts
@ -172,6 +172,6 @@ jobs:
done done
ls -R ../out ls -R ../out
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981
with: with:
files: out/* files: out/*

4
Cargo.lock generated
View File

@ -411,7 +411,7 @@ dependencies = [
[[package]] [[package]]
name = "nod" name = "nod"
version = "0.2.0" version = "1.0.0"
dependencies = [ dependencies = [
"adler", "adler",
"aes", "aes",
@ -434,7 +434,7 @@ dependencies = [
[[package]] [[package]]
name = "nodtool" name = "nodtool"
version = "0.2.0" version = "1.0.0"
dependencies = [ dependencies = [
"argp", "argp",
"base16ct", "base16ct",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "nod" name = "nod"
version = "0.2.0" version = "1.0.0"
edition = "2021" edition = "2021"
rust-version = "1.73.0" rust-version = "1.73.0"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
@ -23,20 +23,20 @@ compress-zlib = ["adler", "miniz_oxide"]
compress-zstd = ["zstd"] compress-zstd = ["zstd"]
[dependencies] [dependencies]
adler = { version = "1.0.2", optional = true } adler = { version = "1.0", optional = true }
aes = "0.8.4" aes = "0.8"
base16ct = "0.2.0" base16ct = "0.2"
bzip2 = { version = "0.4.4", features = ["static"], optional = true } bzip2 = { version = "0.4", features = ["static"], optional = true }
cbc = "0.1.2" cbc = "0.1"
digest = "0.10.7" digest = "0.10"
dyn-clone = "1.0.16" dyn-clone = "1.0"
encoding_rs = "0.8.33" encoding_rs = "0.8"
itertools = "0.12.1" itertools = "0.12"
liblzma = { version = "0.2.3", features = ["static"], optional = true } liblzma = { version = "0.2", features = ["static"], optional = true }
log = "0.4.20" log = "0.4"
miniz_oxide = { version = "0.7.2", optional = true } miniz_oxide = { version = "0.7", optional = true }
rayon = "1.8.1" rayon = "1.8"
sha1 = "0.10.6" sha1 = "0.10"
thiserror = "1.0.57" thiserror = "1.0"
zerocopy = { version = "0.7.32", features = ["alloc", "derive"] } zerocopy = { version = "0.7", features = ["alloc", "derive"] }
zstd = { version = "0.13.0", optional = true } zstd = { version = "0.13", optional = true }

View File

@ -118,7 +118,7 @@ impl PartitionBase for PartitionGC {
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> { fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
assert_eq!(node.kind(), NodeKind::File); assert_eq!(node.kind(), NodeKind::File);
self.new_window(node.offset(false), node.length(false)) self.new_window(node.offset(false), node.length())
} }
fn ideal_buffer_size(&self) -> usize { SECTOR_SIZE } fn ideal_buffer_size(&self) -> usize { SECTOR_SIZE }

View File

@ -437,7 +437,7 @@ impl PartitionBase for PartitionWii {
fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> { fn open_file(&mut self, node: &Node) -> io::Result<SharedWindowedReadStream> {
assert_eq!(node.kind(), NodeKind::File); assert_eq!(node.kind(), NodeKind::File);
self.new_window(node.offset(true), node.length(true)) self.new_window(node.offset(true), node.length())
} }
fn ideal_buffer_size(&self) -> usize { SECTOR_DATA_SIZE } fn ideal_buffer_size(&self) -> usize { SECTOR_DATA_SIZE }

View File

@ -63,18 +63,12 @@ impl Node {
} }
} }
/// For files, this is the byte size of the file. (Wii: >> 2) /// For files, this is the byte size of the file.
/// ///
/// For directories, this is the child end index in the FST. /// For directories, this is the child end index in the FST.
/// ///
/// Number of child files and directories recursively is `length - offset`. /// Number of child files and directories recursively is `length - offset`.
pub fn length(&self, is_wii: bool) -> u64 { pub fn length(&self) -> u64 { self.length.get() as u64 }
if is_wii && self.kind == 0 {
self.length.get() as u64 * 4
} else {
self.length.get() as u64
}
}
} }
/// A view into the file system tree (FST). /// A view into the file system tree (FST).
@ -90,7 +84,7 @@ impl<'a> Fst<'a> {
return Err("FST root node not found"); return Err("FST root node not found");
}; };
// String table starts after the last node // String table starts after the last node
let string_base = root_node.length(false) * size_of::<Node>() as u64; let string_base = root_node.length() * size_of::<Node>() as u64;
if string_base >= buf.len() as u64 { if string_base >= buf.len() as u64 {
return Err("FST string table out of bounds"); return Err("FST string table out of bounds");
} }
@ -137,10 +131,10 @@ impl<'a> Fst<'a> {
} }
// Descend into directory // Descend into directory
idx += 1; idx += 1;
stop_at = Some(node.length(false) as usize + idx); stop_at = Some(node.length() as usize + idx);
} else if node.is_dir() { } else if node.is_dir() {
// Skip directory // Skip directory
idx = node.length(false) as usize; idx = node.length() as usize;
} else { } else {
// Skip file // Skip file
idx += 1; idx += 1;

View File

@ -102,7 +102,7 @@ impl BlockIO for DiscIOCISO {
let phys_block = self.block_map[block as usize]; let phys_block = self.block_map[block as usize];
if phys_block == u16::MAX { if phys_block == u16::MAX {
// Check if block is junk data // Check if block is junk data
if self.nkit_header.as_ref().is_some_and(|h| h.is_junk_block(block).unwrap_or(false)) { if self.nkit_header.as_ref().and_then(|h| h.is_junk_block(block)).unwrap_or(false) {
return Ok(Block::Junk); return Ok(Block::Junk);
}; };

View File

@ -59,7 +59,7 @@ pub struct DiscIOWBFS {
/// WBFS header /// WBFS header
header: WBFSHeader, header: WBFSHeader,
/// Map of Wii LBAs to WBFS LBAs /// Map of Wii LBAs to WBFS LBAs
block_table: Box<[U16]>, block_map: Box<[U16]>,
/// Optional NKit header /// Optional NKit header
nkit_header: Option<NKitHeader>, nkit_header: Option<NKitHeader>,
} }
@ -91,11 +91,11 @@ impl DiscIOWBFS {
return Err(Error::DiscFormat("Only single WBFS discs are supported".to_string())); return Err(Error::DiscFormat("Only single WBFS discs are supported".to_string()));
} }
// Read WBFS LBA table // Read WBFS LBA map
inner inner
.seek(SeekFrom::Start(header.sector_size() as u64 + DISC_HEADER_SIZE as u64)) .seek(SeekFrom::Start(header.sector_size() as u64 + DISC_HEADER_SIZE as u64))
.context("Seeking to WBFS LBA table")?; // Skip header .context("Seeking to WBFS LBA table")?; // Skip header
let block_table: Box<[U16]> = read_box_slice(&mut inner, header.max_blocks() as usize) let block_map: Box<[U16]> = read_box_slice(&mut inner, header.max_blocks() as usize)
.context("Reading WBFS LBA table")?; .context("Reading WBFS LBA table")?;
// Read NKit header if present (always at 0x10000) // Read NKit header if present (always at 0x10000)
@ -104,7 +104,7 @@ impl DiscIOWBFS {
// Reset reader // Reset reader
inner.reset(); inner.reset();
Ok(Box::new(Self { inner, header, block_table, nkit_header })) Ok(Box::new(Self { inner, header, block_map, nkit_header }))
} }
} }
@ -120,16 +120,20 @@ impl BlockIO for DiscIOWBFS {
return Ok(Block::Zero); return Ok(Block::Zero);
} }
// Check if block is junk data // Find the block in the map
if self.nkit_header.as_ref().and_then(|h| h.is_junk_block(block)).unwrap_or(false) { let phys_block = self.block_map[block as usize].get();
return Ok(Block::Junk); if phys_block == 0 {
// Check if block is junk data
if self.nkit_header.as_ref().and_then(|h| h.is_junk_block(block)).unwrap_or(false) {
return Ok(Block::Junk);
}
// Otherwise, read zeroes
return Ok(Block::Zero);
} }
// Read block // Read block
let block_start = block_size as u64 * self.block_table[block as usize].get() as u64; let block_start = block_size as u64 * phys_block as u64;
if block_start == 0 {
return Ok(Block::Zero);
}
self.inner.seek(SeekFrom::Start(block_start))?; self.inner.seek(SeekFrom::Start(block_start))?;
self.inner.read_exact(out)?; self.inner.read_exact(out)?;
Ok(Block::Raw) Ok(Block::Raw)

View File

@ -1,6 +1,6 @@
[package] [package]
name = "nodtool" name = "nodtool"
version = "0.2.0" version = "1.0.0"
edition = "2021" edition = "2021"
rust-version = "1.73.0" rust-version = "1.73.0"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]

View File

@ -31,7 +31,7 @@ use size::{Base, Size};
use supports_color::Stream; use supports_color::Stream;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use zerocopy::FromZeroes; use zerocopy::{AsBytes, FromZeroes};
#[derive(FromArgs, Debug)] #[derive(FromArgs, Debug)]
/// Tool for reading GameCube and Wii disc images. /// Tool for reading GameCube and Wii disc images.
@ -543,44 +543,46 @@ fn extract(args: ExtractArgs) -> Result<()> {
rebuild_encryption: false, rebuild_encryption: false,
validate_hashes: args.validate, validate_hashes: args.validate,
})?; })?;
let is_wii = disc.header().is_wii(); let header = disc.header();
let is_wii = header.is_wii();
if let Some(partition) = args.partition { if let Some(partition) = args.partition {
if partition.eq_ignore_ascii_case("all") { if partition.eq_ignore_ascii_case("all") {
for info in disc.partitions() { for info in disc.partitions() {
let mut out_dir = output_dir.clone(); let mut out_dir = output_dir.clone();
out_dir.push(info.kind.dir_name().as_ref()); out_dir.push(info.kind.dir_name().as_ref());
let mut partition = disc.open_partition(info.index)?; let mut partition = disc.open_partition(info.index)?;
extract_partition(partition.as_mut(), &out_dir, is_wii, args.quiet)?; extract_partition(header, partition.as_mut(), &out_dir, is_wii, args.quiet)?;
} }
} else if partition.eq_ignore_ascii_case("data") { } else if partition.eq_ignore_ascii_case("data") {
let mut partition = disc.open_partition_kind(PartitionKind::Data)?; let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
extract_partition(partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} else if partition.eq_ignore_ascii_case("update") { } else if partition.eq_ignore_ascii_case("update") {
let mut partition = disc.open_partition_kind(PartitionKind::Update)?; let mut partition = disc.open_partition_kind(PartitionKind::Update)?;
extract_partition(partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} else if partition.eq_ignore_ascii_case("channel") { } else if partition.eq_ignore_ascii_case("channel") {
let mut partition = disc.open_partition_kind(PartitionKind::Channel)?; let mut partition = disc.open_partition_kind(PartitionKind::Channel)?;
extract_partition(partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} else { } else {
let idx = partition.parse::<usize>().map_err(|_| "Invalid partition index")?; let idx = partition.parse::<usize>().map_err(|_| "Invalid partition index")?;
let mut partition = disc.open_partition(idx)?; let mut partition = disc.open_partition(idx)?;
extract_partition(partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} }
} else { } else {
let mut partition = disc.open_partition_kind(PartitionKind::Data)?; let mut partition = disc.open_partition_kind(PartitionKind::Data)?;
extract_partition(partition.as_mut(), &output_dir, is_wii, args.quiet)?; extract_partition(header, partition.as_mut(), &output_dir, is_wii, args.quiet)?;
} }
Ok(()) Ok(())
} }
fn extract_partition( fn extract_partition(
header: &DiscHeader,
partition: &mut dyn PartitionBase, partition: &mut dyn PartitionBase,
out_dir: &Path, out_dir: &Path,
is_wii: bool, is_wii: bool,
quiet: bool, quiet: bool,
) -> Result<()> { ) -> Result<()> {
let meta = partition.meta()?; let meta = partition.meta()?;
extract_sys_files(meta.as_ref(), out_dir, quiet)?; extract_sys_files(header, meta.as_ref(), out_dir, quiet)?;
// Extract FST // Extract FST
let files_dir = out_dir.join("files"); let files_dir = out_dir.join("files");
@ -601,7 +603,7 @@ fn extract_partition(
path_segments.truncate(new_size); path_segments.truncate(new_size);
// Add the new path segment // Add the new path segment
let end = if node.is_dir() { node.length(false) as usize } else { idx + 1 }; let end = if node.is_dir() { node.length() as usize } else { idx + 1 };
path_segments.push((name?, end)); path_segments.push((name?, end));
let path = path_segments.iter().map(|(name, _)| name.as_ref()).join("/"); let path = path_segments.iter().map(|(name, _)| name.as_ref()).join("/");
@ -615,7 +617,12 @@ fn extract_partition(
Ok(()) Ok(())
} }
fn extract_sys_files(data: &PartitionMeta, out_dir: &Path, quiet: bool) -> Result<()> { fn extract_sys_files(
header: &DiscHeader,
data: &PartitionMeta,
out_dir: &Path,
quiet: bool,
) -> Result<()> {
let sys_dir = out_dir.join("sys"); let sys_dir = out_dir.join("sys");
fs::create_dir_all(&sys_dir) fs::create_dir_all(&sys_dir)
.with_context(|| format!("Creating directory {}", display(&sys_dir)))?; .with_context(|| format!("Creating directory {}", display(&sys_dir)))?;
@ -626,6 +633,12 @@ fn extract_sys_files(data: &PartitionMeta, out_dir: &Path, quiet: bool) -> Resul
extract_file(data.raw_dol.as_ref(), &sys_dir.join("main.dol"), quiet)?; extract_file(data.raw_dol.as_ref(), &sys_dir.join("main.dol"), quiet)?;
// Wii files // Wii files
if header.is_wii() {
let disc_dir = out_dir.join("disc");
fs::create_dir_all(&disc_dir)
.with_context(|| format!("Creating directory {}", display(&disc_dir)))?;
extract_file(&header.as_bytes()[..0x100], &disc_dir.join("header.bin"), quiet)?;
}
if let Some(ticket) = data.raw_ticket.as_deref() { if let Some(ticket) = data.raw_ticket.as_deref() {
extract_file(ticket, &out_dir.join("ticket.bin"), quiet)?; extract_file(ticket, &out_dir.join("ticket.bin"), quiet)?;
} }
@ -666,7 +679,7 @@ fn extract_node(
println!( println!(
"Extracting {} (size: {})", "Extracting {} (size: {})",
display(&file_path), display(&file_path),
Size::from_bytes(node.length(is_wii)).format().with_base(Base::Base10) Size::from_bytes(node.length()).format().with_base(Base::Base10)
); );
} }
let file = File::create(&file_path) let file = File::create(&file_path)
@ -677,7 +690,7 @@ fn extract_node(
"Opening file {} on disc for reading (offset {}, size {})", "Opening file {} on disc for reading (offset {}, size {})",
name, name,
node.offset(is_wii), node.offset(is_wii),
node.length(is_wii) node.length()
) )
})?; })?;
io::copy(&mut r, &mut w).with_context(|| format!("Extracting file {}", display(&file_path)))?; io::copy(&mut r, &mut w).with_context(|| format!("Extracting file {}", display(&file_path)))?;