pana_dvd: improve some struct definitions and logic

This commit is contained in:
theubusu
2026-02-28 00:14:19 +01:00
parent 223c0998eb
commit 1b17d9388d
3 changed files with 147 additions and 97 deletions
+67 -36
View File
@@ -1,8 +1,10 @@
use crate::utils::common;
use binrw::BinRead;
use binrw::{BinRead, BinWrite};
use super::pana_dvd_crypto::{decrypt_data};
use crate::utils::aes::{decrypt_aes128_cbc_nopad};
//find key
pub fn find_key<'a>(key_array: &'a [&'a str], data: &[u8], expected_magic: &[u8], magic_offset: usize) -> Result<Option<[u8; 8]>, Box<dyn std::error::Error>> {
for key_hex in key_array {
let key_bytes = hex::decode(key_hex)?;
@@ -33,7 +35,7 @@ pub fn find_aes_key_pair<'a>(key_array: &'a [(&'a str, &'a str, &'a str)], data:
Ok(None)
}
pub static MAX_HEADER_SIZE: usize = 0x2000;
// --
#[derive(BinRead)]
pub struct AesHeaderFileEntry {
@@ -43,24 +45,28 @@ pub struct AesHeaderFileEntry {
pub struct FileEntry {
pub offset: u32,
pub base_offset: u32,
pub size: u32,
pub header_size: u32,
}
//checksums are mostly Adler32, but some very old files use Checksum32 instead.
#[derive(BinRead)]
pub static LIST_SIZE: usize = 0x1800;
#[derive(BinRead, BinWrite)]
pub struct ModuleEntry {
#[br(count = 4)] pub name_bytes: Vec<u8>,
name_bytes: [u8; 4],
version_bytes: [u8; 4],
_unk: u32,
pub offset: u32,
platform_bytes: [u8; 8],
_unk1: u16,
model_id_bytes: [u8; 8],
pub _oem_id: u8, //0x4D = Panasonic
pub _force_update_flag: u8, //if not 0x00, force the update and also "Scheduling Format HDD"
id_bytes: [u8; 6],
pub size: u32,
pub data_checksum: u32, //checksum of the entrys' DATA
_unk2: u32,
_entry_checksum: u32, //checksum of THIS header entry (all previous 44 bytes)
pub data_checksum: u32, //checksum of the entrys' DATA
pub _flags: [u8; 4], //last byte denotes compression type in some firmwares, see CompressionType
pub _entry_checksum: u32, //checksum of THIS header entry (all previous 44 bytes)
}
impl ModuleEntry {
pub fn name(&self) -> String {
@@ -69,24 +75,24 @@ impl ModuleEntry {
pub fn version(&self) -> String {
common::string_from_bytes(&self.version_bytes)
}
pub fn platform(&self) -> String {
common::string_from_bytes(&self.platform_bytes)
pub fn model_id(&self) -> String {
common::string_from_bytes(&self.model_id_bytes)
}
pub fn id(&self) -> String {
common::string_from_bytes(&self.id_bytes)
}
pub fn is_valid(&self) -> bool {
self.name().is_ascii() && self.platform().is_ascii()
self.name().is_ascii() && self.model_id().is_ascii()
}
}
#[derive(BinRead)]
pub struct MainListHeader {
_checksum: u32, //checksum of the MAIN LIST
_unk: u32, //seems to be always 1?
pub list_size: u32,
pub decompressed_part_size: u32,
_unk2: u32,
pub _checksum: u32, //checksum of whole list excluding this field
_unk: u32, //seems to be always 1
pub list_size: u32, //size of the entire list, including the entries AND this header
pub decompressed_part_size: u32,
pub _compression_type: u32, //see CompressionType
}
impl MainListHeader {
pub fn entry_count(&self) -> u32 {
@@ -105,30 +111,55 @@ pub const COMPRESSED_FILE_MAGIC: &[u8; 8] = b"EXTRHEAD";
#[derive(BinRead)]
pub struct CompressedFileHeader {
_magic_bytes: [u8; 8], // EXTRHEAD
_unk_string: [u8; 4], // DRV\x20 ?
_compressed_flag: u16, // checks for 1 here, else -> "Error! Not compress"
pub compression_type: u16, // 0 - not compressed, 1 - GZIP , 2 - LZSS
_unk_string: [u8; 4], // "DRV " ?
pub _compressed_flag: u16, // checks for 1 here, else -> "Error! Not compress"
pub compression_type: u16, // see CompressionType. GzipAndLzss is not used in this context.
pub dest_size: u32, // decompressed size
_dest_address: u32,
pub _dest_address: u32,
pub src_size: u32, // compressed size
_src_address: u32,
_footer_offset: u32, // offset to EXTRFOOT
pub _src_address: u32,
pub _footer_offset: u32, // offset to footer, with magic bytes "EXTRFOOT". it aligns data to 8 bytes for encryption.
_unk: u32,
_checksum: u32, // adler32 calculated every checksum_skip bytes of decompressed data
_checksum_skip: u32,
pub _checksum: u32, // adler32 calculated every checksum_skip bytes of decompressed data
pub _checksum_skip: u32,
_unused: [u8; 16],
}
impl CompressedFileHeader {
pub fn compression_type_str(&self) -> &str {
if self.compression_type == 0 {
return "Uncompressed"
} else if self.compression_type == 1 {
return "GZIP"
} else if self.compression_type == 2 {
return "LZSS"
} else {
return "Unknown"
#[derive(Debug, PartialEq)]
pub enum CompressionType {
None,
Gzip,
Lzss,
GzipAndLzss,
Unknown,
}
impl From<u16> for CompressionType {
fn from(value: u16) -> Self {
match value {
0 => CompressionType::None,
1 => CompressionType::Gzip,
2 => CompressionType::Lzss,
3 => CompressionType::GzipAndLzss,
_ => CompressionType::Unknown
}
}
}
#[derive(BinRead)]
pub struct DriveHeader {
manufacturer_bytes: [u8; 8],
model_bytes: [u8; 16],
version_bytes: [u8; 7],
_unk: u8,
}
impl DriveHeader {
pub fn manufacturer(&self) -> String {
common::string_from_bytes(&self.manufacturer_bytes)
}
pub fn model(&self) -> String {
common::string_from_bytes(&self.model_bytes)
}
pub fn version(&self) -> String {
common::string_from_bytes(&self.version_bytes)
}
}
+78 -61
View File
@@ -7,7 +7,7 @@ use crate::AppContext;
use std::path::{Path, PathBuf};
use std::fs::{self, OpenOptions};
use std::io::{Write, Read, Cursor, Seek, SeekFrom};
use std::io::{Write, Cursor, Seek, SeekFrom};
use binrw::BinReaderExt;
use crate::keys;
@@ -64,78 +64,82 @@ pub fn extract_pana_dvd(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), B
let mut file = app_ctx.file().ok_or("Extractor expected file")?;
let context = ctx.downcast::<PanaDvdContext>().expect("Missing context");
let mut data = Vec::new(); // we need to load the entire file into memory so we can swap it with AES decrypted data if its AES encrypted
file.read_to_end(&mut data)?;
let mut file_reader = Cursor::new(data);
let matching_key = context.matching_key;
let mut file_entries: Vec<FileEntry> = Vec::new();
//AES files can contain multiple firmwares inside of itself
if context.is_aes {
let (aes_key, aes_iv) = (context.aes_key.unwrap(), context.aes_iv.unwrap());
println!("Using key: {} + AES key: {}, IV: {}", hex::encode_upper(matching_key), hex::encode_upper(aes_key), hex::encode_upper(aes_iv));
println!("Decrypting AES...\n");
let aes_decrypted = decrypt_aes128_cbc_nopad(&file_reader.get_ref(), &aes_key, &aes_iv)?;
file_reader = Cursor::new(aes_decrypted); //set the file reader to use AES decrypted stream
//read file entries in extra header
let file_table = common::read_exact(&mut file_reader, 48)?;
let mut file_table_reader = Cursor::new(decrypt_data(&file_table, &matching_key));
//read inner file table
let file_table = common::read_exact(&mut file, 48)?;
let file_table_dec_aes = decrypt_aes128_cbc_nopad(&file_table, &aes_key, &aes_iv)?;
let mut file_table_reader = Cursor::new(decrypt_data(&file_table_dec_aes, &matching_key));
for _i in 0..4 {
let file_entry: AesHeaderFileEntry = file_table_reader.read_le()?;
if file_entry.size == 0 && file_entry.offset == 0 {
break
}
//ignore duplicate entries
if !file_entries.iter().any(|f| f.offset == file_entry.offset ){
file_entries.push(FileEntry { offset: file_entry.offset, base_offset: context.base_hdr_size });
file_entries.push(FileEntry { offset: file_entry.offset, size: file_entry.size, header_size: context.base_hdr_size });
}
}
} else {
println!("Using key: {}", hex::encode_upper(matching_key));
file_entries.push(FileEntry { offset: 0, base_offset: context.base_hdr_size });
file_entries.push(FileEntry { offset: 0, size: file.metadata()?.len() as u32, header_size: context.base_hdr_size });
}
if file_entries.len() == 1 {
//only one file, standard extraction
println!("File contains no extra sub-files...\n");
extract_file(app_ctx, &mut file_reader, file_entries[0].offset as u64, file_entries[0].base_offset as u64, matching_key, &app_ctx.output_dir)?;
} else {
println!("File contains {} sub-files...", file_entries.len());
for (i, file_entry ) in file_entries.iter().enumerate() {
println!("\nExtracting file {}/{} - Offset: {}, base: {}", i + 1, file_entries.len(), file_entry.offset, file_entry.base_offset);
extract_file(app_ctx, &mut file_reader, file_entry.offset as u64, file_entry.base_offset as u64, matching_key, &app_ctx.output_dir.join(format!("file_{}", i + 1)))?;
}
println!("File contains {} sub-files...", file_entries.len());
for (i, file_entry ) in file_entries.iter().enumerate() {
let data = common::read_file(&mut file, file_entries[0].offset as u64, file_entries[0].size as usize)?;
let dec_data = if context.is_aes {
let (aes_key, aes_iv) = (context.aes_key.unwrap(), context.aes_iv.unwrap());
decrypt_aes128_cbc_nopad(&data, &aes_key, &aes_iv)?
} else {
data
};
let mut file_reader = Cursor::new(dec_data);
let output_dir = if file_entries.len() == 1 {
&app_ctx.output_dir
} else {
&app_ctx.output_dir.join(format!("file_{}", i + 1))
};
println!("\nExtracting file {}/{} - Offset: {}, Size: {}, Header size: {}",
i + 1, file_entries.len(), file_entry.offset, file_entry.size, file_entry.header_size);
extract_file(app_ctx, &mut file_reader, file_entry.header_size as u64, matching_key, output_dir)?;
}
Ok(())
}
fn extract_file(app_ctx: &AppContext, file_reader: &mut Cursor<Vec<u8>>, offset: u64, base_offset: u64, key: [u8; 8], output_folder: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
file_reader.seek(SeekFrom::Start(offset + base_offset))?;
let enc_header = common::read_exact(file_reader, MAX_HEADER_SIZE)?;
let dec_header = decrypt_data(&enc_header, &key);
opt_dump_dec_hdr(app_ctx, &dec_header, "header")?;
fn extract_file(app_ctx: &AppContext, file_reader: &mut Cursor<Vec<u8>>, header_size: u64, key: [u8; 8], output_folder: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let enc_list = common::read_exact(file_reader, LIST_SIZE)?;
let dec_list = decrypt_data(&enc_list, &key);
opt_dump_dec_hdr(app_ctx, &dec_list, "module_list")?;
let mut list_reader = Cursor::new(dec_list);
list_reader.seek(SeekFrom::Start(header_size))?;
let mut hdr_reader = Cursor::new(dec_header);
let mut modules: Vec<ModuleEntry> = Vec::new();
for i in 0..100 {
let mut entry: ModuleEntry = hdr_reader.read_le()?;
let entry: ModuleEntry = list_reader.read_le()?;
if !entry.is_valid() {break};
println!("Module {} - Name: {}, Version: {}, Platform: {}, ID: {}, Offset: {}, Size: {}",
i + 1, entry.name(), entry.version(), entry.platform(), entry.id(), entry.offset, entry.size);
println!("Module {} - Name: {}, Version: {}, Model ID: {}, ID: {}, Offset: {}, Size: {}",
i + 1, entry.name(), entry.version(), entry.model_id(), entry.id(), entry.offset, entry.size);
if modules.iter().any(|m| m.offset == entry.offset ){
println!("- Duplicate module, skipping!");
continue
}
//prevent collision of modules with the same name
if modules.iter().any(|m| m.name() == entry.name() ){
entry.name_bytes = format!("{}({})", entry.name(), i + 1).as_bytes().to_vec();
}
modules.push(entry);
}
@@ -145,10 +149,14 @@ fn extract_file(app_ctx: &AppContext, file_reader: &mut Cursor<Vec<u8>>, offset:
println!("\n({}/{}) - {}, Offset: {}, Size: {}, Checksum: {:#010x}",
mod_i, modules.len(), module.name(), module.offset, module.size, module.data_checksum);
let output_path = Path::new(&output_folder).join(format!("{}.bin", module.name()));
let rel_offset: u64 = offset + module.offset as u64;
file_reader.seek(SeekFrom::Start(rel_offset))?;
//if there is multiple modules with the same name, add the module ID to the outptut file to prevent collision
let output_path = if modules.iter().filter(|m| m.name() == module.name()).nth(1).is_some() {
Path::new(&output_folder).join(format!("{}_{}.bin", mod_i, module.name()))
} else {
Path::new(&output_folder).join(format!("{}.bin", module.name()))
};
file_reader.seek(SeekFrom::Start(module.offset as u64))?;
if module.name() == "MAIN" {
println!("- Extracting MAIN...");
@@ -228,38 +236,43 @@ fn extract_main(file_reader: &mut Cursor<Vec<u8>>, key: [u8; 8], output_path: &P
fn decompress_data(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut data_reader = Cursor::new(data);
let header: CompressedFileHeader = data_reader.read_le()?;
println!("Compressed size: {}, Decompressed size: {}, Compression type: {}({})",
header.src_size, header.dest_size, header.compression_type, header.compression_type_str());
let compression_type = CompressionType::from(header.compression_type);
println!("Compressed size: {}, Decompressed size: {}, Compression type: {:?}({})",
header.src_size, header.dest_size, compression_type, header.compression_type);
let compressed_data = common::read_exact(&mut data_reader, header.src_size as usize)?;
let decompressed_data;
let mut decompressed_data;
if header.compression_type == 1 { //gzip + optionally lzss
if compression_type == CompressionType::Gzip {
println!("- Decompressing GZIP...");
let (decompressed_gzip, gzip_filename) = decompress_gzip_get_filename(&compressed_data)?;
if let Some(gzip_filename) = gzip_filename {
println!("- GZIP filename: {}", gzip_filename);
}
// the decompressed data can have another header
if decompressed_gzip.starts_with(COMPRESSED_FILE_MAGIC) {
decompressed_data = decompress_data(&decompressed_gzip)?;
} else {
decompressed_data = decompressed_gzip;
}
} else if header.compression_type == 2 { //only lzss
}
decompressed_data = decompressed_gzip;
} else if compression_type == CompressionType::Lzss {
println!("- Decompressing LZSS...");
decompressed_data = decompress_lzss(&compressed_data);
if decompressed_data.len() != header.dest_size as usize {
return Err("Decompressed size does not match size in header, decompression failed!".into());
}
} else if header.compression_type == 0 { //no compression. havent encountered one yet
} else if compression_type == CompressionType::None {
decompressed_data = compressed_data;
//GzipAndLzss is not used in this context.
} else {
println!("- Unknown compression method!");
decompressed_data = compressed_data;
}
// the decompressed data can have another header
if decompressed_data.starts_with(COMPRESSED_FILE_MAGIC) {
decompressed_data = decompress_data(&decompressed_data)?;
}
Ok(decompressed_data)
}
@@ -275,12 +288,16 @@ fn extract_drv(mut data: Vec<u8>, key: &[u8; 8]) -> Result<Vec<u8>, Box<dyn std:
let last_decrypted = decrypt_data(&data[data_size as usize - decrypt_size - 48..data_size - 48], &key);
data[data_size as usize - decrypt_size - 48..data_size - 48].copy_from_slice(&last_decrypted);
let mut reader = Cursor::new(&data);
let header: DriveHeader = reader.read_le()?;
println!("- DRIVE info:\n-- Manufacturer ID: {}\n-- Model: {}\n-- Version: {}", header.manufacturer(), header.model(), header.version());
//can be compressed
if data[header_size..].starts_with(COMPRESSED_FILE_MAGIC) {
let decompressed = decompress_data(&data[header_size..])?;
data.truncate(header_size);
data.extend_from_slice(&decompressed);
}
let out_data = if data[header_size..].starts_with(COMPRESSED_FILE_MAGIC) {
decompress_data(&data[header_size..])?
} else {
data[header_size..].to_vec()
};
Ok(data)
Ok(out_data)
}