mirror of
https://github.com/Ap0dexMe0/unixtract.git
synced 2026-07-15 21:00:03 +02:00
+ android ota zip
This commit is contained in:
@@ -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<Format> {
|
||||
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,
|
||||
|
||||
@@ -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<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
||||
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<dyn Any>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file_ref = app_ctx.file().ok_or("Extractor expected file")?;
|
||||
let ctx = ctx
|
||||
.downcast::<AndroidOtaZipContext>()
|
||||
.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<String> = 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<Option<PathBuf>, Box<dyn std::error::Error>> {
|
||||
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<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<Vec<ZipEntry>, Box<dyn std::error::Error>> {
|
||||
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<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
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<W: Write>(
|
||||
file: &mut File,
|
||||
entry: &ZipEntry,
|
||||
writer: &mut W,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<LocalHeader, Box<dyn std::error::Error>> {
|
||||
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<W: Write>(
|
||||
file: &mut File,
|
||||
entry: &ZipEntry,
|
||||
writer: &mut W,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<R: Read, W: Write>(
|
||||
reader: &mut R,
|
||||
writer: &mut W,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<ScriptAction> {
|
||||
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<Vec<String>> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<u32> {
|
||||
arg.trim().parse().ok()
|
||||
}
|
||||
|
||||
fn write_dumper_script(
|
||||
output_dir: &Path,
|
||||
actions: &[ScriptAction],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<usize, Box<dyn std::error::Error>> {
|
||||
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,
|
||||
}
|
||||
@@ -70,6 +70,7 @@ pub fn extract_funai_mstar(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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,
|
||||
|
||||
@@ -54,6 +54,7 @@ pub fn extract_mstar_secure_old(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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,
|
||||
|
||||
@@ -125,6 +125,7 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> 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,
|
||||
|
||||
@@ -75,7 +75,7 @@ pub fn extract_philips_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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");
|
||||
|
||||
@@ -119,11 +119,13 @@ pub fn extract_sony_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> 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,
|
||||
|
||||
+9
-2
@@ -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<PathBuf>,
|
||||
pub output_dir: PathBuf,
|
||||
pub options: Vec<String>,
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
};
|
||||
} 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,
|
||||
|
||||
Reference in New Issue
Block a user