From 5bc4394cf0bc8dca6bee392406e3c9d1cdd8d3dd Mon Sep 17 00:00:00 2001 From: Pari Malam Date: Mon, 15 Jun 2026 23:56:39 +0800 Subject: [PATCH] + emmc gpt --- src/formats.rs | 6 + src/formats/emmc_gpt/mod.rs | 259 ++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 src/formats/emmc_gpt/mod.rs diff --git a/src/formats.rs b/src/formats.rs index 64526fb..14cf36d 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -49,6 +49,7 @@ pub mod amlogic; pub mod sdboot; pub mod sdimage; pub mod emmc_userarea; +pub mod emmc_gpt; pub mod cd5; pub mod gx_dvb; pub mod onkyo; @@ -125,6 +126,11 @@ pub fn get_registry() -> Vec { detector_func: crate::formats::emmc_userarea::is_emmc_userarea_file, extractor_func: crate::formats::emmc_userarea::extract_emmc_userarea, }, + Format { + name: "emmc_gpt", + detector_func: crate::formats::emmc_gpt::is_emmc_gpt_file, + extractor_func: crate::formats::emmc_gpt::extract_emmc_gpt, + }, Format { name: "novatek", detector_func: crate::formats::novatek::is_novatek_file, diff --git a/src/formats/emmc_gpt/mod.rs b/src/formats/emmc_gpt/mod.rs new file mode 100644 index 0000000..9e4ede9 --- /dev/null +++ b/src/formats/emmc_gpt/mod.rs @@ -0,0 +1,259 @@ +use std::any::Any; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +use log::{info, warn}; + +use crate::AppContext; +use crate::utils::common; + +const SECTOR_SIZE: u64 = 512; +const GPT_HEADER_LBA: u64 = 1; +const GPT_HEADER_SIZE: usize = 92; +const MAX_GPT_ENTRIES: usize = 128; +const TABLE_DUMP_SIZE: usize = 0x10000; + +#[derive(Debug)] +pub struct GptPartition { + name: String, + start_lba: u64, + end_lba: u64, +} + +impl GptPartition { + fn offset(&self) -> u64 { + self.start_lba * SECTOR_SIZE + } + + fn size(&self) -> u64 { + (self.end_lba - self.start_lba + 1) * SECTOR_SIZE + } +} + +fn sanitize_name(raw: &[u8]) -> String { + let name = raw + .chunks(2) + .take_while(|c| c[0] != 0 || c[1] != 0) + .map(|c| char::from_u32(u32::from_le_bytes([c[0], c[1], 0, 0])).unwrap_or('?')) + .collect::(); + + if name.trim().is_empty() { + return String::new(); + } + + name.trim() + .chars() + .filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.') + .collect() +} + +fn parse_gpt_header(data: &[u8]) -> Option<(u64, u64, u64, u32, u32)> { + if data.len() < GPT_HEADER_SIZE || &data[0..8] != b"EFI PART" { + return None; + } + + let entries_lba = u64::from_le_bytes(data[72..80].try_into().unwrap()); + let num_entries = u32::from_le_bytes(data[80..84].try_into().unwrap()); + let entry_size = u32::from_le_bytes(data[84..88].try_into().unwrap()); + + let first_usable = u64::from_le_bytes(data[40..48].try_into().unwrap()); + let last_usable = u64::from_le_bytes(data[48..56].try_into().unwrap()); + + Some((entries_lba, first_usable, last_usable, num_entries, entry_size)) +} + +fn parse_gpt_entries(data: &[u8], entry_count: u32, entry_size: u32, file_size: u64) -> Vec { + let mut partitions = Vec::new(); + let esize = entry_size as usize; + + for i in 0..entry_count as usize { + if i >= MAX_GPT_ENTRIES { + break; + } + let off = i * esize; + if off + 32 > data.len() { + break; + } + + // Check if type GUID is all zeros (unused entry) + if data[off..off + 16].iter().all(|&b| b == 0) { + break; // GPT entries are contiguous; stop at first unused + } + + let start_lba = u64::from_le_bytes(data[off + 32..off + 40].try_into().unwrap()); + let end_lba = u64::from_le_bytes(data[off + 40..off + 48].try_into().unwrap()); + let name_raw = &data[off + 56..off + 128]; + + if start_lba == 0 || end_lba == 0 { + continue; + } + + let name = sanitize_name(name_raw); + if name.is_empty() { + continue; + } + + let part_end = (end_lba + 1) * SECTOR_SIZE; + if part_end > file_size { + warn!( + "Skipping GPT partition '{}' at LBA {}: extends beyond image (end=0x{:x}, file_size=0x{:x})", + name, start_lba, part_end, file_size + ); + continue; + } + + partitions.push(GptPartition { + name, + start_lba, + end_lba, + }); + } + + partitions +} + +pub fn is_emmc_gpt_file( + app_ctx: &AppContext, +) -> Result>, Box> { + let file = match app_ctx.file() { + Some(file) => file, + None => return Ok(None), + }; + + let file_size = file.metadata()?.len(); + if file_size < SECTOR_SIZE * 2 || file_size % SECTOR_SIZE != 0 { + return Ok(None); + } + + // Read MBR (first sector) + let mbr = common::read_file(file, 0, SECTOR_SIZE as usize)?; + + // Check MBR signature 0x55AA + if mbr[0x1FE] != 0x55 || mbr[0x1FF] != 0xAA { + return Ok(None); + } + + // Check for GPT protective MBR entry (type 0xEE) + let has_protective = (0x1BE..0x1FE).step_by(16).any(|i| mbr[i + 4] == 0xEE); + if !has_protective { + return Ok(None); + } + + // Read GPT header at LBA 1 + let gpt_data = common::read_file(file, SECTOR_SIZE * GPT_HEADER_LBA, GPT_HEADER_SIZE)?; + + if parse_gpt_header(&gpt_data).is_none() { + return Ok(None); + } + + // We need the entry_size to parse entries, so re-parse + let (entries_lba, first_usable, _last_usable, num_entries, entry_size) = + parse_gpt_header(&gpt_data).unwrap(); + + let entries_byte_size = (num_entries as usize) + .min(MAX_GPT_ENTRIES) + .saturating_mul(entry_size as usize); + + let entries_data = common::read_file( + file, + entries_lba * SECTOR_SIZE, + entries_byte_size, + )?; + + let partitions = parse_gpt_entries(&entries_data, num_entries, entry_size, file_size); + + if partitions.is_empty() { + return Ok(None); + } + + info!( + "Detected GPT-partitioned eMMC image: {} partitions (size={}, first_usable_lba={})", + partitions.len(), + file_size, + first_usable, + ); + + Ok(Some(Box::new(partitions))) +} + +pub fn extract_emmc_gpt( + app_ctx: &AppContext, + ctx: Box, +) -> Result<(), Box> { + let partitions = ctx + .downcast::>() + .map_err(|_| "Invalid GPT eMMC context")?; + + fs::create_dir_all(&app_ctx.output_dir)?; + + let mut input = app_ctx.file().ok_or("Extractor expected file")?; + + for partition in &*partitions { + let safe_name: String = partition + .name + .chars() + .map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' }) + .collect(); + + let output_path = Path::new(&app_ctx.output_dir).join(format!("{}.bin", safe_name)); + let mut output = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&output_path)?; + + input.seek(SeekFrom::Start(partition.offset()))?; + let copied = io::copy(&mut input.take(partition.size()), &mut output)?; + + info!( + " {}: offset=0x{:x}, size={}, copied={}", + partition.name, + partition.offset(), + partition.size(), + copied, + ); + } + + write_partition_map(&app_ctx.output_dir, &partitions)?; + write_gpt_table(app_ctx.file().ok_or("Extractor expected file")?, &app_ctx.output_dir)?; + + Ok(()) +} + +fn write_partition_map( + output_dir: &Path, + partitions: &[GptPartition], +) -> Result<(), Box> { + let output_path = output_dir.join("partition_map.txt"); + let mut output = File::create(output_path)?; + + writeln!( + output, + "name\tstart_lba\tend_lba\toffset\tsize" + )?; + for partition in partitions { + writeln!( + output, + "{}\t{}\t{}\t0x{:x}\t{}", + partition.name, + partition.start_lba, + partition.end_lba, + partition.offset(), + partition.size(), + )?; + } + + Ok(()) +} + +fn write_gpt_table( + input: &File, + output_dir: &Path, +) -> Result<(), Box> { + let table = common::read_file(input, 0, TABLE_DUMP_SIZE)?; + let output_path = output_dir.join("partition_table.bin"); + let mut output = File::create(output_path)?; + output.write_all(&table)?; + Ok(()) +}