use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; use crate::utils::blowfish::*; use crate::utils::encoding::*; use crate::utils::rc4::*; use crate::utils::serde_base64bytes::Base64Bytes; use crate::utils::struct_pack::*; use crate::utils::xored_stream::*; use anyhow::Result; use flate2::read::ZlibDecoder; use flate2::write::ZlibEncoder; use msg_tool_macro::{StructPack, StructUnpack}; use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; use std::io::{Read, Seek, SeekFrom, Write}; use std::sync::{Arc, Mutex}; include_flate::flate!(static PAZ_DATA: str from "src/scripts/musica/archive/paz.json" with zstd); #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "PascalCase")] struct ArcKey { index_key: Base64Bytes, data_key: Option, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "PascalCase")] struct Schema { version: u32, arc_keys: HashMap, type_keys: HashMap, /// PAZ file signature signature: u32, xor_key: u8, title: Option, } impl Schema { pub fn get_type_key(&self, entry: &PazEntry, is_audio: bool) -> Option<&str> { if is_audio { return self.type_keys.get("ogg").map(|s| s.as_str()); } let mut name = std::path::Path::new(&entry.name) .extension()? .to_string_lossy() .to_lowercase(); if name == "mpg" || name == "mpeg" { name = "avi".to_string(); } self.type_keys.get(&name).map(|s| s.as_str()) } } lazy_static::lazy_static! { static ref PAZ_SCHEMA: BTreeMap = { serde_json::from_str(&PAZ_DATA).expect("Failed to parse paz.json") }; static ref ALIAS_TABLE: HashMap = { let mut table = HashMap::new(); for (game, fulltitle) in get_supported_games_with_title() { if let Some(title) = fulltitle { let mut alias_count = 0usize; for part in title.split("|") { let alias = part.trim(); table.insert(alias.to_string(), game.to_string()); alias_count += 1; } // also insert full title if there are multiple aliases if alias_count > 1 { table.insert(title.to_string(), game.to_string()); } } } table }; } /// Get the supported game titles for PAZ archives. pub fn get_supported_games() -> Vec<&'static str> { PAZ_SCHEMA.keys().map(|s| s.as_str()).collect() } /// Get the supported game titles for PAZ archives with their full titles. pub fn get_supported_games_with_title() -> Vec<(&'static str, Option<&'static str>)> { PAZ_SCHEMA .iter() .map(|(k, v)| (k.as_str(), v.title.as_deref())) .collect() } fn query_paz_schema(game: &str) -> Option<&'static Schema> { PAZ_SCHEMA.get(game).or_else(|| { ALIAS_TABLE .get(game) .and_then(|real_game| PAZ_SCHEMA.get(real_game)) }) } fn query_paz_schema_by_signature(signature: u32) -> Option<(&'static str, &'static Schema)> { for (game, schema) in PAZ_SCHEMA.iter() { if schema.signature == signature { return Some((game.as_str(), schema)); } } None } #[derive(Debug)] pub struct PazArcBuilder {} impl PazArcBuilder { pub fn new() -> Self { PazArcBuilder {} } } impl ScriptBuilder for PazArcBuilder { fn default_encoding(&self) -> Encoding { Encoding::Cp932 } fn default_archive_encoding(&self) -> Option { Some(Encoding::Cp932) } fn build_script( &self, buf: Vec, filename: &str, _encoding: Encoding, archive_encoding: Encoding, config: &ExtraConfig, _archive: Option<&Box>, ) -> Result> { Ok(Box::new(PazArc::new( MemReader::new(buf), filename, archive_encoding, config, )?)) } fn build_script_from_file( &self, filename: &str, _encoding: Encoding, archive_encoding: Encoding, config: &ExtraConfig, _archive: Option<&Box>, ) -> Result> { let f = std::fs::File::open(filename)?; let f = std::io::BufReader::new(f); Ok(Box::new(PazArc::new( f, filename, archive_encoding, config, )?)) } fn build_script_from_reader( &self, reader: Box, filename: &str, _encoding: Encoding, archive_encoding: Encoding, config: &ExtraConfig, _archive: Option<&Box>, ) -> Result> { Ok(Box::new(PazArc::new( reader, filename, archive_encoding, config, )?)) } fn extensions(&self) -> &'static [&'static str] { &["paz"] } fn is_archive(&self) -> bool { true } fn script_type(&self) -> &'static ScriptType { &ScriptType::MusicaPaz } fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { if buf_len >= 4 { let sign = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); if let Some(_) = query_paz_schema_by_signature(sign) { return Some(10); } } None } fn create_archive( &self, filename: &str, files: &[&str], encoding: Encoding, config: &ExtraConfig, ) -> Result> { let file = std::fs::File::create(filename)?; let file = std::io::BufWriter::new(file); Ok(Box::new(PazArcWriter::new( file, files, encoding, filename, config, )?)) } } #[derive(Debug, StructPack, StructUnpack, Clone)] struct PazEntry { #[cstring] name: String, offset: u64, unpacked_size: u32, size: u32, aligned_size: u32, flags: u32, } impl PazEntry { pub fn is_compressed(&self) -> bool { (self.flags & 0x1) != 0 } pub fn set_is_compressed(&mut self, compressed: bool) { if compressed { self.flags |= 0x1; } else { self.flags &= !0x1; } } } #[derive(Debug)] pub struct PazArc { stream: Arc>, schema: Schema, arc_key: ArcKey, entries: Vec, archive_encoding: Encoding, xor_key: u8, is_audio: bool, mov_key: Option>, } const AUDIO_PAZ_NAMES: &[&str] = &["bgm", "se", "voice", "pmbgm", "pmse", "pmvoice"]; impl PazArc { pub fn new( reader: T, filename: &str, archive_encoding: Encoding, config: &ExtraConfig, ) -> Result { let mut stream = MultipleReadStream::new(); stream.add_stream(reader)?; for suffix in b'A'..=b'Z' { let arc_filename = format!("{}{}", filename, suffix as char); if let Ok(f) = std::fs::File::open(&arc_filename) { let f = std::io::BufReader::new(f); stream.add_stream_boxed(Box::new(f))?; } else { break; } } let schema = if let Some(title) = &config.musica_game_title { let schema = query_paz_schema(title).ok_or_else(|| { anyhow::anyhow!("Unsupported game title '{}' for PAZ archive", title) })?; let sig = stream.read_u32()?; if schema.signature != 0 && schema.signature != sig { let extra_title = if let Some(title) = &schema.title { format!(" ('{}')", title) } else { "".to_string() }; eprintln!( "Warning: PAZ signature {:08X} does not match expected signature {:08X} for game '{}'{}", sig, schema.signature, title, extra_title ); crate::COUNTER.inc_warning(); } schema } else { let sig = stream.read_u32()?; let (game, schema) = query_paz_schema_by_signature(sig).ok_or_else(|| { anyhow::anyhow!( "Unknown PAZ signature {:08X}. Use --musica-game-title to specify game title.", sig ) })?; eprintln!("Detected PAZ archive for game '{}'", game); schema }; let arc_name = std::path::Path::new(filename) .file_stem() .and_then(|s| s.to_str()) .ok_or_else(|| anyhow::anyhow!("Invalid filename"))? .to_lowercase(); let is_audio = AUDIO_PAZ_NAMES.contains(&arc_name.as_str()); let is_video = arc_name == "mov"; let arc_key = schema.arc_keys.get(&arc_name).ok_or_else(|| { anyhow::anyhow!( "No ARC key found for archive name '{}' in game schema", arc_name ) })?; let mut start_offset = if schema.version > 0 { 0x20 } else { 0 }; stream.seek(SeekFrom::Start(start_offset))?; let mut index_size = stream.read_u32()?; start_offset += 4; let xor_key = if let Some(xor_key) = config.musica_xor_key { xor_key } else if schema.xor_key != 0 { schema.xor_key } else { let xor = (index_size >> 24) as u8; eprintln!("Detected xor key from index size: {}", xor); xor }; if xor_key != 0 { let t = xor_key as u32; index_size ^= t << 24 | t << 16 | t << 8 | t; } if index_size & 7 != 0 { return Err(anyhow::anyhow!("Invalid PAZ index size")); } let mut mov_key = None; let entries = { let blowfish: Blowfish = Blowfish::new(&arc_key.index_key)?; let mut index_stream: Box = Box::new(StreamRegion::new( &mut stream, start_offset, start_offset + index_size as u64, )?); if xor_key != 0 { index_stream = Box::new(XoredStream::new(index_stream, xor_key)); } let mut index_stream = BlowfishDecryptor::new(blowfish.clone(), index_stream); let count = index_stream.read_u32()?; if is_video { let mut key = index_stream.read_exact_vec(0x100)?; if schema.version < 1 { let mut nkey = vec![0u8; 0x100]; for i in 0..0x100 { nkey[key[i] as usize] = i as u8; } key = nkey; } mov_key = Some(key); } let mut entries = Vec::with_capacity(count as usize); for _ in 0..count { let entry: PazEntry = index_stream.read_struct(false, archive_encoding)?; entries.push(entry); } entries }; Ok(PazArc { stream: Arc::new(Mutex::new(stream)), schema: schema.clone(), arc_key: arc_key.clone(), entries, archive_encoding, xor_key, is_audio, mov_key, }) } } impl Script for PazArc { fn default_output_script_type(&self) -> OutputScriptType { OutputScriptType::Json } fn default_format_type(&self) -> FormatOptions { FormatOptions::None } fn is_archive(&self) -> bool { true } fn iter_archive_filename<'a>( &'a self, ) -> Result> + 'a>> { Ok(Box::new( self.entries.iter().map(|entry| Ok(entry.name.clone())), )) } fn iter_archive_offset<'a>(&'a self) -> Result> + 'a>> { Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset)))) } fn open_file<'a>(&'a self, index: usize) -> Result> { if index >= self.entries.len() { return Err(anyhow::anyhow!("Index out of bounds")); } let entry = self.entries[index].clone(); let stream = XoredStream::new( StreamRegion::new( MutexWrapper::new(self.stream.clone(), entry.offset), entry.offset, entry.offset + entry.aligned_size as u64, )?, self.xor_key, ); if let Some(data_key) = &self.arc_key.data_key { let blowfish: Blowfish = Blowfish::new(&data_key.bytes)?; let stream = StreamRegion::new( BlowfishDecryptor::new(blowfish, stream), 0, entry.size as u64, )?; if self.schema.version > 0 && !entry.is_compressed() { if let Some(type_key) = self.schema.get_type_key(&entry, self.is_audio) { let key = format!( "{} {:08X} {}", entry.name.to_ascii_lowercase(), entry.unpacked_size, type_key ); let key = encode_string(self.archive_encoding, &key, false)?; let mut rc4 = Rc4::new(&key); if self.schema.version >= 2 { let crc = crc32fast::hash(&key); let skip = ((crc >> 12) as i32) & 0xFF; rc4.skip_bytes(skip as usize); } let stream = Rc4Stream::new(stream, rc4); return Ok(Box::new(PazFileEntry::new(entry, stream))); } } if entry.is_compressed() { let stream = ZlibDecoder::new(stream); return Ok(Box::new(PazFileEntry::new(entry, stream))); } return Ok(Box::new(PazFileEntry::new(entry, stream))); } else if let Some(mov_key) = &self.mov_key { if self.schema.version < 1 { let stream = TableEncryptedStream::new(stream, mov_key.clone())?; if entry.is_compressed() { let stream = ZlibDecoder::new(stream); return Ok(Box::new(PazFileEntry::new(entry, stream))); } return Ok(Box::new(PazFileEntry::new(entry, stream))); } let type_key = self .schema .get_type_key(&entry, self.is_audio) .ok_or_else(|| { anyhow::anyhow!("Data decryption key not found for entry '{}'.", entry.name) })?; let key = format!( "{} {:08X} {}", entry.name.to_ascii_lowercase(), entry.unpacked_size, type_key ); let key = encode_string(self.archive_encoding, &key, false)?; let mut rkey = mov_key.clone(); let key_len = key.len(); for i in 0..0x100 { rkey[i] ^= key[i % key_len]; } let mut rc4 = Rc4::new(&rkey); let key_block = rc4.generate_block((entry.size as usize).min(0x10000)); let stream = XoredKeyStream::new(stream, key_block, 0); if entry.is_compressed() { let stream = ZlibDecoder::new(stream); return Ok(Box::new(PazFileEntry::new(entry, stream))); } return Ok(Box::new(PazFileEntry::new(entry, stream))); } Err(anyhow::anyhow!("Data decryption key not found.")) } } #[derive(Debug)] struct PazFileEntry { entry: PazEntry, stream: T, } impl PazFileEntry { pub fn new(entry: PazEntry, stream: T) -> Self { PazFileEntry { entry, stream } } } impl Read for PazFileEntry { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.stream.read(buf) } } impl Seek for PazFileEntry { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.stream.seek(pos) } fn rewind(&mut self) -> std::io::Result<()> { self.stream.rewind() } fn stream_position(&mut self) -> std::io::Result { self.stream.stream_position() } } impl ArchiveContent for PazFileEntry { fn name(&self) -> &str { &self.entry.name } fn script_type(&self) -> Option<&ScriptType> { let ext_name = std::path::Path::new(&self.entry.name) .extension() .and_then(|s| s.to_str()) .unwrap_or("") .to_lowercase(); match ext_name.as_str() { "sc" => Some(&ScriptType::Musica), _ => None, } } } struct TableEncryptedStream { inner: T, table: Vec, } impl TableEncryptedStream { pub fn new(inner: T, table: Vec) -> Result { if table.len() != 256 { return Err(anyhow::anyhow!( "Table length must be 256, got {}", table.len() )); } Ok(TableEncryptedStream { inner, table }) } } impl Read for TableEncryptedStream { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let readed = self.inner.read(buf)?; for i in 0..readed { buf[i] = self.table[buf[i] as usize]; } Ok(readed) } } impl Seek for TableEncryptedStream { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.inner.seek(pos) } fn rewind(&mut self) -> std::io::Result<()> { self.inner.rewind() } fn stream_position(&mut self) -> std::io::Result { self.inner.stream_position() } } impl Write for TableEncryptedStream { fn write(&mut self, buf: &[u8]) -> std::io::Result { let mut encrypted_buf = vec![0u8; buf.len()]; for i in 0..buf.len() { encrypted_buf[i] = self.table[buf[i] as usize]; } self.inner.write(&encrypted_buf) } fn flush(&mut self) -> std::io::Result<()> { self.inner.flush() } } pub struct PazArcWriter { writer: T, headers: HashMap, encoding: Encoding, is_audio: bool, mov_key: Option>, schema: Schema, arc_key: ArcKey, xor_key: u8, compress: bool, compress_level: u32, } impl PazArcWriter { pub fn new( mut writer: T, files: &[&str], encoding: Encoding, filename: &str, config: &ExtraConfig, ) -> Result { let schema = config.musica_game_title.as_ref().ok_or_else(|| { anyhow::anyhow!( "Game title not specified. Use --musica-game-title to specify the game title." ) })?; let schema = query_paz_schema(schema).ok_or_else(|| { anyhow::anyhow!("Unsupported game title '{}' for PAZ archive", schema) })?; let arc_name = std::path::Path::new(filename) .file_stem() .and_then(|s| s.to_str()) .ok_or_else(|| anyhow::anyhow!("Invalid filename"))? .to_lowercase(); let is_audio = AUDIO_PAZ_NAMES.contains(&arc_name.as_str()); let is_video = arc_name == "mov"; let arc_key = schema.arc_keys.get(&arc_name).ok_or_else(|| { anyhow::anyhow!( "No ARC key found for archive name '{}' in game schema", arc_name ) })?; let mov_key = if is_video { let mut key = vec![0u8; 0x100]; for i in 0..0x100 { key[i] = i as u8; } Some(key) } else { None }; let start_offset = if schema.version > 0 { 0x20 } else { 0 }; if start_offset > 0 { if schema.signature != 0 { writer.write_u32(schema.signature)?; } writer.seek(SeekFrom::Start(start_offset))?; } let mut entries = HashMap::new(); for file in files { let entry = PazEntry { name: file.to_string(), offset: 0, unpacked_size: 0, size: 0, aligned_size: 0, flags: 0, }; entries.insert(file.to_string(), entry); } let xor_key = if let Some(xor_key) = config.musica_xor_key { xor_key } else { schema.xor_key }; if xor_key == 0 { eprintln!( "WARN: 0 xor key is used for PAZ archive. Output archive may broken. Use --musica-xor-key to specify a xor key. Xor key can be obtained from existing archive by unpacking it." ); crate::COUNTER.inc_warning(); } writer.write_u32(0)?; // Placeholder for index size { let blowfish: Blowfish = Blowfish::new(&arc_key.index_key)?; let stream = XoredStream::new(&mut writer, xor_key); let mut index_stream = BlowfishEncryptor::new(blowfish, stream); index_stream.write_u32(entries.len() as u32)?; if let Some(mov_data) = &mov_key { index_stream.write_all(mov_data)?; } for entry in entries.values() { index_stream.write_struct(entry, false, encoding)?; } } let index_end = writer.stream_position()?; let index_size = (index_end - start_offset - 4) as u32; if xor_key != 0 { let mut stream = XoredStream::new(&mut writer, xor_key); stream.write_u32_at(start_offset, index_size)?; } else { writer.write_u32_at(start_offset, index_size)?; }; Ok(PazArcWriter { writer, headers: entries, encoding, is_audio, mov_key, schema: schema.clone(), arc_key: arc_key.clone(), xor_key, compress: config.musica_compress, compress_level: config.zlib_compression_level, }) } } impl Archive for PazArcWriter { fn new_file<'a>( &'a mut self, name: &str, _size: Option, ) -> Result> { let entry = self .headers .get_mut(name) .ok_or_else(|| anyhow::anyhow!("File '{}' not found in PAZ archive headers", name))?; if entry.offset != 0 || entry.size != 0 { return Err(anyhow::anyhow!( "File '{}' already exists in PAZ archive", name )); } if let Some(data_key) = &self.arc_key.data_key { let blowfish: Blowfish = Blowfish::new(&data_key.bytes)?; entry.offset = self.writer.stream_position()?; let stream = XoredStream::new(&mut self.writer, self.xor_key); let stream = BlowfishEncryptor::new(blowfish, stream); let mut type_key = None; entry.set_is_compressed(self.compress); if self.schema.version > 0 && !self.compress { if let Some(tkey) = self.schema.get_type_key(&entry, self.is_audio) { type_key = Some(tkey.to_string()); } } let writer = MemDataKeyWriter { inner: Box::new(stream), cache: MemWriter::new(), type_key, entry, encoding: self.encoding, version: self.schema.version, compress: self.compress, compress_level: self.compress_level, compressed_size: 0, }; return Ok(Box::new(writer)); } else if let Some(mov_key) = &self.mov_key { entry.offset = self.writer.stream_position()?; let stream = XoredStream::new(&mut self.writer, self.xor_key); if self.schema.version < 1 { let stream = TableEncryptedStream::new(stream, mov_key.clone())?; let writer = MovDataWriter { inner: Box::new(stream), entry, }; return Ok(Box::new(writer)); } let type_key = self .schema .get_type_key(&entry, self.is_audio) .ok_or_else(|| { anyhow::anyhow!("Data decryption key not found for entry '{}'.", entry.name) })?; let writer = MemMovDataKeyWriter { inner: Box::new(stream), cache: MemWriter::new(), type_key: type_key.to_string(), mov_key: mov_key.clone(), entry, encoding: self.encoding, }; return Ok(Box::new(writer)); } Err(anyhow::anyhow!("Data encryption key not found.")) } fn new_file_non_seek<'a>( &'a mut self, name: &str, size: Option, ) -> Result> { if let Some(data_key) = &self.arc_key.data_key { let size = match size { Some(size) => size, None => { return Ok(Box::new(self.new_file(name, None)?)); } }; let entry = self.headers.get_mut(name).ok_or_else(|| { anyhow::anyhow!("File '{}' not found in PAZ archive headers", name) })?; if entry.offset != 0 || entry.size != 0 { return Err(anyhow::anyhow!( "File '{}' already exists in PAZ archive", name )); } let blowfish: Blowfish = Blowfish::new(&data_key.bytes)?; entry.offset = self.writer.stream_position()?; let stream = XoredStream::new(&mut self.writer, self.xor_key); let stream = BlowfishEncryptor::new(blowfish, stream); if self.schema.version > 0 && !self.compress { if let Some(tkey) = self.schema.get_type_key(&entry, self.is_audio) { let key = format!("{} {:08X} {}", entry.name.to_ascii_lowercase(), size, tkey); let key = encode_string(self.encoding, &key, false)?; let mut rc4 = Rc4::new(&key); if self.schema.version >= 2 { let crc = crc32fast::hash(&key); let skip = ((crc >> 12) as i32) & 0xFF; rc4.skip_bytes(skip as usize); } let writer = Rc4Stream::new(stream, rc4); let writer = DateKeyWriter { inner: Box::new(writer), entry, }; return Ok(Box::new(writer)); } } if self.compress { entry.set_is_compressed(true); let writer = DataKeyComWriter::new(Box::new(stream), entry, self.compress_level); return Ok(Box::new(writer)); } let writer = DateKeyWriter { inner: Box::new(stream), entry, }; return Ok(Box::new(writer)); } else if let Some(mov_key) = &self.mov_key { let size = match size { Some(size) => size, None => { return Ok(Box::new(self.new_file(name, None)?)); } }; let entry = self.headers.get_mut(name).ok_or_else(|| { anyhow::anyhow!("File '{}' not found in PAZ archive headers", name) })?; if entry.offset != 0 || entry.size != 0 { return Err(anyhow::anyhow!( "File '{}' already exists in PAZ archive", name )); } entry.offset = self.writer.stream_position()?; let stream = XoredStream::new(&mut self.writer, self.xor_key); if self.schema.version < 1 { let stream = TableEncryptedStream::new(stream, mov_key.clone())?; let writer = MovDataWriter { inner: Box::new(stream), entry, }; return Ok(Box::new(writer)); } let type_key = self .schema .get_type_key(&entry, self.is_audio) .ok_or_else(|| { anyhow::anyhow!("Data decryption key not found for entry '{}'.", entry.name) })?; let key = format!( "{} {:08X} {}", entry.name.to_ascii_lowercase(), size, type_key ); let key = encode_string(self.encoding, &key, false)?; let mut rkey = mov_key.clone(); let key_len = key.len(); for i in 0..0x100 { rkey[i] ^= key[i % key_len]; } let mut rc4 = Rc4::new(&rkey); let key_block = rc4.generate_block((size as usize).min(0x10000)); let region = StreamRegion::new(stream, entry.offset, entry.offset + size)?; let stream = XoredKeyStream::new(region, key_block, 0); let writer = DateKeyWriter { inner: Box::new(stream), entry, }; return Ok(Box::new(writer)); } Err(anyhow::anyhow!("Data encryption key not found.")) } fn write_header(&mut self) -> Result<()> { let start_offset = if self.schema.version > 0 { 0x24 } else { 4 }; self.writer.seek(SeekFrom::Start(start_offset))?; { let blowfish: Blowfish = Blowfish::new(&self.arc_key.index_key)?; let stream = XoredStream::new(&mut self.writer, self.xor_key); let mut index_stream = BlowfishEncryptor::new(blowfish, stream); index_stream.write_u32(self.headers.len() as u32)?; if let Some(mov_data) = &self.mov_key { index_stream.write_all(mov_data)?; } for entry in self.headers.values() { index_stream.write_struct(entry, false, self.encoding)?; } } Ok(()) } } struct DateKeyWriter<'a> { inner: Box, entry: &'a mut PazEntry, } impl<'a> Write for DateKeyWriter<'a> { fn write(&mut self, buf: &[u8]) -> std::io::Result { let writed = self.inner.write(buf)?; self.entry.size += writed as u32; Ok(writed) } fn flush(&mut self) -> std::io::Result<()> { self.inner.flush() } } impl<'a> Drop for DateKeyWriter<'a> { fn drop(&mut self) { self.entry.unpacked_size = self.entry.size; self.entry.aligned_size = (self.entry.size + 7) & !7; } } struct DataKeyComWriter<'a> { inner: ZlibEncoder>, entry: &'a mut PazEntry, } impl<'a> DataKeyComWriter<'a> { pub fn new(inner: Box, entry: &'a mut PazEntry, level: u32) -> Self { DataKeyComWriter { inner: ZlibEncoder::new(inner, flate2::Compression::new(level)), entry, } } } impl<'a> Write for DataKeyComWriter<'a> { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.inner.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.inner.flush() } } impl<'a> Drop for DataKeyComWriter<'a> { fn drop(&mut self) { if let Err(e) = self.inner.try_finish() { eprintln!( "Error finishing compression for PAZ file entry '{}': {}", self.entry.name, e ); crate::COUNTER.inc_error(); return; } self.entry.size = self.inner.total_out() as u32; self.entry.unpacked_size = self.inner.total_in() as u32; self.entry.aligned_size = (self.entry.size + 7) & !7; } } trait MyWriteSeek: Write + Seek {} impl MyWriteSeek for T {} struct MovDataWriter<'a> { inner: Box, entry: &'a mut PazEntry, } impl<'a> Write for MovDataWriter<'a> { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.inner.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.inner.flush() } } impl<'a> Seek for MovDataWriter<'a> { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.inner.seek(pos) } fn rewind(&mut self) -> std::io::Result<()> { self.inner.rewind() } fn stream_position(&mut self) -> std::io::Result { self.inner.stream_position() } } impl<'a> Drop for MovDataWriter<'a> { fn drop(&mut self) { if let Ok(pos) = self.inner.stream_position() { self.entry.unpacked_size = (pos - self.entry.offset) as u32; self.entry.size = self.entry.unpacked_size; self.entry.aligned_size = self.entry.size; } else { eprintln!( "Error getting stream position for PAZ file entry '{}'", self.entry.name ); crate::COUNTER.inc_error(); } } } struct MemDataKeyWriter<'a> { inner: Box, cache: MemWriter, type_key: Option, entry: &'a mut PazEntry, encoding: Encoding, version: u32, compress: bool, compress_level: u32, compressed_size: u64, } impl<'a> Write for MemDataKeyWriter<'a> { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.cache.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.cache.flush() } } impl<'a> Seek for MemDataKeyWriter<'a> { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.cache.seek(pos) } fn rewind(&mut self) -> std::io::Result<()> { self.cache.rewind() } fn stream_position(&mut self) -> std::io::Result { self.cache.stream_position() } } impl<'a> Drop for MemDataKeyWriter<'a> { fn drop(&mut self) { let data = &self.cache.data; self.entry.unpacked_size = data.len() as u32; self.entry.size = self.entry.unpacked_size; self.entry.aligned_size = (self.entry.size + 7) & !7; { let mut stream = if let Some(tkey) = &self.type_key { let key = format!( "{} {:08X} {}", self.entry.name.to_ascii_lowercase(), self.entry.unpacked_size, tkey ); let key = match encode_string(self.encoding, &key, false) { Ok(key) => key, Err(e) => { eprintln!( "Error encoding key for PAZ file entry '{}': {}", self.entry.name, e ); crate::COUNTER.inc_error(); return; } }; let mut rc4 = Rc4::new(&key); if self.version >= 2 { let crc = crc32fast::hash(&key); let skip = ((crc >> 12) as i32) & 0xFF; rc4.skip_bytes(skip as usize); } Box::new(Rc4Stream::new(&mut self.inner, rc4)) as Box } else if self.compress { let stream = ZlibEncoder::new( TrackStream::new(&mut self.inner, &mut self.compressed_size), flate2::Compression::new(self.compress_level), ); Box::new(stream) as Box } else { Box::new(&mut self.inner) as Box }; if let Err(e) = stream.write_all(&data) { eprintln!("Error writing PAZ file entry '{}': {}", self.entry.name, e); crate::COUNTER.inc_error(); } } if self.compress { self.entry.size = self.compressed_size as u32; self.entry.aligned_size = (self.entry.size + 7) & !7; } } } struct MemMovDataKeyWriter<'a> { inner: Box, cache: MemWriter, type_key: String, entry: &'a mut PazEntry, encoding: Encoding, mov_key: Vec, } impl<'a> Write for MemMovDataKeyWriter<'a> { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.cache.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.cache.flush() } } impl<'a> Seek for MemMovDataKeyWriter<'a> { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.cache.seek(pos) } fn rewind(&mut self) -> std::io::Result<()> { self.cache.rewind() } fn stream_position(&mut self) -> std::io::Result { self.cache.stream_position() } } impl<'a> Drop for MemMovDataKeyWriter<'a> { fn drop(&mut self) { let data = &self.cache.data; self.entry.unpacked_size = data.len() as u32; self.entry.size = self.entry.unpacked_size; self.entry.aligned_size = self.entry.size; let key = format!( "{} {:08X} {}", self.entry.name.to_ascii_lowercase(), self.entry.unpacked_size, self.type_key ); let key = match encode_string(self.encoding, &key, false) { Ok(key) => key, Err(e) => { eprintln!( "Error encoding key for PAZ file entry '{}': {}", self.entry.name, e ); crate::COUNTER.inc_error(); return; } }; let mut rkey = self.mov_key.clone(); let key_len = key.len(); for i in 0..0x100 { rkey[i] ^= key[i % key_len]; } let mut rc4 = Rc4::new(&rkey); let key_block = rc4.generate_block(data.len().min(0x10000)); let region = match StreamRegion::new( &mut self.inner, self.entry.offset, self.entry.offset + self.entry.size as u64, ) { Ok(region) => region, Err(e) => { eprintln!( "Error creating stream region for PAZ file entry '{}': {}", self.entry.name, e ); crate::COUNTER.inc_error(); return; } }; let mut stream = XoredKeyStream::new(region, key_block, 0); if let Err(e) = stream.write_all(&data) { eprintln!("Error writing PAZ file entry '{}': {}", self.entry.name, e); crate::COUNTER.inc_error(); } } } #[test] fn test_deserialize_paz() { for (game, schema) in PAZ_SCHEMA.iter() { println!("Game: {}", game); println!("Version: {}", schema.version); for (arc_name, arc_key) in schema.arc_keys.iter() { println!(" Arc Name: {}", arc_name); println!(" Index Key: {:02X?}", arc_key.index_key.bytes); if let Some(data_key) = &arc_key.data_key { println!(" Data Key: {:02X?}", data_key.bytes); } else { println!(" Data Key: None"); } } for (type_name, type_key) in schema.type_keys.iter() { println!(" Type Name: {}, Type Key: {}", type_name, type_key); } println!("Signature: {:08X}", schema.signature); println!("XOR Key: {:02X}", schema.xor_key); if let Some(title) = &schema.title { println!("Game Title: {}", title); } } }