diff --git a/src/formats.rs b/src/formats.rs index 8c62c8c..88e82cf 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -25,6 +25,7 @@ pub struct Format { } pub mod mstar; +pub mod android_ota_zip; pub mod mstar_secure_old; pub mod samsung_old; pub mod nvt_timg; @@ -78,6 +79,11 @@ pub mod mtk_bdp; /// before their inner formats to avoid misdetection. pub fn get_registry() -> Vec { vec![ + Format { + name: "android_ota_zip", + detector_func: crate::formats::android_ota_zip::is_android_ota_zip_file, + extractor_func: crate::formats::android_ota_zip::extract_android_ota_zip, + }, Format { name: "mstar", detector_func: crate::formats::mstar::is_mstar_file, diff --git a/src/formats/android_ota_zip/mod.rs b/src/formats/android_ota_zip/mod.rs new file mode 100644 index 0000000..18158fb --- /dev/null +++ b/src/formats/android_ota_zip/mod.rs @@ -0,0 +1,734 @@ +use std::any::Any; +use std::collections::HashSet; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; + +use flate2::read::DeflateDecoder; +use log::{debug, info, warn}; + +use crate::AppContext; +use crate::utils::common; +use crate::utils::path_sanitize::safe_output_path; + +const LOCAL_FILE_HEADER_SIZE: u64 = 30; +const CENTRAL_DIRECTORY_HEADER_SIZE: u64 = 46; +const END_OF_CENTRAL_DIRECTORY_SIZE: u64 = 22; +const ZIP_EOCD_SEARCH_LIMIT: u64 = 65_557; + +const LOCAL_FILE_HEADER_MAGIC: u32 = 0x0403_4b50; +const CENTRAL_DIRECTORY_HEADER_MAGIC: u32 = 0x0201_4b50; +const END_OF_CENTRAL_DIRECTORY_MAGIC: u32 = 0x0605_4b50; + +const COMPRESSION_STORED: u16 = 0; +const COMPRESSION_DEFLATED: u16 = 8; + +pub enum AndroidOtaZipContext { + AndroidOta { updater_script: String }, + GenericZip, +} + +#[derive(Clone)] +struct ZipEntry { + name: String, + compression_method: u16, + compressed_size: u64, + uncompressed_size: u64, + local_header_offset: u64, +} + +#[derive(Clone, Debug)] +enum ScriptAction { + WholeImage { + device: String, + image: String, + }, + BlockBackup { + device: String, + offset: u32, + blocks: u32, + }, + BlockRecovery { + device: String, + offset: u32, + blocks: u32, + }, + PackageExtract { + image: String, + destination: String, + }, +} + +pub fn is_android_ota_zip_file( + app_ctx: &AppContext, +) -> Result>, Box> { + let file_ref = match app_ctx.file() { + Some(file) => file, + None => return Ok(None), + }; + + if file_ref.metadata()?.len() < 2 { + return Ok(None); + } + + let header = common::read_file(file_ref, 0, 2)?; + if &header != b"PK" { + return Ok(None); + } + + if let Some(input_path) = app_ctx.input_path() { + if let Some(ext) = input_path.extension().and_then(|e| e.to_str()) { + if !ext.eq_ignore_ascii_case("zip") { + info!("Detected ZIP archive with non-ZIP extension (.{}), treating as ZIP", ext); + } + } + } + + let mut file = file_ref.try_clone()?; + let entries = match read_zip_entries(&mut file) { + Ok(entries) => entries, + Err(error) => { + debug!("File starts with PK magic but is not a valid ZIP: {}", error); + return Ok(None); + } + }; + + let Some(script_entry) = entries + .iter() + .find(|entry| entry.name == "META-INF/com/tcl/updater-script") + else { + return Ok(Some(Box::new(AndroidOtaZipContext::GenericZip))); + }; + + let updater_script = match extract_entry_data(&mut file, script_entry) { + Ok(data) => String::from_utf8_lossy(&data).to_string(), + Err(error) => { + debug!("android_ota_zip: updater script could not be read: {}", error); + return Ok(Some(Box::new(AndroidOtaZipContext::GenericZip))); + } + }; + + Ok(Some(Box::new(AndroidOtaZipContext::AndroidOta { + updater_script, + }))) +} + +pub fn extract_android_ota_zip( + app_ctx: &AppContext, + ctx: Box, +) -> Result<(), Box> { + let file_ref = app_ctx.file().ok_or("Extractor expected file")?; + let ctx = ctx + .downcast::() + .map_err(|_| "Invalid context type")?; + let renamed_zip_path = maybe_rename_input_to_zip(app_ctx)?; + let mut file = file_ref.try_clone()?; + + fs::create_dir_all(&app_ctx.output_dir)?; + + let entries = read_zip_entries(&mut file)?; + + match *ctx { + AndroidOtaZipContext::AndroidOta { ref updater_script } => { + let actions = parse_updater_script(updater_script); + + info!("TCL Android OTA ZIP detected"); + info!("ZIP entries: {}", entries.len()); + info!("Partition actions: {}", actions.len()); + + let mut ota_image_names: HashSet = HashSet::new(); + for action in &actions { + match action { + ScriptAction::WholeImage { image, .. } + | ScriptAction::PackageExtract { image, .. } => { + ota_image_names.insert(image.clone()); + } + ScriptAction::BlockBackup { .. } | ScriptAction::BlockRecovery { .. } => {} + } + } + + extract_all_entries_with_progress( + &mut file, + &entries, + &app_ctx.output_dir, + &ota_image_names, + )?; + + write_dumper_script(&app_ctx.output_dir, &actions)?; + write_partition_map(&app_ctx.output_dir, &actions)?; + + if !actions.is_empty() { + info!("- Generated dump_partitions.sh and partitions.tsv"); + } + } + AndroidOtaZipContext::GenericZip => { + info!("ZIP archive detected"); + info!("ZIP entries: {}", entries.len()); + extract_all_entries_with_progress( + &mut file, + &entries, + &app_ctx.output_dir, + &HashSet::new(), + )?; + } + } + + if let Some(zip_path) = renamed_zip_path { + if let Err(e) = fs::remove_file(&zip_path) { + warn!("Could not remove {}: {}", zip_path.display(), e); + } + } + + Ok(()) +} + +fn maybe_rename_input_to_zip( + app_ctx: &AppContext, +) -> Result, Box> { + let Some(input_path) = app_ctx.input_path() else { + return Ok(None); + }; + + if let Some(extension) = input_path + .extension() + .and_then(|extension| extension.to_str()) + { + if extension.eq_ignore_ascii_case("zip") { + return Ok(None); + } + } + + let zip_path = input_path.with_extension("zip"); + if zip_path.exists() { + warn!("Input rename skipped because {} already exists", zip_path.display()); + return Ok(None); + } + + match fs::rename(input_path, &zip_path) { + Ok(()) => { + info!("Renamed input file from {} to {}", input_path.display(), zip_path.display()); + Ok(Some(zip_path)) + } + Err(e) => { + warn!("Could not rename {} to {}: {}. Extraction will proceed without renaming.", + input_path.display(), zip_path.display(), e); + Ok(None) + } + } +} + +fn extract_all_entries_with_progress( + file: &mut File, + entries: &[ZipEntry], + output_dir: &Path, + ota_image_names: &HashSet, +) -> Result<(), Box> { + let file_entries: Vec<&ZipEntry> = entries + .iter() + .filter(|entry| !entry.name.ends_with('/') && !entry.name.ends_with('\\')) + .collect(); + let total = file_entries.len(); + + for (i, entry) in file_entries.iter().enumerate() { + let output_path = safe_output_path(output_dir, &entry.name)?; + + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent)?; + } + + let compression_label = match entry.compression_method { + COMPRESSION_DEFLATED => " [DEFLATED]", + COMPRESSION_STORED => "", + _ => " [COMPRESSED]", + }; + + let ota_label = if ota_image_names.contains(&entry.name) { " [OTA]" } else { "" }; + + info!("\n({}/{}) - {}, Size: {}{}{}", + i + 1, total, entry.name, entry.uncompressed_size, compression_label, ota_label); + + if entry.compression_method == COMPRESSION_DEFLATED { + info!("- Decompressing..."); + } + + let mut out_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&output_path)?; + extract_entry_to_writer(file, entry, &mut out_file)?; + + info!("-- Saved file!"); + } + + for entry in entries { + if entry.name.ends_with('/') || entry.name.ends_with('\\') { + let output_path = safe_output_path(output_dir, &entry.name)?; + fs::create_dir_all(&output_path)?; + } + } + + Ok(()) +} + +fn read_zip_entries(file: &mut File) -> Result, Box> { + let file_len = file.metadata()?.len(); + if file_len < END_OF_CENTRAL_DIRECTORY_SIZE { + return Err("ZIP file is too small".into()); + } + + let search_start = file_len.saturating_sub(ZIP_EOCD_SEARCH_LIMIT); + let search_end = file_len - END_OF_CENTRAL_DIRECTORY_SIZE; + + for pos in (search_start..=search_end).rev() { + file.seek(SeekFrom::Start(pos))?; + let mut eocd = [0u8; END_OF_CENTRAL_DIRECTORY_SIZE as usize]; + file.read_exact(&mut eocd)?; + + if read_u32(&eocd, 0) != END_OF_CENTRAL_DIRECTORY_MAGIC { + continue; + } + + let total_entries = read_u16(&eocd, 10) as u64; + let central_dir_size = read_u32(&eocd, 12) as u64; + let central_dir_offset = read_u32(&eocd, 16) as u64; + let central_dir_end = central_dir_offset + .checked_add(central_dir_size) + .ok_or("ZIP central directory offset overflow")?; + + if central_dir_end > pos || central_dir_end > file_len { + continue; + } + + file.seek(SeekFrom::Start(central_dir_offset))?; + let mut entries = Vec::with_capacity(total_entries as usize); + + for _ in 0..total_entries { + let mut header = [0u8; CENTRAL_DIRECTORY_HEADER_SIZE as usize]; + file.read_exact(&mut header)?; + + if read_u32(&header, 0) != CENTRAL_DIRECTORY_HEADER_MAGIC { + return Err("Invalid ZIP central directory entry".into()); + } + + let name_len = read_u16(&header, 28) as u64; + let extra_len = read_u16(&header, 30) as u64; + let comment_len = read_u16(&header, 32) as u64; + let mut name = vec![0u8; name_len as usize]; + file.read_exact(&mut name)?; + + let mut extra = vec![0u8; extra_len as usize]; + file.read_exact(&mut extra)?; + + let mut comment = vec![0u8; comment_len as usize]; + file.read_exact(&mut comment)?; + + entries.push(ZipEntry { + name: String::from_utf8_lossy(&name).to_string(), + compression_method: read_u16(&header, 10), + compressed_size: read_u32(&header, 20) as u64, + uncompressed_size: read_u32(&header, 24) as u64, + local_header_offset: read_u32(&header, 42) as u64, + }); + } + + return Ok(entries); + } + + Err("ZIP end of central directory not found".into()) +} + +fn extract_entry_data( + file: &mut File, + entry: &ZipEntry, +) -> Result, Box> { + let mut data = Vec::with_capacity(try_usize(entry.uncompressed_size, &entry.name)?); + let mut cursor = io::Cursor::new(&mut data); + extract_entry_to_writer(file, entry, &mut cursor)?; + Ok(data) +} + +fn extract_entry_to_writer( + file: &mut File, + entry: &ZipEntry, + writer: &mut W, +) -> Result<(), Box> { + let local_header = read_local_header(file, entry)?; + + file.seek(SeekFrom::Start(local_header.data_offset))?; + + match entry.compression_method { + COMPRESSION_STORED => copy_stored_entry(file, entry, writer), + COMPRESSION_DEFLATED => { + let mut limited = file.take(entry.compressed_size); + let mut decoder = DeflateDecoder::new(&mut limited); + copy_reader_to_writer(&mut decoder, writer) + } + method => Err(format!( + "Unsupported ZIP compression method {} for {}", + method, entry.name + ) + .into()), + } +} + +fn read_local_header( + file: &mut File, + entry: &ZipEntry, +) -> Result> { + file.seek(SeekFrom::Start(entry.local_header_offset))?; + + let mut header = [0u8; LOCAL_FILE_HEADER_SIZE as usize]; + file.read_exact(&mut header)?; + + if read_u32(&header, 0) != LOCAL_FILE_HEADER_MAGIC { + return Err(format!("Invalid local ZIP header for {}", entry.name).into()); + } + + let name_len = read_u16(&header, 26) as u64; + let extra_len = read_u16(&header, 28) as u64; + let data_offset = entry + .local_header_offset + .checked_add(LOCAL_FILE_HEADER_SIZE) + .and_then(|offset| offset.checked_add(name_len)) + .and_then(|offset| offset.checked_add(extra_len)) + .ok_or("ZIP local header offset overflow")?; + + Ok(LocalHeader { data_offset }) +} + +fn copy_stored_entry( + file: &mut File, + entry: &ZipEntry, + writer: &mut W, +) -> Result<(), Box> { + let mut remaining = entry.compressed_size; + let mut buffer = [0u8; 64 * 1024]; + + while remaining > 0 { + let chunk = remaining.min(buffer.len() as u64) as usize; + let read = file.read(&mut buffer[..chunk])?; + if read == 0 { + return Err(format!("Unexpected end of ZIP data for {}", entry.name).into()); + } + writer.write_all(&buffer[..read])?; + remaining -= read as u64; + } + + Ok(()) +} + +fn copy_reader_to_writer( + reader: &mut R, + writer: &mut W, +) -> Result<(), Box> { + let mut buffer = [0u8; 64 * 1024]; + + loop { + let read = reader.read(&mut buffer)?; + if read == 0 { + break; + } + writer.write_all(&buffer[..read])?; + } + + Ok(()) +} + +fn parse_updater_script(script: &str) -> Vec { + let mut actions = Vec::new(); + + for line in script + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with('#')) + { + if let Some(args) = parse_function_args(line, "whole_image_update") { + if args.len() == 2 { + if let (Some(device), Some(image)) = + (parse_string_arg(&args[0]), parse_string_arg(&args[1])) + { + actions.push(ScriptAction::WholeImage { device, image }); + continue; + } + } + } + + if let Some(args) = parse_function_args(line, "block_Backup") { + if args.len() == 3 { + if let (Some(device), Some(offset), Some(blocks)) = ( + parse_string_arg(&args[0]), + parse_u32_arg(&args[1]), + parse_u32_arg(&args[2]), + ) { + actions.push(ScriptAction::BlockBackup { + device, + offset, + blocks, + }); + continue; + } + } + } + + if let Some(args) = parse_function_args(line, "block_Recovery") { + if args.len() == 3 { + if let (Some(device), Some(offset), Some(blocks)) = ( + parse_string_arg(&args[0]), + parse_u32_arg(&args[1]), + parse_u32_arg(&args[2]), + ) { + actions.push(ScriptAction::BlockRecovery { + device, + offset, + blocks, + }); + continue; + } + } + } + + if let Some(args) = parse_function_args(line, "package_extract_file") { + if args.len() == 2 { + if let (Some(image), Some(destination)) = + (parse_string_arg(&args[0]), parse_string_arg(&args[1])) + { + actions.push(ScriptAction::PackageExtract { image, destination }); + continue; + } + } + } + } + + actions +} + +fn parse_function_args(line: &str, name: &str) -> Option> { + let prefix = format!("{}(", name); + let rest = line.strip_prefix(&prefix)?; + let rest = rest.strip_suffix(");")?.trim(); + + if rest.is_empty() { + return Some(Vec::new()); + } + + Some(split_args(rest)) +} + +fn split_args(input: &str) -> Vec { + let mut args = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + let mut escaped = false; + + for ch in input.chars() { + if escaped { + current.push(ch); + escaped = false; + continue; + } + + if ch == '\\' && in_quotes { + escaped = true; + continue; + } + + if ch == '"' { + in_quotes = !in_quotes; + continue; + } + + if ch == ',' && !in_quotes { + args.push(current.trim().to_string()); + current.clear(); + continue; + } + + current.push(ch); + } + + args.push(current.trim().to_string()); + args +} + +fn parse_string_arg(arg: &str) -> Option { + let trimmed = arg.trim(); + if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') { + Some(trimmed[1..trimmed.len() - 1].to_string()) + } else { + None + } +} + +fn parse_u32_arg(arg: &str) -> Option { + arg.trim().parse().ok() +} + +fn write_dumper_script( + output_dir: &Path, + actions: &[ScriptAction], +) -> Result<(), Box> { + let output_path = safe_output_path(output_dir, "dump_partitions.sh")?; + let mut lines = Vec::new(); + + lines.push("#!/system/bin/sh".to_string()); + lines.push("set -eu".to_string()); + lines.push("OUT=\"/sdcard/unixtract_dump\"".to_string()); + lines.push("mkdir -p \"$OUT\"".to_string()); + lines.push(String::new()); + + let mut whole_image_count = 0; + for action in actions { + match action { + ScriptAction::WholeImage { device, image } => { + whole_image_count += 1; + lines.push(format!( + "echo \"Dumping {} to {}\"", + shell_single_quote(device), + shell_single_quote(image) + )); + lines.push(format!( + "dd if={} of=\"$OUT/{}\" bs=1M conv=fsync", + shell_single_quote(device), + shell_single_quote(basename(image)) + )); + lines.push(String::new()); + } + ScriptAction::BlockBackup { + device, + offset, + blocks, + } => { + lines.push(format!( + "# block_Backup {} offset={} blocks={}", + device, offset, blocks + )); + } + ScriptAction::BlockRecovery { + device, + offset, + blocks, + } => { + lines.push(format!( + "# block_Recovery {} offset={} blocks={}", + device, offset, blocks + )); + } + ScriptAction::PackageExtract { destination, .. } => { + lines.push(format!( + "# Optional package_extract_file payload: {}", + destination + )); + lines.push(format!( + "if [ -f {} ]; then dd if={} of=\"$OUT/{}\" bs=1M conv=fsync; fi", + shell_single_quote(destination), + shell_single_quote(destination), + shell_single_quote(basename(destination)) + )); + lines.push(String::new()); + } + } + } + + if whole_image_count == 0 { + lines + .push("echo \"No whole_image_update partitions found in updater-script.\"".to_string()); + } + + lines.push("sync".to_string()); + lines.push("echo \"Done: $OUT\"".to_string()); + + write_text_file(&output_path, &lines.join("\n")) +} + +fn write_partition_map( + output_dir: &Path, + actions: &[ScriptAction], +) -> Result<(), Box> { + let output_path = safe_output_path(output_dir, "partitions.tsv")?; + let mut lines = Vec::new(); + + lines.push("operation\tdevice\timage\toffset\tblocks".to_string()); + + for action in actions { + match action { + ScriptAction::WholeImage { device, image } => { + lines.push(format!("whole_image\t{}\t{}\t\t", device, image)); + } + ScriptAction::BlockBackup { + device, + offset, + blocks, + } => { + lines.push(format!( + "block_backup\t{}\t\t{}\t{}", + device, offset, blocks + )); + } + ScriptAction::BlockRecovery { + device, + offset, + blocks, + } => { + lines.push(format!( + "block_recovery\t{}\t\t{}\t{}", + device, offset, blocks + )); + } + ScriptAction::PackageExtract { image, destination } => { + lines.push(format!("package_extract\t\t{}\t\t{}", image, destination)); + } + } + } + + write_text_file(&output_path, &lines.join("\n")) +} + +fn write_text_file(path: &Path, content: &str) -> Result<(), Box> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path)?; + file.write_all(content.as_bytes())?; + file.write_all(b"\n")?; + Ok(()) +} + +fn shell_single_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\\''")) +} + +fn basename(path: &str) -> &str { + Path::new(path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("dump.img") +} + +fn try_usize(value: u64, name: &str) -> Result> { + value + .try_into() + .map_err(|_| format!("{} is too large", name).into()) +} + +fn read_u16(buffer: &[u8], offset: usize) -> u16 { + u16::from_le_bytes([buffer[offset], buffer[offset + 1]]) +} + +fn read_u32(buffer: &[u8], offset: usize) -> u32 { + u32::from_le_bytes([ + buffer[offset], + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + ]) +} + +struct LocalHeader { + data_offset: u64, +} diff --git a/src/formats/funai_mstar/mod.rs b/src/formats/funai_mstar/mod.rs index f75c49e..7958a39 100644 --- a/src/formats/funai_mstar/mod.rs +++ b/src/formats/funai_mstar/mod.rs @@ -70,6 +70,7 @@ pub fn extract_funai_mstar(app_ctx: &AppContext, ctx: Box) -> Result<() let r_out_file = File::open(&output_path)?; let in_ctx: AppContext = AppContext { input: InputTarget::File(r_out_file), + input_path: Some(output_path.clone()), output_dir: app_ctx.output_dir.join("SoC"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, diff --git a/src/formats/mstar_secure_old/mod.rs b/src/formats/mstar_secure_old/mod.rs index cba8207..1beb00c 100644 --- a/src/formats/mstar_secure_old/mod.rs +++ b/src/formats/mstar_secure_old/mod.rs @@ -54,6 +54,7 @@ pub fn extract_mstar_secure_old(app_ctx: &AppContext, ctx: Box) -> Resu let r_out_file = File::open(&output_path)?; let in_ctx: AppContext = AppContext { input: InputTarget::File(r_out_file), + input_path: Some(output_path.clone()), output_dir: app_ctx.output_dir.clone(), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, diff --git a/src/formats/pfl_upg/mod.rs b/src/formats/pfl_upg/mod.rs index de6bfbd..f8a90c9 100644 --- a/src/formats/pfl_upg/mod.rs +++ b/src/formats/pfl_upg/mod.rs @@ -125,6 +125,7 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box) -> Result<(), B let r_temp_file = File::open(&temp_path)?; let in_ctx: AppContext = AppContext { input: InputTarget::File(r_temp_file), + input_path: None, output_dir: output_path, options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, diff --git a/src/formats/philips_bdp/mod.rs b/src/formats/philips_bdp/mod.rs index 8a973c2..99b064a 100644 --- a/src/formats/philips_bdp/mod.rs +++ b/src/formats/philips_bdp/mod.rs @@ -75,7 +75,7 @@ pub fn extract_philips_bdp(app_ctx: &AppContext, ctx: Box) -> Result<() info!("Checking if it's also MTK BDP..."); let new_file = File::open(&output_path)?; - let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, quiet: app_ctx.quiet, verbose: app_ctx.verbose }; + let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), input_path: Some(output_path.clone()), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, quiet: app_ctx.quiet, verbose: app_ctx.verbose }; if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? { info!("- MTK BDP file detected!\n"); diff --git a/src/formats/sony_bdp/mod.rs b/src/formats/sony_bdp/mod.rs index 2a083c0..c236ca4 100644 --- a/src/formats/sony_bdp/mod.rs +++ b/src/formats/sony_bdp/mod.rs @@ -119,11 +119,13 @@ pub fn extract_sony_bdp(app_ctx: &AppContext, ctx: Box) -> Result<(), B //The last file is the host MTK BDP file so we can extract that here (wont work for pre-linux which have old mtk bdp though.) if last_file_path.is_some() { - let last_file = File::open(last_file_path.unwrap())?; + let last_file_path = last_file_path.unwrap(); + let last_file = File::open(&last_file_path)?; let mtk_extraction_path = app_ctx.output_dir.join(format!("{}", i)); let ctx: AppContext = AppContext { input: InputTarget::File(last_file), + input_path: Some(last_file_path), output_dir: mtk_extraction_path, options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, diff --git a/src/main.rs b/src/main.rs index 1e7c0c6..096e150 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod keys; mod utils; use clap::Parser; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::io::{self, Seek, SeekFrom, Write}; use std::fs::{self, File}; use crate::formats::{Format, get_registry}; @@ -55,6 +55,7 @@ pub enum InputTarget { pub struct AppContext { pub input: InputTarget, + pub input_path: Option, pub output_dir: PathBuf, pub options: Vec, pub dry_run: bool, @@ -70,6 +71,10 @@ impl AppContext { } } + pub fn input_path(&self) -> Option<&Path> { + self.input_path.as_deref() + } + pub fn dir(&self) -> Option<&PathBuf> { match &self.input { InputTarget::Directory(p) => Some(p), @@ -169,6 +174,7 @@ fn main() -> Result<(), Box> { let file = File::open(&target_path)?; app_ctx = AppContext { input: InputTarget::File(file), + input_path: Some(target_path.clone()), output_dir: output_directory_path, options: args.options, dry_run: args.dry_run, @@ -177,7 +183,8 @@ fn main() -> Result<(), Box> { }; } else if target_path.is_dir() { app_ctx = AppContext { - input: InputTarget::Directory(target_path), + input: InputTarget::Directory(target_path.clone()), + input_path: Some(target_path.clone()), output_dir: output_directory_path, options: args.options, dry_run: args.dry_run,