Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement unpack iro subcommand #1

Merged
merged 11 commits into from
May 18, 2024
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