Skip to content

Commit

Permalink
feat: implement unpack iro subcommand (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
tangtang95 authored May 18, 2024
1 parent 51a5b05 commit f3ad7da
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 35 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ edition = "2021"
clap = { version = "4.5.4", features = ["derive"] }
thiserror = "1.0.59"
walkdir = "2.5.0"
nom = "7.1.3"

[dev-dependencies]
assert_cmd = "2.0.12"
Expand Down
37 changes: 37 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::path::PathBuf;

use thiserror::Error;


#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
Io(#[from] ::std::io::Error),
#[error(transparent)]
StripPrefix(#[from] ::std::path::StripPrefixError),
#[error("{0} is not a directory")]
NotDir(PathBuf),
#[error("output path already exists: {0}")]
OutputPathExists(PathBuf),
#[error("{0} has invalid unicode")]
InvalidUnicode(PathBuf),
#[error("could not find default name from {0}")]
CannotDetectDefaultName(PathBuf),
#[error("parsing error due to invalid iro flags {0}")]
InvalidIroFlags(i32),
// #[error("failed to parse binary data")]
#[error(transparent)]
CannotParseBinary(nom::Err<::nom::error::Error<Vec<u8>>>),
#[error("parsing error due to invalid file flags {0}")]
InvalidFileFlags(i32),
#[error("invalid utf16 {0}")]
InvalidUtf16(String),
#[error("parten file path does not exists: {0}")]
ParentPathDoesNotExist(PathBuf),
}

impl From<nom::Err<nom::error::Error<&[u8]>>> for Error {
fn from(err: nom::Err<nom::error::Error<&[u8]>>) -> Self {
Self::CannotParseBinary(err.map_input(|input| input.into()))
}
}
23 changes: 19 additions & 4 deletions src/iro_entry.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use crate::Error;

pub const INDEX_FIXED_BYTE_SIZE: usize = 20;

#[derive(Debug)]
pub struct IroEntry {
path: Vec<u8>,
flags: FileFlags,
offset: u64,
data_len: u32,
pub path: Vec<u8>,
pub flags: FileFlags,
pub offset: u64,
pub data_len: u32,
}

#[derive(Debug)]
pub enum FileFlags {
Uncompressed = 0,
}
Expand Down Expand Up @@ -34,3 +38,14 @@ impl From<IroEntry> for Vec<u8> {
bytes
}
}

impl TryFrom<i32> for FileFlags {
type Error = Error;

fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(FileFlags::Uncompressed),
_ => Err(Error::InvalidFileFlags(value))
}
}
}
50 changes: 42 additions & 8 deletions src/iro_header.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
const IRO_SIG: i32 = 0x534f5249; // represents IROS text
use std::fmt::Display;

#[derive(Clone)]
use crate::Error;

pub const IRO_SIG: i32 = 0x534f5249; // represents IROS text

#[derive(Clone, Debug)]
pub struct IroHeader {
version: IroVersion,
flags: IroFlags,
size: i32,
num_files: u32,
pub version: IroVersion,
pub flags: IroFlags,
pub size: i32,
pub num_files: u32,
}

#[derive(Clone)]
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub enum IroFlags {
None = 0,
Patch = 1,
}

#[derive(Clone)]
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub enum IroVersion {
Zero = 0x10000,
Expand Down Expand Up @@ -45,3 +49,33 @@ impl From<IroHeader> for Vec<u8> {
.concat()
}
}

impl TryFrom<i32> for IroFlags {
type Error = Error;

fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(IroFlags::None),
1 => Ok(IroFlags::Patch),
_ => Err(Error::InvalidIroFlags(value)),
}
}
}

impl Display for IroFlags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IroFlags::None => f.write_str("Full IRO"),
IroFlags::Patch => f.write_str("Patch IRO"),
}
}
}

impl Display for IroVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IroVersion::Zero => f.write_str("0x10000"),
IroVersion::Two => f.write_str("0x10002"),
}
}
}
42 changes: 42 additions & 0 deletions src/iro_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use nom::{
bytes::complete::{tag, take},
number::complete::{le_i32, le_u16, le_u32, le_u64},
};

use crate::{
iro_entry::{FileFlags, IroEntry},
iro_header::{IroFlags, IroHeader, IroVersion, IRO_SIG},
Error,
};

pub fn parse_iro_header_v2(bytes: &[u8]) -> Result<(&[u8], IroHeader), Error> {
let (bytes, _) = tag(&IRO_SIG.to_le_bytes())(bytes)?;
let (bytes, _) = tag((IroVersion::Two as i32).to_le_bytes())(bytes)?;
let (bytes, flags) = le_i32(bytes)?;
let (bytes, _) = tag(16i32.to_le_bytes())(bytes)?;
let (bytes, num_files) = le_u32(bytes)?;

Ok((
bytes,
IroHeader::new(IroVersion::Two, IroFlags::try_from(flags)?, 16, num_files),
))
}

/// Parse IroEntry without considering length of entire block
pub fn parse_iro_entry_v2(bytes: &[u8]) -> Result<(&[u8], IroEntry), Error> {
let (bytes, filepath_len) = le_u16(bytes)?;
let (bytes, filepath) = take(filepath_len)(bytes)?;
let (bytes, file_flags) = le_i32(bytes)?;
let (bytes, offset) = le_u64(bytes)?;
let (bytes, data_len) = le_u32(bytes)?;

Ok((
bytes,
IroEntry::new(
filepath.to_vec(),
FileFlags::try_from(file_flags)?,
offset,
data_len,
),
))
}
119 changes: 103 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
mod iro_entry;
mod iro_header;
mod iro_parser;
mod error;

use std::{
io::{BufRead, BufReader, Seek, Write},
io::{BufRead, BufReader, Read, Seek, Write},
path::{Path, PathBuf},
process,
result::Result,
};

use clap::{Args, Parser, Subcommand};
use error::Error;
use iro_entry::{FileFlags, IroEntry, INDEX_FIXED_BYTE_SIZE};
use iro_header::{IroFlags, IroHeader, IroVersion};
use thiserror::Error;
use iro_parser::{parse_iro_entry_v2, parse_iro_header_v2};
use walkdir::{DirEntry, WalkDir};

/// Command line tool to pack a single directory into a single archive in IRO format
Expand All @@ -26,6 +29,7 @@ struct Cli {
enum Commands {
/// Pack a single directory into an IRO archive
Pack(PackArgs),
Unpack(UnpackArgs),
}

#[derive(Args)]
Expand All @@ -39,20 +43,15 @@ struct PackArgs {
output: Option<PathBuf>,
}

#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
Io(#[from] ::std::io::Error),
#[error(transparent)]
StripPrefix(#[from] ::std::path::StripPrefixError),
#[error("{0} is not a directory")]
NotDir(PathBuf),
#[error("output file path already exists: {0}")]
OutputPathExists(PathBuf),
#[error("{0} has invalid unicode")]
InvalidUnicode(PathBuf),
#[error("could not find default name from {0}")]
CannotDetectDefaultName(PathBuf),
#[derive(Args)]
struct UnpackArgs {
/// IRO file to unpack
#[arg()]
iro_path: PathBuf,

/// Output directory path (default is the name of the IRO to unpack)
#[arg(short, long)]
output: Option<PathBuf>,
}

fn main() {
Expand All @@ -72,6 +71,17 @@ fn main() {
process::exit(1);
}
},
Commands::Unpack(args) => match unpack_archive(args.iro_path, args.output) {
Ok(output_dir) => {
println!("IRO unpacked into \"{}\" directory", output_dir.display());
process::exit(0);
}
Err(err) => {
let stderr = std::io::stderr();
writeln!(stderr.lock(), "[iroga error]: {}", err).ok();
process::exit(1);
}
},
}
}

Expand Down Expand Up @@ -154,6 +164,83 @@ fn pack_archive(dir_to_pack: PathBuf, output_path: Option<PathBuf>) -> Result<Pa
Ok(output_path)
}

fn unpack_archive(iro_path: PathBuf, output_path: Option<PathBuf>) -> Result<PathBuf, Error> {
// compute output filepath: either default generated name or given output_path
let output_path = match output_path {
Some(path) => path,
None => {
let filename = iro_path
.file_name()
.ok_or(Error::CannotDetectDefaultName(iro_path.clone()))?
.to_str()
.ok_or(Error::CannotDetectDefaultName(iro_path.clone()))?
.trim_end_matches(".iro");
Path::new(filename).to_owned()
}
};
if std::fs::read_dir(&output_path).is_ok() {
return Err(Error::OutputPathExists(output_path));
}

let mut iro_file = std::fs::File::open(&iro_path)?;
let mut iro_header_bytes = [0u8; 20];
iro_file.read_exact(&mut iro_header_bytes)?;
let (_, iro_header) = parse_iro_header_v2(&iro_header_bytes)?;

println!("IRO metadata");
println!("- version: {}", iro_header.version);
println!("- type: {}", iro_header.flags);
println!("- number of files: {}", iro_header.num_files);
println!();

let mut iro_entries: Vec<IroEntry> = Vec::new();
for _ in 0..iro_header.num_files {
let mut entry_len_bytes = [0u8; 2];
iro_file.read_exact(&mut entry_len_bytes)?;
let entry_len = u16::from_le_bytes(entry_len_bytes);
println!("{}", entry_len);

let mut entry_bytes = vec![0u8; entry_len as usize - 2];
iro_file.read_exact(entry_bytes.as_mut())?;
println!("{:?}", entry_bytes);

let (_, iro_entry) = parse_iro_entry_v2(&entry_bytes)?;

iro_entries.push(iro_entry);
}

for iro_entry in iro_entries {
let iro_entry_path = parse_utf16(&iro_entry.path)?.replace('\\', "/");
let entry_path = output_path.join(&iro_entry_path);
std::fs::create_dir_all(
entry_path
.parent()
.ok_or(Error::ParentPathDoesNotExist(entry_path.clone()))?,
)?;
let mut entry_file = std::fs::File::create(&entry_path).unwrap();

let mut buf_reader = BufReader::new(&iro_file);
buf_reader.seek(std::io::SeekFrom::Start(iro_entry.offset))?;
let mut entry_buffer = buf_reader.take(iro_entry.data_len as u64);
std::io::copy(&mut entry_buffer, &mut entry_file)?;

println!("\"{}\" file written!", iro_entry_path);
}

Ok(output_path)
}

fn parse_utf16(bytes: &[u8]) -> Result<String, Error> {
let bytes_u16 = bytes
.chunks(2)
.map(|e| e.try_into().map(u16::from_le_bytes))
.collect::<Result<Vec<_>, _>>()
.map_err(|_| Error::InvalidUtf16("uneven bytes".to_owned()))?;

String::from_utf16(&bytes_u16)
.map_err(|_| Error::InvalidUtf16("bytes in u16 cannot be converted to string".to_owned()))
}

fn unicode_filepath_bytes(path: &Path, strip_prefix_str: &Path) -> Result<Vec<u8>, Error> {
Ok(path
.strip_prefix(strip_prefix_str)?
Expand Down
Loading

0 comments on commit f3ad7da

Please sign in to comment.