2025-09-27 00:31:18 +02:00
|
|
|
mod formats;
|
2025-10-08 15:14:53 +02:00
|
|
|
mod keys;
|
2025-10-30 22:04:59 +01:00
|
|
|
mod utils;
|
2025-09-27 00:31:18 +02:00
|
|
|
|
2025-09-11 20:26:51 +02:00
|
|
|
use clap::Parser;
|
|
|
|
|
use std::path::{PathBuf};
|
2026-02-17 18:57:14 +01:00
|
|
|
use std::io::{self, Seek, SeekFrom};
|
2025-10-07 21:43:49 +02:00
|
|
|
use std::fs::{self, File};
|
2026-02-05 15:18:26 +01:00
|
|
|
use crate::formats::{Format, get_registry};
|
2025-09-11 20:26:51 +02:00
|
|
|
|
|
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
|
struct Args {
|
|
|
|
|
input_target: String,
|
2026-02-05 19:38:50 +01:00
|
|
|
output_directory: Option<String>,
|
2026-02-17 21:28:43 +01:00
|
|
|
|
|
|
|
|
///format specific options
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
options: Vec<String>,
|
2025-09-11 20:26:51 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 15:08:38 +01:00
|
|
|
pub enum InputTarget {
|
|
|
|
|
File(File),
|
|
|
|
|
Directory(PathBuf),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct AppContext {
|
|
|
|
|
pub input: InputTarget,
|
|
|
|
|
pub output_dir: PathBuf,
|
2026-02-17 21:28:43 +01:00
|
|
|
pub options: Vec<String>,
|
2026-02-05 15:18:26 +01:00
|
|
|
}
|
2026-02-06 16:27:13 +01:00
|
|
|
impl AppContext {
|
|
|
|
|
pub fn file(&self) -> Option<&File> {
|
|
|
|
|
match &self.input {
|
|
|
|
|
InputTarget::File(f) => Some(f),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn dir(&self) -> Option<&PathBuf> {
|
|
|
|
|
match &self.input {
|
|
|
|
|
InputTarget::Directory(p) => Some(p),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 15:18:26 +01:00
|
|
|
|
2025-09-11 20:26:51 +02:00
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
2025-10-07 16:00:32 +02:00
|
|
|
println!("unixtract Firmware extractor");
|
2025-09-11 20:26:51 +02:00
|
|
|
let args = Args::parse();
|
|
|
|
|
|
2026-02-05 19:38:50 +01:00
|
|
|
let target_path_str = args.input_target;
|
|
|
|
|
println!("Input target: {}", target_path_str);
|
|
|
|
|
let target_path = PathBuf::from(&target_path_str);
|
|
|
|
|
|
|
|
|
|
let output_path_str = if args.output_directory.is_some() {
|
|
|
|
|
args.output_directory.unwrap()
|
2025-12-10 20:27:32 +01:00
|
|
|
} else {
|
2026-02-05 19:38:50 +01:00
|
|
|
format!("_{}", target_path.file_name().and_then(|s| s.to_str()).unwrap())
|
2025-12-10 20:27:32 +01:00
|
|
|
};
|
2026-02-06 15:08:38 +01:00
|
|
|
println!("Output directory: {}", output_path_str);
|
2026-02-05 19:38:50 +01:00
|
|
|
let output_directory_path = PathBuf::from(&output_path_str);
|
2025-09-11 20:26:51 +02:00
|
|
|
|
2026-02-05 19:38:50 +01:00
|
|
|
if output_directory_path.exists() {
|
|
|
|
|
if output_directory_path.is_dir() {
|
|
|
|
|
let is_empty = fs::read_dir(&output_directory_path)?.next().is_none();
|
2025-10-07 21:43:49 +02:00
|
|
|
if !is_empty {
|
2026-02-06 15:08:38 +01:00
|
|
|
println!("\nWarning: Output folder already exists and is NOT empty! Files may be overwritten!");
|
2025-10-07 21:43:49 +02:00
|
|
|
println!("Press Enter if you want to continue...");
|
|
|
|
|
io::stdin().read_line(&mut String::new())?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 15:18:26 +01:00
|
|
|
|
2026-02-06 15:08:38 +01:00
|
|
|
let app_ctx;
|
|
|
|
|
|
|
|
|
|
if target_path.is_file() {
|
|
|
|
|
let file = File::open(&target_path)?;
|
|
|
|
|
app_ctx = AppContext {
|
|
|
|
|
input: InputTarget::File(file),
|
|
|
|
|
output_dir: output_directory_path,
|
2026-02-17 21:28:43 +01:00
|
|
|
options: args.options,
|
2026-02-06 15:08:38 +01:00
|
|
|
};
|
|
|
|
|
} else if target_path.is_dir() {
|
|
|
|
|
app_ctx = AppContext {
|
|
|
|
|
input: InputTarget::Directory(target_path),
|
|
|
|
|
output_dir: output_directory_path,
|
2026-02-17 21:28:43 +01:00
|
|
|
options: args.options,
|
2026-02-06 15:08:38 +01:00
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
return Err("Invalid input path!".into());
|
|
|
|
|
}
|
2026-02-05 15:18:26 +01:00
|
|
|
|
2026-02-05 19:38:50 +01:00
|
|
|
let formats: Vec<Format> = get_registry();
|
2026-02-06 16:27:13 +01:00
|
|
|
println!("Loaded {} formats!", formats.len());
|
2026-02-06 15:08:38 +01:00
|
|
|
|
2026-02-05 15:18:26 +01:00
|
|
|
for format in formats {
|
2026-02-05 18:53:35 +01:00
|
|
|
if let Some(ctx) = (format.detector_func)(&app_ctx)? {
|
2026-02-06 16:27:13 +01:00
|
|
|
println!("\n{} detected!", format.name);
|
2026-02-17 18:57:14 +01:00
|
|
|
|
|
|
|
|
//reset seek of the file if present
|
|
|
|
|
if let Some(mut file) = app_ctx.file() {
|
|
|
|
|
file.seek(SeekFrom::Start(0))?;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 16:27:13 +01:00
|
|
|
(format.extractor_func)(&app_ctx, ctx)?;
|
2026-02-17 18:34:46 +01:00
|
|
|
|
|
|
|
|
//extractor returned with no error
|
|
|
|
|
println!("\nExtraction finished! Saved extracted files to {}", output_path_str);
|
2026-02-05 15:18:26 +01:00
|
|
|
return Ok(());
|
2025-09-11 20:26:51 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 15:18:26 +01:00
|
|
|
println!("\nInput format not recognized!");
|
2025-09-11 20:26:51 +02:00
|
|
|
Ok(())
|
|
|
|
|
}
|