mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-05 23:11:02 -04:00
Added ASH decompression, added corresponding CLI command
Also cleaned up some minor parts of the LZ77 (de)compression library and CLI code
This commit is contained in:
parent
42fd523843
commit
e55edc10fd
217
src/archive/ash.rs
Normal file
217
src/archive/ash.rs
Normal file
@ -0,0 +1,217 @@
|
||||
// archive/ash.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Implements the decompression routines used for the Wii's ASH compression scheme.
|
||||
// May someday even include the compression routines! If I ever get around to it.
|
||||
//
|
||||
// This code is MESSY. It's a weird combination of Garhoogin's C implementation and my Python
|
||||
// implementation of his C implementation. It should definitely be rewritten someday.
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
use byteorder::{ByteOrder, BigEndian};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ASHError {
|
||||
#[error("this does not appear to be ASH-compressed data (missing magic number)")]
|
||||
NotASHData,
|
||||
#[error("ASH data is invalid")]
|
||||
InvalidData,
|
||||
#[error("LZ77 data is not in a valid format")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
const TREE_RIGHT: u32 = 0x80000000;
|
||||
const TREE_LEFT: u32 = 0x40000000;
|
||||
const TREE_VAL_MASK: u32 = 0x3FFFFFFF;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ASHBitReader<'a> {
|
||||
src: &'a [u8],
|
||||
size: u32,
|
||||
src_pos: u32,
|
||||
word: u32,
|
||||
bit_capacity: u32,
|
||||
}
|
||||
|
||||
fn ash_bit_reader_feed_word(reader: &mut ASHBitReader) -> Result<(), ASHError> {
|
||||
// Ensure that there's enough data to read en entire word, then if there is, read one.
|
||||
if reader.src_pos + 4 > reader.size {
|
||||
return Err(ASHError::InvalidData);
|
||||
}
|
||||
reader.word = BigEndian::read_u32(&reader.src[reader.src_pos as usize..reader.src_pos as usize + 4]);
|
||||
reader.bit_capacity = 0;
|
||||
reader.src_pos += 4;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ash_bit_reader_init(src: &[u8], size: u32, startpos: u32) -> Result<ASHBitReader, ASHError> {
|
||||
// Load data into a bit reader, then have it read its first word.
|
||||
let mut reader = ASHBitReader {
|
||||
src,
|
||||
size,
|
||||
src_pos: startpos,
|
||||
word: 0,
|
||||
bit_capacity: 0,
|
||||
};
|
||||
ash_bit_reader_feed_word(&mut reader)?;
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
fn ash_bit_reader_read_bit(reader: &mut ASHBitReader) -> Result<u32, ASHError> {
|
||||
// Reads the starting bit of the current word in the provided bit reader. If the capacity is at
|
||||
// 31, then we've shifted through the entire word, so a new one should be fed. If not, increase
|
||||
// the capacity by one and shift the current word left.
|
||||
let bit: u32 = reader.word >> 31;
|
||||
if reader.bit_capacity == 31 {
|
||||
ash_bit_reader_feed_word(reader)?;
|
||||
} else {
|
||||
reader.bit_capacity += 1;
|
||||
reader.word <<= 1;
|
||||
}
|
||||
Ok(bit)
|
||||
}
|
||||
|
||||
fn ash_bit_reader_read_bits(reader: &mut ASHBitReader, num_bits: u32) -> Result<u32, ASHError> {
|
||||
// Reads a series of bytes from the current word in the supplied bit reader.
|
||||
let mut bits: u32;
|
||||
let next_bit = reader.bit_capacity + num_bits;
|
||||
if next_bit <= 32 {
|
||||
bits = reader.word >> (32 - num_bits);
|
||||
if next_bit != 32 {
|
||||
reader.word <<= num_bits;
|
||||
reader.bit_capacity += num_bits;
|
||||
} else {
|
||||
ash_bit_reader_feed_word(reader)?;
|
||||
}
|
||||
} else {
|
||||
bits = reader.word >> (32 - num_bits);
|
||||
ash_bit_reader_feed_word(reader)?;
|
||||
bits |= reader.word >> (64 - next_bit);
|
||||
reader.word <<= next_bit - 32;
|
||||
reader.bit_capacity = next_bit - 32;
|
||||
}
|
||||
Ok(bits)
|
||||
}
|
||||
|
||||
fn ash_read_tree(reader: &mut ASHBitReader, width: u32, left_tree: &mut [u32], right_tree: &mut [u32]) -> Result<u32, ASHError> {
|
||||
// Read either the symbol or distance tree from the ASH file, and return the root of that tree.
|
||||
let mut work = vec![0; 2 * (1 << width)];
|
||||
let mut work_pos = 0;
|
||||
|
||||
let mut r23: u32 = 1 << width;
|
||||
let mut tree_root: u32 = 0;
|
||||
let mut num_nodes: u32 = 0;
|
||||
loop {
|
||||
if ash_bit_reader_read_bit(reader)? != 0 {
|
||||
work[work_pos] = r23 | TREE_RIGHT;
|
||||
work_pos += 1;
|
||||
work[work_pos] = r23 | TREE_LEFT;
|
||||
work_pos += 1;
|
||||
num_nodes += 2;
|
||||
r23 += 1;
|
||||
} else {
|
||||
tree_root = ash_bit_reader_read_bits(reader, width)?;
|
||||
loop {
|
||||
work_pos -= 1;
|
||||
let node_value: u32 = work[work_pos];
|
||||
let idx = node_value & TREE_VAL_MASK;
|
||||
num_nodes -= 1;
|
||||
if (node_value & TREE_RIGHT) != 0 {
|
||||
right_tree[idx as usize] = tree_root;
|
||||
tree_root = idx;
|
||||
} else {
|
||||
left_tree[idx as usize] = tree_root;
|
||||
break;
|
||||
}
|
||||
if num_nodes == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if num_nodes == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(tree_root)
|
||||
}
|
||||
|
||||
fn ash_decompress_main(data: &[u8], size: u32, sym_bits: u32, dist_bits: u32) -> Result<Vec<u8>, ASHError> {
|
||||
let mut decompressed_size: u32 = BigEndian::read_u32(&data[0x4..0x8]) & 0x00FFFFFF;
|
||||
|
||||
let mut buf = vec![0u8; decompressed_size as usize];
|
||||
let mut buf_pos: usize = 0;
|
||||
|
||||
let mut reader1 = ash_bit_reader_init(data, size, BigEndian::read_u32(&data[0x8..0xC]))?;
|
||||
let mut reader2 = ash_bit_reader_init(data, size, 0xC)?;
|
||||
|
||||
let sym_max: u32 = 1 << sym_bits;
|
||||
let dist_max: u32 = 1 << dist_bits;
|
||||
|
||||
let mut sym_left_tree = vec![0u32; (2 * sym_max - 1) as usize];
|
||||
let mut sym_right_tree = vec![0u32; (2 * sym_max - 1) as usize];
|
||||
let mut dist_left_tree = vec![0u32; (2 * dist_max - 1) as usize];
|
||||
let mut dist_right_tree = vec![0u32; (2 * dist_max - 1) as usize];
|
||||
|
||||
let sym_root = ash_read_tree(&mut reader2, sym_bits, &mut sym_left_tree, &mut sym_right_tree)?;
|
||||
let dist_root = ash_read_tree(&mut reader1, dist_bits, &mut dist_left_tree, &mut dist_right_tree)?;
|
||||
|
||||
// Main decompression loop.
|
||||
loop {
|
||||
let mut sym = sym_root;
|
||||
while sym >= sym_max {
|
||||
if ash_bit_reader_read_bit(&mut reader2)? != 0 {
|
||||
sym = sym_right_tree[sym as usize];
|
||||
} else {
|
||||
sym = sym_left_tree[sym as usize];
|
||||
}
|
||||
}
|
||||
if sym < 0x100 {
|
||||
buf[buf_pos] = sym as u8;
|
||||
buf_pos += 1;
|
||||
decompressed_size -= 1;
|
||||
} else {
|
||||
let mut dist_sym = dist_root;
|
||||
while dist_sym >= dist_max {
|
||||
if ash_bit_reader_read_bit(&mut reader1)? != 0 {
|
||||
dist_sym = dist_right_tree[dist_sym as usize];
|
||||
} else {
|
||||
dist_sym = dist_left_tree[dist_sym as usize];
|
||||
}
|
||||
}
|
||||
let mut copy_len = (sym - 0x100) + 3;
|
||||
let mut src_pos = buf_pos - dist_sym as usize - 1;
|
||||
if copy_len > decompressed_size {
|
||||
return Err(ASHError::InvalidData);
|
||||
}
|
||||
|
||||
decompressed_size -= copy_len;
|
||||
while copy_len > 0 {
|
||||
buf[buf_pos] = buf[src_pos];
|
||||
buf_pos += 1;
|
||||
src_pos += 1;
|
||||
copy_len -= 1;
|
||||
}
|
||||
}
|
||||
if decompressed_size == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Decompresses ASH-compressed data and returns the decompressed result.
|
||||
pub fn decompress_ash(data: &[u8], sym_tree_bits: Option<u8>, dist_tree_bits: Option<u8>) -> Result<Vec<u8>, ASHError> {
|
||||
let mut buf = Cursor::new(data);
|
||||
// Check for magic "ASH0" to make sure that this is actually ASH data.
|
||||
let mut magic = [0u8; 4];
|
||||
buf.read_exact(&mut magic)?;
|
||||
if &magic != b"ASH0" {
|
||||
return Err(ASHError::NotASHData);
|
||||
}
|
||||
// Unwrap passed bit lengths or use defaults.
|
||||
let sym_tree_bits = sym_tree_bits.unwrap_or(9) as u32;
|
||||
let dist_tree_bits = dist_tree_bits.unwrap_or(11) as u32;
|
||||
let decompressed_data = ash_decompress_main(data, buf.get_ref().len() as u32, sym_tree_bits, dist_tree_bits)?;
|
||||
Ok(decompressed_data)
|
||||
}
|
@ -135,7 +135,7 @@ pub fn compress_lz77(data: &[u8]) -> Result<Vec<u8>, LZ77Error> {
|
||||
while src_pos < data.len() {
|
||||
let mut flag = 0;
|
||||
let flag_pos = buf.position();
|
||||
buf.write_u8(b'\x00')?; // Reserve a byte for the chunk head.
|
||||
buf.write_u8(b'\x00')?; // Reserve a byte for the flag.
|
||||
let mut i = 0;
|
||||
while i < 8 && src_pos < data.len() {
|
||||
let current_node = nodes[src_pos].clone();
|
||||
|
@ -3,5 +3,6 @@
|
||||
//
|
||||
// Root for all archive-related modules.
|
||||
|
||||
pub mod ash;
|
||||
pub mod lz77;
|
||||
pub mod u8;
|
||||
|
53
src/bin/rustii/archive/ash.rs
Normal file
53
src/bin/rustii/archive/ash.rs
Normal file
@ -0,0 +1,53 @@
|
||||
// archive/ash.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Code for the ASH decompression command in the rustii CLI.
|
||||
// Might even have the compression command someday if I ever write the compression code!
|
||||
|
||||
use std::{str, fs};
|
||||
use std::path::{Path, PathBuf};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::Subcommand;
|
||||
use rustii::archive::ash;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
pub enum Commands {
|
||||
/// Compress a file with ASH compression (NOT IMPLEMENTED)
|
||||
Compress {
|
||||
/// The path to the file to compress
|
||||
input: String,
|
||||
/// An optional output name; defaults to <input name>.ash
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
},
|
||||
/// Decompress an ASH-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_ash(_input: &str, _output: &Option<String>) -> Result<()> {
|
||||
todo!();
|
||||
}
|
||||
|
||||
pub fn decompress_ash(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 = ash::decompress_ash(&compressed, None, None).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(format!("{}.out", in_path.extension().unwrap_or("".as_ref()).to_str().unwrap()))
|
||||
};
|
||||
fs::write(out_path.clone(), decompressed)?;
|
||||
println!("Successfully decompressed ASH file to \"{}\"!", out_path.display());
|
||||
Ok(())
|
||||
}
|
@ -12,7 +12,7 @@ use rustii::archive::lz77;
|
||||
#[derive(Subcommand)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
pub enum Commands {
|
||||
/// Compress a file with LZ77 compression (NOT IMPLEMENTED)
|
||||
/// Compress a file with LZ77 compression
|
||||
Compress {
|
||||
/// The path to the file to compress
|
||||
input: String,
|
||||
@ -40,9 +40,10 @@ pub fn compress_lz77(input: &str, output: &Option<String>) -> Result<()> {
|
||||
let out_path = if output.is_some() {
|
||||
PathBuf::from(output.clone().unwrap())
|
||||
} else {
|
||||
PathBuf::from(in_path).with_extension("lz77")
|
||||
PathBuf::from(in_path).with_extension(format!("{}.lz77", in_path.extension().unwrap_or("".as_ref()).to_str().unwrap()))
|
||||
};
|
||||
fs::write(out_path, compressed)?;
|
||||
fs::write(out_path.clone(), compressed)?;
|
||||
println!("Successfully compressed file to \"{}\"!", out_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -56,8 +57,9 @@ pub fn decompress_lz77(input: &str, output: &Option<String>) -> Result<()> {
|
||||
let out_path = if output.is_some() {
|
||||
PathBuf::from(output.clone().unwrap())
|
||||
} else {
|
||||
PathBuf::from(in_path).with_extension("out")
|
||||
PathBuf::from(in_path).with_extension(format!("{}.out", in_path.extension().unwrap_or("".as_ref()).to_str().unwrap()))
|
||||
};
|
||||
fs::write(out_path, decompressed)?;
|
||||
fs::write(out_path.clone(), decompressed)?;
|
||||
println!("Successfully decompressed LZ77 file to \"{}\"!", out_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
// archive/mod.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
|
||||
pub mod ash;
|
||||
pub mod lz77;
|
||||
|
@ -21,6 +21,11 @@ struct Cli {
|
||||
#[derive(Subcommand)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
enum Commands {
|
||||
/// Decompress data using ASH compression
|
||||
Ash {
|
||||
#[command(subcommand)]
|
||||
command: archive::ash::Commands,
|
||||
},
|
||||
/// Fakesign a TMD, Ticket, or WAD (trucha bug)
|
||||
Fakesign {
|
||||
/// The path to a TMD, Ticket, or WAD
|
||||
@ -50,6 +55,16 @@ fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Some(Commands::Ash { command }) => {
|
||||
match command {
|
||||
archive::ash::Commands::Compress { input, output } => {
|
||||
archive::ash::compress_ash(input, output)?
|
||||
},
|
||||
archive::ash::Commands::Decompress { input, output } => {
|
||||
archive::ash::decompress_ash(input, output)?
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Fakesign { input, output }) => {
|
||||
title::fakesign::fakesign(input, output)?
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user