mstar: add sparse_write and LZO compression support + some fixes

This commit is contained in:
theubusu
2025-11-23 21:24:56 +01:00
parent 2c996d19b1
commit faad86f5a7
7 changed files with 187 additions and 17 deletions
Generated
+10
View File
@@ -474,6 +474,15 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 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]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -785,6 +794,7 @@ dependencies = [
"lz4", "lz4",
"lzma-rs", "lzma-rs",
"md5", "md5",
"minilzo-rs",
"rsa", "rsa",
"sha1", "sha1",
"simd-adler32", "simd-adler32",
+1
View File
@@ -18,3 +18,4 @@ ecb = "0.1"
tar = "0.4.44" tar = "0.4.44"
binrw = "0.15" binrw = "0.15"
simd-adler32 = "*" simd-adler32 = "*"
minilzo-rs = "0.6.1"
+36 -13
View File
@@ -5,6 +5,13 @@ use lz4::block::decompress;
use lzma_rs::lzma_decompress; use lzma_rs::lzma_decompress;
use crate::common; 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 { pub fn is_mstar_file(file: &File) -> bool {
let header = common::read_file(&file, 0, 32768).expect("Failed to read from file."); 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<dyn std
let size = parse_number(parts[4]).unwrap_or(0); let size = parse_number(parts[4]).unwrap_or(0);
//try to get partname from comment //try to get partname from comment
let mut partname = if let Some(idx) = line.find('#') { //let mut partname = if let Some(idx) = line.find('#') {
line[idx + 1..].trim() // line[idx + 1..].trim()
} else { //} else {
"unknown" // "unknown"
}; //};
let mut partname = "unknown";
let mut compression = "none"; let mut compression = "none";
let mut lz4_expect_size = 0; let mut lz4_expect_size = 0;
@@ -114,6 +122,14 @@ pub fn extract_mstar(file: &File, output_folder: &str) -> Result<(), Box<dyn std
partname = parts[4] partname = parts[4]
} }
} }
if lines[j].starts_with("sparse_write"){
compression = "sparse"; //its not really compression but anyway
let parts: Vec<&str> = lines[j].split_whitespace().collect();
// get part name from sparse_write
if partname == "unknown" {
partname = parts[3]
}
}
// check if its boot partition // check if its boot partition
if lines[j].starts_with("mmc write.boot") { if lines[j].starts_with("mmc write.boot") {
@@ -133,13 +149,16 @@ pub fn extract_mstar(file: &File, output_folder: &str) -> Result<(), Box<dyn std
j += 1; j += 1;
} }
println!("\nPart: Offset: {}, Size: {} --> {}", offset, size, partname); println!("\nPart - Offset: {}, Size: {} --> {}", offset, size, partname);
if partname == "unknown"{ if partname == "unknown" {
println!("- Unknown destination, skipping!"); println!("- Unknown destination, skipping!");
} else if partname == "userdata" && CONFIG_SKIP_USERDATA {
println!("- Skipping userdata according to config!")
} else { } else {
let data = common::read_file(&file, offset, size.try_into().unwrap())?; 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" { if compression == "lzma" {
println!("- Decompressing LZMA..."); println!("- Decompressing LZMA...");
@@ -153,15 +172,19 @@ pub fn extract_mstar(file: &File, output_folder: &str) -> Result<(), Box<dyn std
println!("- Decompressing lz4, expected size: {}", lz4_expect_size); println!("- Decompressing lz4, expected size: {}", lz4_expect_size);
out_data = decompress_lz4(&data, lz4_expect_size.try_into().unwrap())?; out_data = decompress_lz4(&data, lz4_expect_size.try_into().unwrap())?;
} else if compression == "lzo" { } else if compression == "lzo" {
// nothing in rust to parse lzo file with header println!("- Decompessing LZO..");
println!("- lzo compression is not supported yet!"); out_data = decompress_lzop(&data)?;
out_data = data; } else if compression == "sparse" {
}else { println!("- Unsparsing...");
unsparse_to_file(&data, output_path)?;
println!("-- Saved file!");
i += 1;
continue
//out_data = unsparse(&data)?;
} else {
out_data = data; out_data = data;
} }
let output_path = Path::new(&output_folder).join(partname.to_owned() + ".bin");
fs::create_dir_all(&output_folder)?; fs::create_dir_all(&output_folder)?;
let mut out_file = OpenOptions::new() let mut out_file = OpenOptions::new()
.append(true) .append(true)
+2
View File
@@ -1,3 +1,5 @@
pub mod aes; pub mod aes;
pub mod pana_dvd_crypto; pub mod pana_dvd_crypto;
pub mod lzss; pub mod lzss;
pub mod lzop;
pub mod sparse;
+73
View File
@@ -0,0 +1,73 @@
use binrw::{BinRead, BinReaderExt};
use std::io::{Cursor};
use simd_adler32::adler32;
use crate::common;
#[derive(BinRead)]
struct LzopHeader {
#[br(count = 9)] magic_bytes: Vec<u8>,
_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<u8>,
_header_checksum: u32,
}
#[derive(BinRead, Debug)]
struct SegmentHeader {
uncompressed_size: u32,
compressed_size: u32,
checksum: u32,
}
pub fn decompress_lzop(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
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)
}
+61
View File
@@ -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<u8>,
_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<dyn std::error::Error>> {
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(())
}
+1 -1
View File
@@ -7,7 +7,7 @@
| INVINCIBLE_IMAGE | LG Broadcom-based Blu-Ray players | Only version 3 is supported (2011+) | - | | 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.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 | | 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 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 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 | - | | Mediatek upgrade_loader | Older Mediatek-based TVs | All files should be supported providing they are not encrypted | - |