From 558310c36ee9ce6f7a3ab71e1a2f24c43d65c212 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:07:41 +0200 Subject: [PATCH] nvt_timg: add support for more variants/types --- README.md | 4 +- src/formats/nvt_timg/include.rs | 199 +++++++++++++++++++++++++++++--- src/formats/nvt_timg/mod.rs | 58 ++++++++-- 3 files changed, 228 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 533d560..1a397cc 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,8 @@ Options: **Notes:** None, all files should be supported. ## Novatek TIMG -**Used in:** Newer Novatek-based TVs (Philips TitanOS, Hisense) -**Notes:** There is an older type of this format that is not yet supported, but for newer type all files should work. +**Used in:** Newer Novatek-based TVs (Philips(TPVision), Hisense, TCL...) +**Notes:** None, all files should be supported. ## Panasonic Blu-Ray (PANA_DVD.FRM, PANA_ESD.FRM, PANAEUSB.FRM) **Used in:** Panasonic Blu-Ray Players and Recorders diff --git a/src/formats/nvt_timg/include.rs b/src/formats/nvt_timg/include.rs index 1b91aa1..cb0e823 100644 --- a/src/formats/nvt_timg/include.rs +++ b/src/formats/nvt_timg/include.rs @@ -1,38 +1,199 @@ use crate::utils::common; use binrw::BinRead; -#[derive(Debug, BinRead)] -pub struct TIMG { - _magic_bytes: [u8; 4], //TIMG - _unused1: u32, - pub data_size: u32, - _unused2: u32, - _md5_checksum: [u8; 16], - _signature: [u8; 256], +#[derive(PartialEq, Eq, Debug)] +pub enum TimgVariant { + Old, + Old2, + New, +} + +pub trait TIMG { + fn _magic_bytes(&self) -> Vec; + fn data_size(&self) -> usize; + fn _data_checksum(&self) -> [u8; 16]; //md5 of data_size after timg header + fn _signature(&self) -> [u8; 256]; } #[derive(Debug, BinRead)] -pub struct PIMG { - pub magic_bytes: [u8; 4], //PIMG - _unused1: u32, - pub size: u32, - _unused2: u32, - _md5_checksum: [u8; 16], +pub struct TIMG64 { //new + _magic_bytes: [u8; 8], //TIMG/x00/x00/x00/x00 + data_size: u64, + _data_checksum: [u8; 16], + _signature: [u8; 256], +} +impl TIMG for TIMG64 { + fn _magic_bytes(&self) -> Vec { + self._magic_bytes.to_vec() + } + fn data_size(&self) -> usize { + self.data_size as usize + } + fn _data_checksum(&self) -> [u8; 16] { + self._data_checksum + } + fn _signature(&self) -> [u8; 256] { + self._signature + } +} + +#[derive(Debug, BinRead)] +pub struct TIMG32 { + _magic_bytes: [u8; 4], //TIMG + data_size: u32, + _data_checksum: [u8; 16], + _signature: [u8; 256], +} +impl TIMG for TIMG32 { + fn _magic_bytes(&self) -> Vec { + self._magic_bytes.to_vec() + } + fn data_size(&self) -> usize { + self.data_size as usize + } + fn _data_checksum(&self) -> [u8; 16] { + self._data_checksum + } + fn _signature(&self) -> [u8; 256] { + self._signature + } +} + +#[derive(Debug, BinRead)] +pub struct TIMGOld2 { + _magic_bytes: [u8; 4], //TIMG + data_size: u32, + _data_checksum: [u8; 16], + _pad: u32, + _signature: [u8; 256], +} +impl TIMG for TIMGOld2 { + fn _magic_bytes(&self) -> Vec { + self._magic_bytes.to_vec() + } + fn data_size(&self) -> usize { + self.data_size as usize + } + fn _data_checksum(&self) -> [u8; 16] { + self._data_checksum + } + fn _signature(&self) -> [u8; 256] { + self._signature + } +} + +pub trait PIMG { + fn magic_bytes(&self) -> Vec; + fn name(&self) -> String; + fn size(&self) -> usize; + fn _checksum(&self) -> [u8; 16]; //md5 of stored data + fn dest_dev(&self) -> String; + fn comp_type(&self) -> String; + fn comment(&self) -> String; +} + +#[derive(Debug, BinRead)] +pub struct PIMG64 { + magic_bytes: [u8; 8], //PIMG\x00\x00\x00\x00 + size: u64, + _checksum: [u8; 16], name_bytes: [u8; 16], dest_dev_bytes: [u8; 64], comp_type_bytes: [u8; 16], _unknown1: u32, - _comment: [u8; 1024], + comment_bytes: [u8; 1024], _unknown2: u32, } -impl PIMG { - pub fn name(&self) -> String { +impl PIMG for PIMG64 { + fn magic_bytes(&self) -> Vec { + self.magic_bytes.to_vec() + } + fn name(&self) -> String { common::string_from_bytes(&self.name_bytes) } - pub fn dest_dev(&self) -> String { + fn size(&self) -> usize { + self.size as usize + } + fn _checksum(&self) -> [u8; 16] { + self._checksum + } + fn dest_dev(&self) -> String { common::string_from_bytes(&self.dest_dev_bytes) } - pub fn comp_type(&self) -> String { + fn comp_type(&self) -> String { common::string_from_bytes(&self.comp_type_bytes) } + fn comment(&self) -> String { + common::string_from_bytes(&self.comment_bytes) + } +} + +#[derive(Debug, BinRead)] +pub struct PIMG32 { + magic_bytes: [u8; 4], //PIMG + size: u32, + _checksum: [u8; 16], + name_bytes: [u8; 16], + dest_dev_bytes: [u8; 32], + comp_type_bytes: [u8; 16], + _unknown1: u32, + comment_bytes: [u8; 1024], + _unknown2: u32, +} +impl PIMG for PIMG32 { + fn magic_bytes(&self) -> Vec { + self.magic_bytes.to_vec() + } + fn name(&self) -> String { + common::string_from_bytes(&self.name_bytes) + } + fn size(&self) -> usize { + self.size as usize + } + fn _checksum(&self) -> [u8; 16] { + self._checksum + } + fn dest_dev(&self) -> String { + common::string_from_bytes(&self.dest_dev_bytes) + } + fn comp_type(&self) -> String { + common::string_from_bytes(&self.comp_type_bytes) + } + fn comment(&self) -> String { + common::string_from_bytes(&self.comment_bytes) + } +} + +#[derive(Debug, BinRead)] +pub struct PIMGOld2 { + magic_bytes: [u8; 4], //PIMG + size: u32, + _checksum: [u8; 16], + name_bytes: [u8; 16], + dest_dev_bytes: [u8; 24], + comp_type_bytes: [u8; 16], + _unknown1: u32, +} +impl PIMG for PIMGOld2 { + fn magic_bytes(&self) -> Vec { + self.magic_bytes.to_vec() + } + fn name(&self) -> String { + common::string_from_bytes(&self.name_bytes) + } + fn size(&self) -> usize { + self.size as usize + } + fn _checksum(&self) -> [u8; 16] { + self._checksum + } + fn dest_dev(&self) -> String { + common::string_from_bytes(&self.dest_dev_bytes) + } + fn comp_type(&self) -> String { + common::string_from_bytes(&self.comp_type_bytes) + } + fn comment(&self) -> String { + "".to_string() //yes (this variant has no comment) + } } \ No newline at end of file diff --git a/src/formats/nvt_timg/mod.rs b/src/formats/nvt_timg/mod.rs index 581e4fd..677750a 100644 --- a/src/formats/nvt_timg/mod.rs +++ b/src/formats/nvt_timg/mod.rs @@ -12,35 +12,66 @@ use crate::utils::compression::{decompress_gzip}; use crate::utils::sparse::{unsparse_to_file}; use include::*; +pub struct TimgContext { + variant: TimgVariant, +} + pub fn is_nvt_timg_file(app_ctx: &AppContext) -> Result>, Box> { let file = match app_ctx.file() {Some(f) => f, None => return Ok(None)}; - let header = common::read_file(&file, 0, 4)?; - if header == b"TIMG" { - Ok(Some(Box::new(()))) + let header = common::read_file(&file, 0, 8)?; + if header == b"TIMG\x00\x00\x00\x00" { //new variant checks magic as 64bit int (probably) + Ok(Some(Box::new(TimgContext {variant: TimgVariant::New}))) + + } else if header.starts_with(b"TIMG") { + //check based on where the first PIMG appears, since Old2 header is 4 bytes bigger, it will appear later + let check = common::read_file(&file, 280, 8)?; + if &check[0..4] == b"PIMG" { + Ok(Some(Box::new(TimgContext {variant: TimgVariant::Old}))) + } + else if &check[4..8] == b"PIMG" { + Ok(Some(Box::new(TimgContext {variant: TimgVariant::Old2}))) + } + else { + Ok(None) //? + } } else { Ok(None) } } -pub fn extract_nvt_timg(app_ctx: &AppContext, _ctx: Box) -> Result<(), Box> { +pub fn extract_nvt_timg(app_ctx: &AppContext, ctx: Box) -> Result<(), Box> { let mut file = app_ctx.file().ok_or("Extractor expected file")?; + let ctx = ctx.downcast::().expect("Missing context"); - let file_size = file.metadata()?.len(); - let timg: TIMG = file.read_le()?; - println!("File info:\nData size: {}", timg.data_size); + let timg: Box = match ctx.variant { + TimgVariant::New => Box::new(file.read_le::()?), + TimgVariant::Old => Box::new(file.read_le::()?), + TimgVariant::Old2 => Box::new(file.read_le::()?), + }; + println!("File info:\nVariant: {:?}\nData size: {}", ctx.variant, timg.data_size()); + + //position after header + data size + let end = file.stream_position()? + timg.data_size() as u64; let mut pimg_i = 0; - while file.stream_position()? < file_size as u64 { + while file.stream_position()? < end { pimg_i += 1; - let pimg: PIMG = file.read_le()?; - if &pimg.magic_bytes != b"PIMG" { + + let pimg: Box = match ctx.variant { + TimgVariant::New => Box::new(file.read_le::()?), + TimgVariant::Old => Box::new(file.read_le::()?), + TimgVariant::Old2 => Box::new(file.read_le::()?), + }; + + if !pimg.magic_bytes().starts_with(b"PIMG") { return Err("Invalid PIMG magic!".into()); } - let data = common::read_exact(&mut file, pimg.size as usize)?; + let data = common::read_exact(&mut file, pimg.size())?; - println!("\n#{} - {}, Size: {}, Dest: {}, Compression: {}", pimg_i, pimg.name(), pimg.size, pimg.dest_dev(), pimg.comp_type()); + println!("\n#{} - {}, Size: {}, Dest: {}, Compression: {}, Comment: {}", + pimg_i, pimg.name(), pimg.size(), pimg.dest_dev(), pimg.comp_type(), pimg.comment()); let out_data; let output_path = Path::new(&app_ctx.output_dir).join(pimg.name() + ".bin"); @@ -48,13 +79,16 @@ pub fn extract_nvt_timg(app_ctx: &AppContext, _ctx: Box) -> Result<(), if pimg.comp_type() == "gzip" && data.starts_with(b"\x1F\x8B") { //additionally check for gzip header, because sometimes its deceptive println!("- Decompressing gzip..."); out_data = decompress_gzip(&data)?; + } else if pimg.comp_type() == "none" || pimg.comp_type() == "" { out_data = data; + } else if pimg.comp_type() == "sparse" { println!("- Unsparsing..."); unsparse_to_file(&data, output_path)?; println!("-- Saved file!"); continue + } else { println!("- Warning: unsupported compression type, saving stored data!"); out_data = data;