From 13482802ba5afe7aca82f34d2de7bcaae4bce2a9 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:11:49 +0100 Subject: [PATCH] Add amlogic image extractor, sparse: add support for FILL chunk. --- README.md | 5 ++ src/formats.rs | 1 + src/formats/amlogic.rs | 108 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 6 ++- src/utils/sparse.rs | 10 ++++ 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/formats/amlogic.rs diff --git a/README.md b/README.md index 09d422f..1233822 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,11 @@ Build from source, by downloading the code or cloning the respository and runnin If an output folder is not provided, extracted files will be saved in folder `_`. # Supported formats +## Amlogic burning image +**Used in:** Android TVs and Boxes +**Notes:** V1 format is not supported because of the lack of sample file. +**Thanks to:** https://github.com/7Ji/ampack + ## Android OTA payload.bin **Used in:** Android devices, smartphones, TVs **Notes:** Some compression methods may not be supported. diff --git a/src/formats.rs b/src/formats.rs index c3dd604..d0d70d1 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -14,6 +14,7 @@ pub mod funai_upg; pub mod pana_dvd; pub mod android_ota_payload; pub mod bdl; +pub mod amlogic; pub mod pup; diff --git a/src/formats/amlogic.rs b/src/formats/amlogic.rs new file mode 100644 index 0000000..0f4180d --- /dev/null +++ b/src/formats/amlogic.rs @@ -0,0 +1,108 @@ +use std::fs::File; +use std::path::{Path}; +use std::fs::{self, OpenOptions}; +use std::io::{Write, Seek, SeekFrom}; +use binrw::{BinRead, BinReaderExt}; + +use crate::utils::common; +use crate::utils::sparse::{unsparse_to_file}; + +#[derive(BinRead)] +struct ImageHeader { + _crc32: u32, + version: u32, + #[br(count = 4)] _magic_bytes: Vec, //56 19 B5 27 + image_size: u64, + item_align_size: u32, + item_count: u32, + #[br(count = 36)] _reserved: Vec, +} + +#[derive(BinRead)] +struct ItemEntry { + _item_id: u32, + file_type: u32, + _current_offset_in_item: u64, + offset_in_image: u64, + item_size: u64, + #[br(count = 256)] item_type_bytes: Vec, + #[br(count = 256)] name_bytes: Vec, + _verify: u32, + _is_backup_item: u16, + _backup_item_id: u16, + #[br(count = 24)] _reserved: Vec, +} +impl ItemEntry { + fn item_type(&self) -> String { + common::string_from_bytes(&self.item_type_bytes) + } + fn name(&self) -> String { + common::string_from_bytes(&self.name_bytes) + } + fn is_sparse(&self) -> bool { + self.file_type == 254 + } +} + +pub fn is_amlogic_file(file: &File) -> bool { + let header = common::read_file(&file, 8, 4).expect("Failed to read from file."); + if header == b"\x56\x19\xB5\x27" { + true + } else { + false + } +} + +pub fn extract_amlogic(mut file: &File, output_folder: &str) -> Result<(), Box> { + file.seek(SeekFrom::Start(0))?; + let header: ImageHeader = file.read_le()?; + + println!("File info -\nImage size: {}\nItem align size: {}\nItem count: {}\nFormat version: {}", + header.image_size, header.item_align_size, header.item_count, header.version); + + if header.version != 2 { + println!("\nSorry, this format version is not yet supported!"); + return Ok(()); + } + + let mut items: Vec = Vec::new(); + + for _i in 0..header.item_count { + let item: ItemEntry = file.read_le()?; + items.push(item); + } + + for (i, item) in items.iter().enumerate() { + println!("\n({}/{}) - {}, Type: {}, Offset: {}, Size: {} {}", + i+1, header.item_count, item.name(), item.item_type(), item.offset_in_image, item.item_size, if item.is_sparse() {"[SPARSE]"} else {""}); + + if item.item_type() == "VERIFY" { //verify item is SHA1 of partition item + let sum_bytes = common::read_file(&file, item.offset_in_image, item.item_size as usize)?; + let sum = common::string_from_bytes(&sum_bytes); + println!("- Checksum for {}: {}", item.name(), sum); + + } else { + let data = common::read_file(&file, item.offset_in_image, item.item_size as usize)?; + + let extension = if item.item_type() == "PARTITION" {"img"} else {&item.item_type()}; + let output_path = Path::new(&output_folder).join(format!("{}.{}", item.name(), extension)); + fs::create_dir_all(&output_folder)?; + + if item.is_sparse() { + println!("- Unsparsing..."); + unsparse_to_file(&data, output_path)?; + println!("-- Saved file!"); + continue + + } else { + let mut out_file = OpenOptions::new().write(true).create(true).open(output_path)?; + out_file.write_all(&data)?; + println!("- Saved file!"); + } + + } + } + + println!("\nExtraction finished!"); + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 45ad6f6..642c0cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -115,7 +115,11 @@ fn main() -> Result<(), Box> { else if formats::pfl_upg::is_pfl_upg_file(&file) { println!("PFL UPG file detected!"); formats::pfl_upg::extract_pfl_upg(&file, &output_path)?; - } + } + else if formats::amlogic::is_amlogic_file(&file) { + println!("Amlogic image file detected!"); + formats::amlogic::extract_amlogic(&file, &output_path)?; + } else if formats::pana_dvd::is_pana_dvd_file(&file) { println!("PANA_DVD file detected!"); formats::pana_dvd::extract_pana_dvd(&file, &output_path)?; diff --git a/src/utils/sparse.rs b/src/utils/sparse.rs index aa8c8f8..2e4d788 100644 --- a/src/utils/sparse.rs +++ b/src/utils/sparse.rs @@ -41,6 +41,16 @@ pub fn unsparse_to_file(data: &[u8], file_path: PathBuf) -> Result<(), Box