From faad86f5a718332e9075ded180a4a7a19e298770 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:24:56 +0100 Subject: [PATCH] mstar: add sparse_write and LZO compression support + some fixes --- Cargo.lock | 10 ++++++ Cargo.toml | 3 +- src/formats/mstar.rs | 51 ++++++++++++++++++++++--------- src/utils.rs | 4 ++- src/utils/lzop.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++ src/utils/sparse.rs | 61 ++++++++++++++++++++++++++++++++++++ unixtract.md | 2 +- 7 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 src/utils/lzop.rs create mode 100644 src/utils/sparse.rs diff --git a/Cargo.lock b/Cargo.lock index 259049a..501e2fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "minilzo-rs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71a19a4b57d7712a7946e51a7a998f1d8252e16000706ae63243e951fcfce09" +dependencies = [ + "cc", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -785,6 +794,7 @@ dependencies = [ "lz4", "lzma-rs", "md5", + "minilzo-rs", "rsa", "sha1", "simd-adler32", diff --git a/Cargo.toml b/Cargo.toml index 36d52f9..40380f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ hex = "0.4" ecb = "0.1" tar = "0.4.44" binrw = "0.15" -simd-adler32 = "*" \ No newline at end of file +simd-adler32 = "*" +minilzo-rs = "0.6.1" \ No newline at end of file diff --git a/src/formats/mstar.rs b/src/formats/mstar.rs index 13744a0..16a7372 100644 --- a/src/formats/mstar.rs +++ b/src/formats/mstar.rs @@ -5,6 +5,13 @@ use lz4::block::decompress; use lzma_rs::lzma_decompress; use crate::common; +use crate::utils::lzop::{decompress_lzop}; +use crate::utils::sparse::{unsparse_to_file}; + +//change whether the "userdata" partition is skipped +// this is because the userdata partition is sometimes enourmous sizes like 27gb, and it will fail to allocate memory on most computers +// if you want to attempt to extract "userdata" you can enable the option +static CONFIG_SKIP_USERDATA: bool = true; pub fn is_mstar_file(file: &File) -> bool { let header = common::read_file(&file, 0, 32768).expect("Failed to read from file."); @@ -80,11 +87,12 @@ pub fn extract_mstar(file: &File, output_folder: &str) -> Result<(), Box Result<(), Box = lines[j].split_whitespace().collect(); + // get part name from sparse_write + if partname == "unknown" { + partname = parts[3] + } + } // check if its boot partition if lines[j].starts_with("mmc write.boot") { @@ -133,13 +149,16 @@ pub fn extract_mstar(file: &File, output_folder: &str) -> Result<(), Box {}", offset, size, partname); + println!("\nPart - Offset: {}, Size: {} --> {}", offset, size, partname); - if partname == "unknown"{ + if partname == "unknown" { println!("- Unknown destination, skipping!"); + } else if partname == "userdata" && CONFIG_SKIP_USERDATA { + println!("- Skipping userdata according to config!") } else { let data = common::read_file(&file, offset, size.try_into().unwrap())?; - let out_data; + let out_data; + let output_path = Path::new(&output_folder).join(format!("{}.bin", partname)); if compression == "lzma" { println!("- Decompressing LZMA..."); @@ -153,15 +172,19 @@ pub fn extract_mstar(file: &File, output_folder: &str) -> Result<(), Box, + _version: u16, + _lib_version: u16, + _version_needed_to_extract: u16, + method: u8, + _level: u8, + _flags: u32, + _mode: u32, + _mtime_low: u32, + _mtime_high: u32, + _name_len: u8, + #[br(count = _name_len)] _name_bytes: Vec, + _header_checksum: u32, +} + +#[derive(BinRead, Debug)] +struct SegmentHeader { + uncompressed_size: u32, + compressed_size: u32, + checksum: u32, +} + +pub fn decompress_lzop(data: &[u8]) -> Result, Box> { + let mut data_reader = Cursor::new(data); + let header: LzopHeader = data_reader.read_be()?; + if header.magic_bytes != b"\x89LZO\x00\x0D\x0A\x1A\x0A" { + return Err("Invalid magic!".into()); + } + if ![1, 2, 3].contains(&header.method) { + return Err("Unsupported compression method!".into()); + } + + let lzo = minilzo_rs::LZO::init()?; + let mut decompressed_output = Vec::new(); + loop { + if (data.len() as u64 - data_reader.position()) < 12 { //check if there are enough bytes to read a segment header + break; + } + let segment_header: SegmentHeader = data_reader.read_be()?; + if segment_header.compressed_size > segment_header.uncompressed_size { + println!("{:?}", segment_header); + } + //println!("{:?}", segment_header); + if segment_header.uncompressed_size == 0 { + break + } + + let stored_data = common::read_exact(&mut data_reader, segment_header.compressed_size as usize)?; + let out_data = + if segment_header.uncompressed_size == segment_header.compressed_size { //if uncomp size = comp size, this segment is not compressed + stored_data + } else { + lzo.decompress(&stored_data, segment_header.uncompressed_size as usize)? + }; + + let calc_checksum = adler32(&out_data.as_slice()); + if calc_checksum != segment_header.checksum { + return Err("Invalid segment checksum! Data corrupted?".into()); + }; + + decompressed_output.extend_from_slice(&out_data); + } + + Ok(decompressed_output) +} \ No newline at end of file diff --git a/src/utils/sparse.rs b/src/utils/sparse.rs new file mode 100644 index 0000000..deebf2c --- /dev/null +++ b/src/utils/sparse.rs @@ -0,0 +1,61 @@ +use binrw::{BinRead, BinReaderExt}; +use std::fs::OpenOptions; +use std::path::{PathBuf}; +use std::io::{Cursor, Seek, SeekFrom, Write}; + +use crate::common; + +#[derive(BinRead)] +struct SparseHeader { + #[br(count = 4)] magic_bytes: Vec, + _major_version: u16, + _minor_version: u16, + _file_header_size: u16, + _chunk_header_size: u16, + block_size: u32, + _total_blocks: u32, + total_chunks: u32, + _image_checksum: u32 +} + +#[derive(BinRead, Debug)] +struct ChunkHeader { + chunk_type: u16, + _reserved1: u16, + chunk_size: u32, + total_size: u32, +} + +pub fn unsparse_to_file(data: &[u8], file_path: PathBuf) -> Result<(), Box> { + let mut data_reader = Cursor::new(data); + let file_header: SparseHeader = data_reader.read_le()?; + if file_header.magic_bytes != b"\x3A\xFF\x26\xED" { + return Err("Invalid magic!".into()); + } + + let mut out_file = OpenOptions::new().create(true).read(true).write(true).open(file_path)?; + + for _i in 0..file_header.total_chunks{ + let chunk_header: ChunkHeader = data_reader.read_le()?; + let chunk_data = common::read_exact(&mut data_reader, chunk_header.total_size as usize - 12)?; + + if chunk_header.chunk_type == 0xCAC1 { //"raw" type chunk (actual data) + out_file.write_all(&chunk_data)?; + } else if chunk_header.chunk_type == 0xCAC3 { // "dont care" type chunk (skip over) + let skip_size = file_header.block_size as u64 * chunk_header.chunk_size as u64; + let current_pos = out_file.stream_position()?; + let new_pos = current_pos + skip_size; + + //enlarge file with zeros if the seek is larger than file + let current_file_size = out_file.metadata()?.len(); + if new_pos > current_file_size { + out_file.set_len(new_pos)?; + } + + out_file.seek(SeekFrom::Start(new_pos))?; + } + } + + Ok(()) + +} \ No newline at end of file diff --git a/unixtract.md b/unixtract.md index 477064e..88ec226 100644 --- a/unixtract.md +++ b/unixtract.md @@ -7,7 +7,7 @@ | INVINCIBLE_IMAGE | LG Broadcom-based Blu-Ray players | Only version 3 is supported (2011+) | - | | MSD v1.0 | Samsung TVs 2013-2015 | Only some files are supported (**depends on keys**) | https://github.com/bugficks/msddecrypt | | MSD v1.1 | Samsung TVs 2016+ | Only some files are supported (**depends on keys**) | https://github.com/bugficks/msddecrypt | -| Mstar upgrade bin | Many MStar-based TVs (Hisense, Toshiba..) | Files not using sparse_write and LZO compression are supported | - | +| Mstar upgrade bin | Many MStar-based TVs (Hisense, Toshiba..) | Most files should be supported | - | | Mediatek BDP | Many Mediatek-based Blu-Ray players (LG, Samsung, Philips, Panasonic...) | Some older files may not be supported | - | | Mediatek PKG | Many Mediatek-based TVs (Hisense, Sony, Panasonic, Philips...) | Only files with 144 byte header and using 4xVendor magic as enc key are supported | https://github.com/openlgtv/epk2extract | | Mediatek upgrade_loader | Older Mediatek-based TVs | All files should be supported providing they are not encrypted | - |