mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-05 23:11:02 -04:00
Added LZ77 decompression, added corresponding CLI command
This commit is contained in:
parent
c2169f84c4
commit
e1190e1e58
71
src/archive/lz77.rs
Normal file
71
src/archive/lz77.rs
Normal file
@ -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<Vec<u8>, 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::<LittleEndian>()? 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::<BigEndian>()?;
|
||||
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)
|
||||
}
|
@ -3,4 +3,5 @@
|
||||
//
|
||||
// Root for all archive-related modules.
|
||||
|
||||
pub mod lz77;
|
||||
pub mod u8;
|
||||
|
51
src/bin/rustii/archive/lz77.rs
Normal file
51
src/bin/rustii/archive/lz77.rs
Normal file
@ -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 <input name>.lz77
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
},
|
||||
/// Decompress an LZ77-compressed file
|
||||
Decompress {
|
||||
/// The path to the file to decompress
|
||||
input: String,
|
||||
/// An optional output name; defaults to <input name>.out
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compress_lz77(_input: &str, _output: &Option<String>) -> Result<()> {
|
||||
bail!("compression is not yet implemented");
|
||||
}
|
||||
|
||||
pub fn decompress_lz77(input: &str, output: &Option<String>) -> 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(())
|
||||
}
|
4
src/bin/rustii/archive/mod.rs
Normal file
4
src/bin/rustii/archive/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
// archive/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
|
||||
pub mod lz77;
|
@ -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<wad::Commands>,
|
||||
},
|
||||
/// 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)?
|
||||
},
|
||||
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 => {}
|
||||
}
|
||||
},
|
||||
None => { /* Clap handles no passed command by itself */}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ pub enum Commands {
|
||||
Convert {
|
||||
/// The path to the WAD to convert
|
||||
input: String,
|
||||
/// An (optional) WAD name; defaults to <input name>_<new type>.wad
|
||||
/// An optional WAD name; defaults to <input name>_<new type>.wad
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
#[command(flatten)]
|
||||
|
Loading…
x
Reference in New Issue
Block a user