From ed2fd5776a23214fc40c5e49799ab115de79ca65 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Sun, 19 Oct 2025 22:12:21 +0200 Subject: [PATCH] Add PUP extractor (PS4) --- src/formats.rs | 2 + src/formats/pup.rs | 182 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 4 + 3 files changed, 188 insertions(+) create mode 100644 src/formats/pup.rs diff --git a/src/formats.rs b/src/formats.rs index 42b04da..2ae6cc6 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -6,6 +6,8 @@ pub mod sddl_sec; pub mod novatek; pub mod ruf; +pub mod pup; + pub mod msd; pub mod msd10; pub mod msd11; diff --git a/src/formats/pup.rs b/src/formats/pup.rs new file mode 100644 index 0000000..f035c27 --- /dev/null +++ b/src/formats/pup.rs @@ -0,0 +1,182 @@ +use std::path::{Path}; +use std::fs::{self, File, OpenOptions}; +use std::io; +use binrw::{BinRead, BinReaderExt}; +use flate2::read::ZlibDecoder; +use std::io::{Write, Read, Seek, SeekFrom}; + +use crate::common; + +#[derive(BinRead)] +struct Header { + #[br(count = 4)] _magic_bytes: Vec, + _unk1: u32, + _unk2: u16, + _flags: u8, + _unk3: u8, + _header_size: u16, + _hash_size: u16, + file_size: u64, + entry_count: u16, + _hash_count: u16, + _unk4: u32, +} + +#[derive(BinRead, Clone)] +struct Entry { + flags: u32, + _unk1: u32, + offset: u64, + compressed_size: u64, + uncompressed_size: u64, +} +impl Entry { + fn id(&self) -> u32 { + self.flags >> 20 + } + fn is_compressed(&self) -> bool { + (self.flags & 8) != 0 + } + fn is_blocked(&self) -> bool { + (self.flags & 0x800) != 0 + } + fn is_block_table(&self) -> bool { + (self.flags & 1) != 0 + } +} + +#[derive(BinRead, Clone)] +struct BlockEntry { + offset: u32, + size: u32, +} + +pub fn is_pup_file(file: &File) -> bool { + let header = common::read_file(&file, 0, 4).expect("Failed to read from file."); + if header == b"\x4F\x15\x3D\x1D" { + true + } else { + false + } +} + +fn decompress_zlib(data: &[u8]) -> io::Result> { + let mut decoder = ZlibDecoder::new(data); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed)?; + + Ok(decompressed) +} + +pub fn extract_pup(mut file: &File, output_folder: &str) -> Result<(), Box> { + let header: Header = file.read_le()?; + + println!("\nFile info:\nFile size: {}\nEntry count: {}", + header.file_size, header.entry_count); + + let mut entries: Vec = Vec::new(); + let mut block_tables: Vec = Vec::new(); + + for _i in 0..header.entry_count { + let entry: Entry = file.read_le()?; + if entry.is_block_table() { + block_tables.push(entry.clone()) + } + entries.push(entry); + } + + let mut e_i = 0; + for entry in &entries { + println!("\n{}/{}: ID: {} Offset: {}, Compressed Size: {}, Uncompressed Size: {}\nCompressed: {}, Blocked: {}, Block table: {}", + e_i + 1, entries.len(), entry.id(), entry.offset, entry.compressed_size, entry.uncompressed_size, entry.is_compressed(), entry.is_blocked(), entry.is_block_table()); + + if !entry.is_block_table () { + if entry.is_blocked() && entry.is_compressed() { + let block_size = 2_u32.pow(((entry.flags & 0xF000) >> 12) + 12); + let block_count = (block_size + entry.uncompressed_size as u32 - 1) / block_size; + let mut my_block_table: Option = None; + println!("Block size: {}, Block count: {}", block_size, block_count); + + for block_table in &block_tables { + if block_table.id() == e_i { + my_block_table = Some(block_table.clone()); + println!("Found block table: Offset: {}, Size: {}", block_table.offset, block_table.compressed_size); + break + } + } + if my_block_table.is_none() { + println!("Failed to find block table!"); + continue + } + + let initial_offset = my_block_table.as_ref().unwrap().offset + (32 * block_count as u64) + (8 * block_count as u64); + + file.seek(SeekFrom::Start(my_block_table.as_ref().unwrap().offset + 32 * block_count as u64))?; + + for i in 0..block_count { + let block: BlockEntry = file.read_le()?; + let current_pos = file.stream_position()?; + + let padding = block.size & 0xF; + let data_size = block.size - padding; + let compressed = if data_size == block_size {false} else {true}; + println!("Block {}/{}: Offset: {}, Data Size: {}, Padding: {}, Compressed: {}", i + 1, block_count, block.offset, data_size, padding, compressed); + + file.seek(std::io::SeekFrom::Start(initial_offset + block.offset as u64))?; + let out_data; + let data = common::read_exact(&mut file, data_size as usize)?; + if compressed { + println!("- Decompressing..."); + out_data = decompress_zlib(&data)?; + } else { + out_data = data; + } + + let output_path = Path::new(&output_folder).join(format!("{}.bin", entry.id())); + + fs::create_dir_all(&output_folder)?; + let mut out_file = OpenOptions::new() + .append(true) + .create(true) + .open(output_path)?; + + out_file.write_all(&out_data)?; + + println!("-- Saved!"); + + file.seek(std::io::SeekFrom::Start(current_pos))?; + } + + } else { + let data = common::read_file(&file, entry.offset, entry.compressed_size as usize)?; + let out_data; + + if entry.is_compressed() { + println!("- Decompressing..."); + out_data = decompress_zlib(&data)?; + } else { + out_data = data; + } + + let output_path = Path::new(&output_folder).join(format!("{}.bin", entry.id())); + + fs::create_dir_all(&output_folder)?; + let mut out_file = OpenOptions::new() + .write(true) + .create(true) + .open(output_path)?; + + out_file.write_all(&out_data)?; + + println!("-- Saved file!"); + } + } else { + println!("- Skipping block table..") + } + e_i += 1; + + } + + println!("\nExtraction finished!"); + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d01041a..9eb8c43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,10 @@ fn main() -> Result<(), Box> { println!("Mstar upgrade file detected!"); formats::mstar::extract_mstar(&file, &output_path)?; } + else if formats::pup::is_pup_file(&file) { + println!("PUP file detected!"); + formats::pup::extract_pup(&file, &output_path)?; + } else if formats::mtk_pkg::is_mtk_pkg_file(&file) { println!("MTK Pkg file detected!"); formats::mtk_pkg::extract_mtk_pkg(&file, &output_path)?;