diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 54db1a8..9e1f511 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,7 +19,7 @@ jobs: RUSTFLAGS: -D warnings steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@master with: @@ -37,7 +37,7 @@ jobs: RUSTFLAGS: -D warnings steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Rust toolchain # We use nightly options in rustfmt.toml uses: dtolnay/rust-toolchain@nightly @@ -58,7 +58,7 @@ jobs: # Prevent new advisories from failing CI continue-on-error: ${{ matrix.checks == 'advisories' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 with: command: check ${{ matrix.checks }} @@ -72,7 +72,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Cargo test @@ -127,7 +127,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install dependencies if: matrix.packages != '' run: | @@ -143,7 +143,7 @@ jobs: - name: Cargo build run: cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }} - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} path: | @@ -160,7 +160,7 @@ jobs: needs: [ build ] steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts - name: Rename artifacts @@ -172,6 +172,6 @@ jobs: done ls -R ../out - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981 with: files: out/* diff --git a/Cargo.lock b/Cargo.lock index 072f148..ed2577c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,7 +411,7 @@ dependencies = [ [[package]] name = "nod" -version = "0.2.0" +version = "1.0.0" dependencies = [ "adler", "aes", @@ -434,7 +434,7 @@ dependencies = [ [[package]] name = "nodtool" -version = "0.2.0" +version = "1.0.0" dependencies = [ "argp", "base16ct", diff --git a/nod/Cargo.toml b/nod/Cargo.toml index 431f5b3..71e22c8 100644 --- a/nod/Cargo.toml +++ b/nod/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nod" -version = "0.2.0" +version = "1.0.0" edition = "2021" rust-version = "1.73.0" authors = ["Luke Street "] @@ -23,20 +23,20 @@ compress-zlib = ["adler", "miniz_oxide"] compress-zstd = ["zstd"] [dependencies] -adler = { version = "1.0.2", optional = true } -aes = "0.8.4" -base16ct = "0.2.0" -bzip2 = { version = "0.4.4", features = ["static"], optional = true } -cbc = "0.1.2" -digest = "0.10.7" -dyn-clone = "1.0.16" -encoding_rs = "0.8.33" -itertools = "0.12.1" -liblzma = { version = "0.2.3", features = ["static"], optional = true } -log = "0.4.20" -miniz_oxide = { version = "0.7.2", optional = true } -rayon = "1.8.1" -sha1 = "0.10.6" -thiserror = "1.0.57" -zerocopy = { version = "0.7.32", features = ["alloc", "derive"] } -zstd = { version = "0.13.0", optional = true } +adler = { version = "1.0", optional = true } +aes = "0.8" +base16ct = "0.2" +bzip2 = { version = "0.4", features = ["static"], optional = true } +cbc = "0.1" +digest = "0.10" +dyn-clone = "1.0" +encoding_rs = "0.8" +itertools = "0.12" +liblzma = { version = "0.2", features = ["static"], optional = true } +log = "0.4" +miniz_oxide = { version = "0.7", optional = true } +rayon = "1.8" +sha1 = "0.10" +thiserror = "1.0" +zerocopy = { version = "0.7", features = ["alloc", "derive"] } +zstd = { version = "0.13", optional = true } diff --git a/nod/src/disc/gcn.rs b/nod/src/disc/gcn.rs index cbc3efd..f718a8f 100644 --- a/nod/src/disc/gcn.rs +++ b/nod/src/disc/gcn.rs @@ -118,7 +118,7 @@ impl PartitionBase for PartitionGC { fn open_file(&mut self, node: &Node) -> io::Result { 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 } diff --git a/nod/src/disc/wii.rs b/nod/src/disc/wii.rs index 56354f7..443d47d 100644 --- a/nod/src/disc/wii.rs +++ b/nod/src/disc/wii.rs @@ -437,7 +437,7 @@ impl PartitionBase for PartitionWii { fn open_file(&mut self, node: &Node) -> io::Result { 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 } diff --git a/nod/src/fst.rs b/nod/src/fst.rs index 8f2a474..5568fa9 100644 --- a/nod/src/fst.rs +++ b/nod/src/fst.rs @@ -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. /// /// Number of child files and directories recursively is `length - offset`. - pub fn length(&self, is_wii: bool) -> u64 { - if is_wii && self.kind == 0 { - self.length.get() as u64 * 4 - } else { - self.length.get() as u64 - } - } + pub fn length(&self) -> u64 { self.length.get() as u64 } } /// A view into the file system tree (FST). @@ -90,7 +84,7 @@ impl<'a> Fst<'a> { return Err("FST root node not found"); }; // String table starts after the last node - let string_base = root_node.length(false) * size_of::() as u64; + let string_base = root_node.length() * size_of::() as u64; if string_base >= buf.len() as u64 { return Err("FST string table out of bounds"); } @@ -137,10 +131,10 @@ impl<'a> Fst<'a> { } // Descend into directory idx += 1; - stop_at = Some(node.length(false) as usize + idx); + stop_at = Some(node.length() as usize + idx); } else if node.is_dir() { // Skip directory - idx = node.length(false) as usize; + idx = node.length() as usize; } else { // Skip file idx += 1; diff --git a/nod/src/io/ciso.rs b/nod/src/io/ciso.rs index 3a089bf..9ec8119 100644 --- a/nod/src/io/ciso.rs +++ b/nod/src/io/ciso.rs @@ -102,7 +102,7 @@ impl BlockIO for DiscIOCISO { let phys_block = self.block_map[block as usize]; if phys_block == u16::MAX { // 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); }; diff --git a/nod/src/io/wbfs.rs b/nod/src/io/wbfs.rs index 8981649..0411cf8 100644 --- a/nod/src/io/wbfs.rs +++ b/nod/src/io/wbfs.rs @@ -59,7 +59,7 @@ pub struct DiscIOWBFS { /// WBFS header header: WBFSHeader, /// Map of Wii LBAs to WBFS LBAs - block_table: Box<[U16]>, + block_map: Box<[U16]>, /// Optional NKit header nkit_header: Option, } @@ -91,11 +91,11 @@ impl DiscIOWBFS { return Err(Error::DiscFormat("Only single WBFS discs are supported".to_string())); } - // Read WBFS LBA table + // Read WBFS LBA map inner .seek(SeekFrom::Start(header.sector_size() as u64 + DISC_HEADER_SIZE as u64)) .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")?; // Read NKit header if present (always at 0x10000) @@ -104,7 +104,7 @@ impl DiscIOWBFS { // Reset reader 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); } - // 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); + // Find the block in the map + let phys_block = self.block_map[block as usize].get(); + 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 - let block_start = block_size as u64 * self.block_table[block as usize].get() as u64; - if block_start == 0 { - return Ok(Block::Zero); - } + let block_start = block_size as u64 * phys_block as u64; self.inner.seek(SeekFrom::Start(block_start))?; self.inner.read_exact(out)?; Ok(Block::Raw) diff --git a/nodtool/Cargo.toml b/nodtool/Cargo.toml index 50c8d8c..4ad54f7 100644 --- a/nodtool/Cargo.toml +++ b/nodtool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nodtool" -version = "0.2.0" +version = "1.0.0" edition = "2021" rust-version = "1.73.0" authors = ["Luke Street "] diff --git a/nodtool/src/main.rs b/nodtool/src/main.rs index c2a6393..e323355 100644 --- a/nodtool/src/main.rs +++ b/nodtool/src/main.rs @@ -31,7 +31,7 @@ use size::{Base, Size}; use supports_color::Stream; use tracing::level_filters::LevelFilter; use tracing_subscriber::EnvFilter; -use zerocopy::FromZeroes; +use zerocopy::{AsBytes, FromZeroes}; #[derive(FromArgs, Debug)] /// Tool for reading GameCube and Wii disc images. @@ -543,44 +543,46 @@ fn extract(args: ExtractArgs) -> Result<()> { rebuild_encryption: false, 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 partition.eq_ignore_ascii_case("all") { for info in disc.partitions() { let mut out_dir = output_dir.clone(); out_dir.push(info.kind.dir_name().as_ref()); 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") { 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") { 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") { 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 { let idx = partition.parse::().map_err(|_| "Invalid partition index")?; 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 { 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(()) } fn extract_partition( + header: &DiscHeader, partition: &mut dyn PartitionBase, out_dir: &Path, is_wii: bool, quiet: bool, ) -> Result<()> { 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 let files_dir = out_dir.join("files"); @@ -601,7 +603,7 @@ fn extract_partition( path_segments.truncate(new_size); // 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)); let path = path_segments.iter().map(|(name, _)| name.as_ref()).join("/"); @@ -615,7 +617,12 @@ fn extract_partition( 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"); fs::create_dir_all(&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)?; // 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() { extract_file(ticket, &out_dir.join("ticket.bin"), quiet)?; } @@ -666,7 +679,7 @@ fn extract_node( println!( "Extracting {} (size: {})", 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) @@ -677,7 +690,7 @@ fn extract_node( "Opening file {} on disc for reading (offset {}, size {})", name, node.offset(is_wii), - node.length(is_wii) + node.length() ) })?; io::copy(&mut r, &mut w).with_context(|| format!("Extracting file {}", display(&file_path)))?;