diff --git a/src/archive/lz77.rs b/src/archive/lz77.rs new file mode 100644 index 0000000..88fb8e0 --- /dev/null +++ b/src/archive/lz77.rs @@ -0,0 +1,71 @@ +// archive/lz77.rs from rustii (c) 2025 NinjaCheetah & Contributors +// https://github.com/NinjaCheetah/rustii +// +// Implements the compression and decompression routines used for the Wii's LZ77 compression scheme. + +use std::io::{Cursor, Read, Seek, SeekFrom}; +use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum LZ77Error { + #[error("compression is type `{0}` but only 0x10 is supported")] + InvalidCompressionType(u8), + #[error("LZ77 data is not in a valid format")] + IO(#[from] std::io::Error), +} + +/// Decompresses LZ77-compressed data and returns the decompressed result. +pub fn decompress_lz77(data: &[u8]) -> Result, LZ77Error> { + let mut buf = Cursor::new(data); + // Check for magic so that we know where to start. If the compressed data was sourced from + // inside of something, it may not have the magic and instead starts immediately at 0. + let mut magic = [0u8; 4]; + buf.read_exact(&mut magic)?; + if &magic != b"LZ77" { + buf.seek(SeekFrom::Start(0))?; + } + // Read one byte to ensure this is compression type 0x10. Nintendo used other types, but only + // 0x10 was supported on the Wii. + let compression_type = buf.read_u8()?; + if compression_type != 0x10 { + return Err(LZ77Error::InvalidCompressionType(compression_type)); + } + // Read the decompressed size, which is stored as 3 LE bytes for some reason. + let decompressed_size = buf.read_u24::()? as usize; + let mut out_buf = vec![0u8; decompressed_size]; + let mut pos = 0; + while pos < decompressed_size { + let flag = buf.read_u8()?; + // Read bits in flag from most to least significant. + let mut x = 7; + while x >= 0 { + // Prevents buffer overrun if the final flag is only partially used. + if pos >= decompressed_size { + break; + } + // Bit is 1, which is a reference to previous data in the file. + if flag & (1 << x) != 0 { + let reference = buf.read_u16::()?; + let length = 3 + ((reference >> 12) & 0xF); + let mut offset = pos - (reference & 0xFFF) as usize - 1; + for _ in 0..length { + out_buf[pos] = out_buf[offset]; + pos += 1; + offset += 1; + // Avoids a buffer overrun if the copy length would extend past the end of the file. + if pos >= decompressed_size { + break; + } + } + } + // Bit is 0, which is a direct byte copy. + else { + out_buf[pos] = buf.read_u8()?; + pos += 1; + } + x -= 1; + } + } + Ok(out_buf) +} diff --git a/src/archive/mod.rs b/src/archive/mod.rs index eca7b5a..6c4d210 100644 --- a/src/archive/mod.rs +++ b/src/archive/mod.rs @@ -3,4 +3,5 @@ // // Root for all archive-related modules. +pub mod lz77; pub mod u8; diff --git a/src/bin/rustii/archive/lz77.rs b/src/bin/rustii/archive/lz77.rs new file mode 100644 index 0000000..796dd5a --- /dev/null +++ b/src/bin/rustii/archive/lz77.rs @@ -0,0 +1,51 @@ +// archive/lz77.rs from rustii (c) 2025 NinjaCheetah & Contributors +// https://github.com/NinjaCheetah/rustii +// +// Code for the LZ77 compression/decompression commands in the rustii CLI. + +use std::{str, fs}; +use std::path::{Path, PathBuf}; +use anyhow::{bail, Context, Result}; +use clap::Subcommand; +use rustii::archive::lz77; + +#[derive(Subcommand)] +#[command(arg_required_else_help = true)] +pub enum Commands { + /// Compress a file with LZ77 compression (NOT IMPLEMENTED) + Compress { + /// The path to the file to compress + input: String, + /// An optional output name; defaults to .lz77 + #[arg(short, long)] + output: Option, + }, + /// Decompress an LZ77-compressed file + Decompress { + /// The path to the file to decompress + input: String, + /// An optional output name; defaults to .out + #[arg(short, long)] + output: Option, + } +} + +pub fn compress_lz77(_input: &str, _output: &Option) -> Result<()> { + bail!("compression is not yet implemented"); +} + +pub fn decompress_lz77(input: &str, output: &Option) -> Result<()> { + let in_path = Path::new(input); + if !in_path.exists() { + bail!("Compressed file \"{}\" could not be found.", in_path.display()); + } + let compressed = fs::read(in_path)?; + let decompressed = lz77::decompress_lz77(&compressed).with_context(|| "An unknown error occurred while decompressing the data.")?; + let out_path = if output.is_some() { + PathBuf::from(output.clone().unwrap()) + } else { + PathBuf::from(in_path).with_extension("out") + }; + fs::write(out_path, decompressed)?; + Ok(()) +} diff --git a/src/bin/rustii/archive/mod.rs b/src/bin/rustii/archive/mod.rs new file mode 100644 index 0000000..834a339 --- /dev/null +++ b/src/bin/rustii/archive/mod.rs @@ -0,0 +1,4 @@ +// archive/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors +// https://github.com/NinjaCheetah/rustii + +pub mod lz77; diff --git a/src/bin/rustii/main.rs b/src/bin/rustii/main.rs index fdb19d3..7a0437c 100644 --- a/src/bin/rustii/main.rs +++ b/src/bin/rustii/main.rs @@ -3,13 +3,13 @@ // // Base for the rustii CLI that handles argument parsing and directs execution to the proper module. +mod archive; mod title; mod filetypes; mod info; use anyhow::Result; use clap::{Subcommand, Parser}; -use title::{wad, fakesign}; #[derive(Parser)] #[command(version, about, long_about = None)] @@ -21,11 +21,6 @@ struct Cli { #[derive(Subcommand)] #[command(arg_required_else_help = true)] enum Commands { - /// Pack/unpack/edit a WAD file - Wad { - #[command(subcommand)] - command: Option, - }, /// Fakesign a TMD, Ticket, or WAD (trucha bug) Fakesign { /// The path to a TMD, Ticket, or WAD @@ -38,34 +33,53 @@ enum Commands { Info { /// The path to a TMD, Ticket, or WAD input: String, - } + }, + /// Compress/decompress data using LZ77 compression + Lz77 { + #[command(subcommand)] + command: archive::lz77::Commands + }, + /// Pack/unpack/edit a WAD file + Wad { + #[command(subcommand)] + command: title::wad::Commands, + }, } fn main() -> Result<()> { let cli = Cli::parse(); match &cli.command { - Some(Commands::Wad { command }) => { - match command { - Some(wad::Commands::Convert { input, target, output }) => { - wad::convert_wad(input, target, output)? - }, - Some(wad::Commands::Pack { input, output}) => { - wad::pack_wad(input, output)? - }, - Some(wad::Commands::Unpack { input, output }) => { - wad::unpack_wad(input, output)? - }, - &None => { /* This is for me handled by clap */} - } - }, Some(Commands::Fakesign { input, output }) => { - fakesign::fakesign(input, output)? + title::fakesign::fakesign(input, output)? + }, + Some(Commands::Lz77 { command }) => { + match command { + archive::lz77::Commands::Compress { input, output } => { + archive::lz77::compress_lz77(input, output)? + }, + archive::lz77::Commands::Decompress { input, output } => { + archive::lz77::decompress_lz77(input, output)? + } + } }, Some(Commands::Info { input }) => { info::info(input)? - } - None => {} + }, + Some(Commands::Wad { command }) => { + match command { + title::wad::Commands::Convert { input, target, output } => { + title::wad::convert_wad(input, target, output)? + }, + title::wad::Commands::Pack { input, output} => { + title::wad::pack_wad(input, output)? + }, + title::wad::Commands::Unpack { input, output } => { + title::wad::unpack_wad(input, output)? + } + } + }, + None => { /* Clap handles no passed command by itself */} } Ok(()) } diff --git a/src/bin/rustii/title/wad.rs b/src/bin/rustii/title/wad.rs index 424e73a..9895baa 100644 --- a/src/bin/rustii/title/wad.rs +++ b/src/bin/rustii/title/wad.rs @@ -18,7 +18,7 @@ pub enum Commands { Convert { /// The path to the WAD to convert input: String, - /// An (optional) WAD name; defaults to _.wad + /// An optional WAD name; defaults to _.wad #[arg(short, long)] output: Option, #[command(flatten)]