1use crate::ext::io::*;
2use crate::scripts::base::*;
3use crate::types::*;
4use crate::utils::blowfish::*;
5use crate::utils::encoding::*;
6use crate::utils::rc4::*;
7use crate::utils::serde_base64bytes::Base64Bytes;
8use crate::utils::struct_pack::*;
9use crate::utils::xored_stream::*;
10use anyhow::Result;
11use flate2::read::ZlibDecoder;
12use flate2::write::ZlibEncoder;
13use msg_tool_macro::{StructPack, StructUnpack};
14use serde::Deserialize;
15use std::collections::{BTreeMap, HashMap};
16use std::io::{Read, Seek, SeekFrom, Write};
17use std::sync::{Arc, Mutex};
18
19include_flate::flate!(static PAZ_DATA: str from "src/scripts/musica/archive/paz.json" with zstd);
20
21#[derive(Clone, Debug, Deserialize)]
22#[serde(rename_all = "PascalCase")]
23struct ArcKey {
24 index_key: Base64Bytes,
25 data_key: Option<Base64Bytes>,
26}
27
28#[derive(Clone, Debug, Deserialize)]
29#[serde(rename_all = "PascalCase")]
30struct Schema {
31 version: u32,
32 arc_keys: HashMap<String, ArcKey>,
33 type_keys: HashMap<String, String>,
34 signature: u32,
36 xor_key: u8,
37 title: Option<String>,
38}
39
40impl Schema {
41 pub fn get_type_key(&self, entry: &PazEntry, is_audio: bool) -> Option<&str> {
42 if is_audio {
43 return self.type_keys.get("ogg").map(|s| s.as_str());
44 }
45 let mut name = std::path::Path::new(&entry.name)
46 .extension()?
47 .to_string_lossy()
48 .to_lowercase();
49 if name == "mpg" || name == "mpeg" {
50 name = "avi".to_string();
51 }
52 self.type_keys.get(&name).map(|s| s.as_str())
53 }
54}
55
56lazy_static::lazy_static! {
57 static ref PAZ_SCHEMA: BTreeMap<String, Schema> = {
58 serde_json::from_str(&PAZ_DATA).expect("Failed to parse paz.json")
59 };
60 static ref ALIAS_TABLE: HashMap<String, String> = {
61 let mut table = HashMap::new();
62 for (game, fulltitle) in get_supported_games_with_title() {
63 if let Some(title) = fulltitle {
64 let mut alias_count = 0usize;
65 for part in title.split("|") {
66 let alias = part.trim();
67 table.insert(alias.to_string(), game.to_string());
68 alias_count += 1;
69 }
70 if alias_count > 1 {
72 table.insert(title.to_string(), game.to_string());
73 }
74 }
75 }
76 table
77 };
78}
79
80pub fn get_supported_games() -> Vec<&'static str> {
82 PAZ_SCHEMA.keys().map(|s| s.as_str()).collect()
83}
84
85pub fn get_supported_games_with_title() -> Vec<(&'static str, Option<&'static str>)> {
87 PAZ_SCHEMA
88 .iter()
89 .map(|(k, v)| (k.as_str(), v.title.as_deref()))
90 .collect()
91}
92
93fn query_paz_schema(game: &str) -> Option<&'static Schema> {
94 PAZ_SCHEMA.get(game).or_else(|| {
95 ALIAS_TABLE
96 .get(game)
97 .and_then(|real_game| PAZ_SCHEMA.get(real_game))
98 })
99}
100
101fn query_paz_schema_by_signature(signature: u32) -> Option<(&'static str, &'static Schema)> {
102 for (game, schema) in PAZ_SCHEMA.iter() {
103 if schema.signature == signature {
104 return Some((game.as_str(), schema));
105 }
106 }
107 None
108}
109
110#[derive(Debug)]
111pub struct PazArcBuilder {}
112
113impl PazArcBuilder {
114 pub fn new() -> Self {
115 PazArcBuilder {}
116 }
117}
118
119impl ScriptBuilder for PazArcBuilder {
120 fn default_encoding(&self) -> Encoding {
121 Encoding::Cp932
122 }
123
124 fn default_archive_encoding(&self) -> Option<Encoding> {
125 Some(Encoding::Cp932)
126 }
127
128 fn build_script(
129 &self,
130 buf: Vec<u8>,
131 filename: &str,
132 _encoding: Encoding,
133 archive_encoding: Encoding,
134 config: &ExtraConfig,
135 _archive: Option<&Box<dyn Script>>,
136 ) -> Result<Box<dyn Script + Send + Sync>> {
137 Ok(Box::new(PazArc::new(
138 MemReader::new(buf),
139 filename,
140 archive_encoding,
141 config,
142 )?))
143 }
144
145 fn build_script_from_file(
146 &self,
147 filename: &str,
148 _encoding: Encoding,
149 archive_encoding: Encoding,
150 config: &ExtraConfig,
151 _archive: Option<&Box<dyn Script>>,
152 ) -> Result<Box<dyn Script + Send + Sync>> {
153 let f = std::fs::File::open(filename)?;
154 let f = std::io::BufReader::new(f);
155 Ok(Box::new(PazArc::new(
156 f,
157 filename,
158 archive_encoding,
159 config,
160 )?))
161 }
162
163 fn build_script_from_reader<'a>(
164 &self,
165 reader: Box<dyn ReadSeek + Send + Sync + 'a>,
166 filename: &str,
167 _encoding: Encoding,
168 archive_encoding: Encoding,
169 config: &ExtraConfig,
170 _archive: Option<&Box<dyn Script>>,
171 ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
172 Ok(Box::new(PazArc::new(
173 reader,
174 filename,
175 archive_encoding,
176 config,
177 )?))
178 }
179
180 fn extensions(&self) -> &'static [&'static str] {
181 &["paz"]
182 }
183
184 fn is_archive(&self) -> bool {
185 true
186 }
187
188 fn script_type(&self) -> &'static ScriptType {
189 &ScriptType::MusicaPaz
190 }
191
192 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
193 if buf_len >= 4 {
194 let sign = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
195 if let Some(_) = query_paz_schema_by_signature(sign) {
196 return Some(10);
197 }
198 }
199 None
200 }
201
202 fn create_archive(
203 &self,
204 filename: &str,
205 files: &[&str],
206 encoding: Encoding,
207 config: &ExtraConfig,
208 ) -> Result<Box<dyn Archive>> {
209 let file = std::fs::File::create(filename)?;
210 let file = std::io::BufWriter::new(file);
211 Ok(Box::new(PazArcWriter::new(
212 file, files, encoding, filename, config,
213 )?))
214 }
215}
216
217#[derive(Debug, StructPack, StructUnpack, Clone)]
218struct PazEntry {
219 #[cstring]
220 name: String,
221 offset: u64,
222 unpacked_size: u32,
223 size: u32,
224 aligned_size: u32,
225 flags: u32,
226}
227
228impl PazEntry {
229 pub fn is_compressed(&self) -> bool {
230 (self.flags & 0x1) != 0
231 }
232
233 pub fn set_is_compressed(&mut self, compressed: bool) {
234 if compressed {
235 self.flags |= 0x1;
236 } else {
237 self.flags &= !0x1;
238 }
239 }
240}
241
242#[derive(Debug)]
243pub struct PazArc<'a> {
244 stream: Arc<Mutex<MultipleReadStream<'a>>>,
245 schema: Schema,
246 arc_key: ArcKey,
247 entries: Vec<PazEntry>,
248 archive_encoding: Encoding,
249 xor_key: u8,
250 is_audio: bool,
251 mov_key: Option<Vec<u8>>,
252}
253
254const AUDIO_PAZ_NAMES: &[&str] = &["bgm", "se", "voice", "pmbgm", "pmse", "pmvoice"];
255
256impl<'a> PazArc<'a> {
257 pub fn new<T: ReadSeek + Send + Sync + 'a>(
258 reader: T,
259 filename: &str,
260 archive_encoding: Encoding,
261 config: &ExtraConfig,
262 ) -> Result<Self> {
263 let mut stream = MultipleReadStream::new();
264 stream.add_stream(reader)?;
265 for suffix in b'A'..=b'Z' {
266 let arc_filename = format!("{}{}", filename, suffix as char);
267 if let Ok(f) = std::fs::File::open(&arc_filename) {
268 let f = std::io::BufReader::new(f);
269 stream.add_stream_boxed(Box::new(f))?;
270 } else {
271 break;
272 }
273 }
274 let arc_size = stream.stream_length()?;
275 let schema = if let Some(title) = &config.musica_game_title {
276 let schema = query_paz_schema(title).ok_or_else(|| {
277 anyhow::anyhow!("Unsupported game title '{}' for PAZ archive", title)
278 })?;
279 let sig = stream.read_u32()?;
280 if schema.signature != 0 && schema.signature != sig {
281 let extra_title = if let Some(title) = &schema.title {
282 format!(" ('{}')", title)
283 } else {
284 "".to_string()
285 };
286 eprintln!(
287 "Warning: PAZ signature {:08X} does not match expected signature {:08X} for game '{}'{}",
288 sig, schema.signature, title, extra_title
289 );
290 crate::COUNTER.inc_warning();
291 }
292 schema
293 } else {
294 let sig = stream.read_u32()?;
295 let (game, schema) = query_paz_schema_by_signature(sig).ok_or_else(|| {
296 anyhow::anyhow!(
297 "Unknown PAZ signature {:08X}. Use --musica-game-title to specify game title.",
298 sig
299 )
300 })?;
301 eprintln!("Detected PAZ archive for game '{}'", game);
302 schema
303 };
304 let arc_name = std::path::Path::new(filename)
305 .file_stem()
306 .and_then(|s| s.to_str())
307 .ok_or_else(|| anyhow::anyhow!("Invalid filename"))?
308 .to_lowercase();
309 let is_audio = AUDIO_PAZ_NAMES.contains(&arc_name.as_str());
310 let is_video = arc_name == "mov";
311 let arc_key = schema.arc_keys.get(&arc_name).ok_or_else(|| {
312 anyhow::anyhow!(
313 "No ARC key found for archive name '{}' in game schema",
314 arc_name
315 )
316 })?;
317 let mut start_offset = if schema.version > 0 { 0x20 } else { 0 };
318 stream.seek(SeekFrom::Start(start_offset))?;
319 let mut index_size = stream.read_u32()?;
320 start_offset += 4;
321 let xor_key = if let Some(xor_key) = config.musica_xor_key {
322 xor_key
323 } else if schema.xor_key != 0 {
324 schema.xor_key
325 } else {
326 let xor = (index_size >> 24) as u8;
327 eprintln!("Detected xor key from index size: {}", xor);
328 xor
329 };
330 if xor_key != 0 {
331 let t = xor_key as u32;
332 index_size ^= t << 24 | t << 16 | t << 8 | t;
333 }
334 if index_size & 7 != 0 || index_size as u64 > arc_size - start_offset {
335 return Err(anyhow::anyhow!("Invalid PAZ index size: {}", index_size));
336 }
337 let mut mov_key = None;
338 let entries = {
339 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&arc_key.index_key)?;
340 let mut index_stream: Box<dyn ReadSeek> = Box::new(StreamRegion::new(
341 &mut stream,
342 start_offset,
343 start_offset + index_size as u64,
344 )?);
345 if xor_key != 0 {
346 index_stream = Box::new(XoredStream::new(index_stream, xor_key));
347 }
348 let mut index_stream = BlowfishDecryptor::new(blowfish.clone(), index_stream);
349 let count = index_stream.read_u32()?;
350 if is_video {
351 let mut key = index_stream.read_exact_vec(0x100)?;
352 if schema.version < 1 {
353 let mut nkey = vec![0u8; 0x100];
354 for i in 0..0x100 {
355 nkey[key[i] as usize] = i as u8;
356 }
357 key = nkey;
358 }
359 mov_key = Some(key);
360 }
361 let least_len = match count.checked_mul(0x18) {
363 Some(v) => v,
364 None => {
365 return Err(anyhow::anyhow!("Invalid PAZ entry count: {}", count));
366 }
367 };
368 let other_len = if is_video { 0x104 } else { 4 };
369 if least_len > index_size - other_len {
370 return Err(anyhow::anyhow!("Invalid PAZ entry count: {}", count));
371 }
372 let mut entries = Vec::with_capacity(count as usize);
373 for _ in 0..count {
374 let entry: PazEntry = index_stream.read_struct(false, archive_encoding, &None)?;
375 entries.push(entry);
376 }
377 entries
378 };
379 Ok(PazArc {
380 stream: Arc::new(Mutex::new(stream)),
381 schema: schema.clone(),
382 arc_key: arc_key.clone(),
383 entries,
384 archive_encoding,
385 xor_key,
386 is_audio,
387 mov_key,
388 })
389 }
390}
391
392impl<'b> Script for PazArc<'b> {
393 fn default_output_script_type(&self) -> OutputScriptType {
394 OutputScriptType::Json
395 }
396
397 fn default_format_type(&self) -> FormatOptions {
398 FormatOptions::None
399 }
400
401 fn is_archive(&self) -> bool {
402 true
403 }
404
405 fn iter_archive_filename<'a>(
406 &'a self,
407 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
408 Ok(Box::new(
409 self.entries.iter().map(|entry| Ok(entry.name.clone())),
410 ))
411 }
412
413 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
414 Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
415 }
416
417 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
418 if index >= self.entries.len() {
419 return Err(anyhow::anyhow!("Index out of bounds"));
420 }
421 let entry = self.entries[index].clone();
422 let stream = XoredStream::new(
423 StreamRegion::new(
424 MutexWrapper::new(self.stream.clone(), entry.offset),
425 entry.offset,
426 entry.offset + entry.aligned_size as u64,
427 )?,
428 self.xor_key,
429 );
430 if let Some(data_key) = &self.arc_key.data_key {
431 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&data_key.bytes)?;
432 let stream = StreamRegion::new(
433 BlowfishDecryptor::new(blowfish, stream),
434 0,
435 entry.size as u64,
436 )?;
437 if self.schema.version > 0 && !entry.is_compressed() {
438 if let Some(type_key) = self.schema.get_type_key(&entry, self.is_audio) {
439 let key = format!(
440 "{} {:08X} {}",
441 entry.name.to_ascii_lowercase(),
442 entry.unpacked_size,
443 type_key
444 );
445 let key = encode_string(self.archive_encoding, &key, false)?;
446 let mut rc4 = Rc4::new(&key);
447 if self.schema.version >= 2 {
448 let crc = crc32fast::hash(&key);
449 let skip = ((crc >> 12) as i32) & 0xFF;
450 rc4.skip_bytes(skip as usize);
451 }
452 let stream = Rc4Stream::new(stream, rc4);
453 return Ok(Box::new(PazFileEntry::new(entry, stream)));
454 }
455 }
456 if entry.is_compressed() {
457 let stream = ZlibDecoder::new(stream);
458 return Ok(Box::new(PazFileEntry::new(entry, stream)));
459 }
460 return Ok(Box::new(PazFileEntry::new(entry, stream)));
461 } else if let Some(mov_key) = &self.mov_key {
462 if self.schema.version < 1 {
463 let stream = TableEncryptedStream::new(stream, mov_key.clone())?;
464 if entry.is_compressed() {
465 let stream = ZlibDecoder::new(stream);
466 return Ok(Box::new(PazFileEntry::new(entry, stream)));
467 }
468 return Ok(Box::new(PazFileEntry::new(entry, stream)));
469 }
470 let type_key = self
471 .schema
472 .get_type_key(&entry, self.is_audio)
473 .ok_or_else(|| {
474 anyhow::anyhow!("Data decryption key not found for entry '{}'.", entry.name)
475 })?;
476 let key = format!(
477 "{} {:08X} {}",
478 entry.name.to_ascii_lowercase(),
479 entry.unpacked_size,
480 type_key
481 );
482 let key = encode_string(self.archive_encoding, &key, false)?;
483 let mut rkey = mov_key.clone();
484 let key_len = key.len();
485 for i in 0..0x100 {
486 rkey[i] ^= key[i % key_len];
487 }
488 let mut rc4 = Rc4::new(&rkey);
489 let key_block = rc4.generate_block((entry.size as usize).min(0x10000));
490 let stream = XoredKeyStream::new(stream, key_block, 0);
491 if entry.is_compressed() {
492 let stream = ZlibDecoder::new(stream);
493 return Ok(Box::new(PazFileEntry::new(entry, stream)));
494 }
495 return Ok(Box::new(PazFileEntry::new(entry, stream)));
496 }
497 Err(anyhow::anyhow!("Data decryption key not found."))
498 }
499}
500
501#[derive(Debug)]
502struct PazFileEntry<T: Read> {
503 entry: PazEntry,
504 stream: T,
505}
506
507impl<T: Read> PazFileEntry<T> {
508 pub fn new(entry: PazEntry, stream: T) -> Self {
509 PazFileEntry { entry, stream }
510 }
511}
512
513impl<T: Read> Read for PazFileEntry<T> {
514 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
515 self.stream.read(buf)
516 }
517}
518
519impl<T: Seek + Read> Seek for PazFileEntry<T> {
520 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
521 self.stream.seek(pos)
522 }
523
524 fn rewind(&mut self) -> std::io::Result<()> {
525 self.stream.rewind()
526 }
527
528 fn stream_position(&mut self) -> std::io::Result<u64> {
529 self.stream.stream_position()
530 }
531}
532
533impl<T: Read> ArchiveContent for PazFileEntry<T> {
534 fn name(&self) -> &str {
535 &self.entry.name
536 }
537
538 fn size(&self) -> Option<u64> {
539 Some(self.entry.size as u64)
540 }
541
542 fn script_type(&self) -> Option<&ScriptType> {
543 let ext_name = std::path::Path::new(&self.entry.name)
544 .extension()
545 .and_then(|s| s.to_str())
546 .unwrap_or("")
547 .to_lowercase();
548 match ext_name.as_str() {
549 "sc" => Some(&ScriptType::Musica),
550 _ => None,
551 }
552 }
553}
554
555struct TableEncryptedStream<T> {
556 inner: T,
557 table: Vec<u8>,
558}
559
560impl<T> TableEncryptedStream<T> {
561 pub fn new(inner: T, table: Vec<u8>) -> Result<Self> {
562 if table.len() != 256 {
563 return Err(anyhow::anyhow!(
564 "Table length must be 256, got {}",
565 table.len()
566 ));
567 }
568 Ok(TableEncryptedStream { inner, table })
569 }
570}
571
572impl<T: Read> Read for TableEncryptedStream<T> {
573 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
574 let readed = self.inner.read(buf)?;
575 for i in 0..readed {
576 buf[i] = self.table[buf[i] as usize];
577 }
578 Ok(readed)
579 }
580}
581
582impl<T: Seek> Seek for TableEncryptedStream<T> {
583 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
584 self.inner.seek(pos)
585 }
586
587 fn rewind(&mut self) -> std::io::Result<()> {
588 self.inner.rewind()
589 }
590
591 fn stream_position(&mut self) -> std::io::Result<u64> {
592 self.inner.stream_position()
593 }
594}
595
596impl<T: Write> Write for TableEncryptedStream<T> {
597 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
598 let mut encrypted_buf = vec![0u8; buf.len()];
599 for i in 0..buf.len() {
600 encrypted_buf[i] = self.table[buf[i] as usize];
601 }
602 self.inner.write(&encrypted_buf)
603 }
604
605 fn flush(&mut self) -> std::io::Result<()> {
606 self.inner.flush()
607 }
608}
609
610pub struct PazArcWriter<T: Write + Seek> {
611 writer: T,
612 headers: HashMap<String, PazEntry>,
613 encoding: Encoding,
614 is_audio: bool,
615 mov_key: Option<Vec<u8>>,
616 schema: Schema,
617 arc_key: ArcKey,
618 xor_key: u8,
619 compress: bool,
620 compress_level: u32,
621}
622
623impl<T: Write + Seek> PazArcWriter<T> {
624 pub fn new(
625 mut writer: T,
626 files: &[&str],
627 encoding: Encoding,
628 filename: &str,
629 config: &ExtraConfig,
630 ) -> Result<Self> {
631 let schema = config.musica_game_title.as_ref().ok_or_else(|| {
632 anyhow::anyhow!(
633 "Game title not specified. Use --musica-game-title to specify the game title."
634 )
635 })?;
636 let schema = query_paz_schema(schema).ok_or_else(|| {
637 anyhow::anyhow!("Unsupported game title '{}' for PAZ archive", schema)
638 })?;
639 let arc_name = std::path::Path::new(filename)
640 .file_stem()
641 .and_then(|s| s.to_str())
642 .ok_or_else(|| anyhow::anyhow!("Invalid filename"))?
643 .to_lowercase();
644 let is_audio = AUDIO_PAZ_NAMES.contains(&arc_name.as_str());
645 let is_video = arc_name == "mov";
646 let arc_key = schema.arc_keys.get(&arc_name).ok_or_else(|| {
647 anyhow::anyhow!(
648 "No ARC key found for archive name '{}' in game schema",
649 arc_name
650 )
651 })?;
652 let mov_key = if is_video {
653 let mut key = vec![0u8; 0x100];
654 for i in 0..0x100 {
655 key[i] = i as u8;
656 }
657 Some(key)
658 } else {
659 None
660 };
661 let start_offset = if schema.version > 0 { 0x20 } else { 0 };
662 if start_offset > 0 {
663 if schema.signature != 0 {
664 writer.write_u32(schema.signature)?;
665 }
666 writer.seek(SeekFrom::Start(start_offset))?;
667 }
668 let mut entries = HashMap::new();
669 for file in files {
670 let entry = PazEntry {
671 name: file.to_string(),
672 offset: 0,
673 unpacked_size: 0,
674 size: 0,
675 aligned_size: 0,
676 flags: 0,
677 };
678 entries.insert(file.to_string(), entry);
679 }
680 let xor_key = if let Some(xor_key) = config.musica_xor_key {
681 xor_key
682 } else {
683 schema.xor_key
684 };
685 if xor_key == 0 {
686 eprintln!(
687 "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."
688 );
689 crate::COUNTER.inc_warning();
690 }
691 writer.write_u32(0)?; {
693 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&arc_key.index_key)?;
694 let stream = XoredStream::new(&mut writer, xor_key);
695 let mut index_stream = BlowfishEncryptor::new(blowfish, stream);
696 index_stream.write_u32(entries.len() as u32)?;
697 if let Some(mov_data) = &mov_key {
698 index_stream.write_all(mov_data)?;
699 }
700 for entry in entries.values() {
701 index_stream.write_struct(entry, false, encoding, &None)?;
702 }
703 }
704 let index_end = writer.stream_position()?;
705 let index_size = (index_end - start_offset - 4) as u32;
706 if xor_key != 0 {
707 let mut stream = XoredStream::new(&mut writer, xor_key);
708 stream.write_u32_at(start_offset, index_size)?;
709 } else {
710 writer.write_u32_at(start_offset, index_size)?;
711 };
712 Ok(PazArcWriter {
713 writer,
714 headers: entries,
715 encoding,
716 is_audio,
717 mov_key,
718 schema: schema.clone(),
719 arc_key: arc_key.clone(),
720 xor_key,
721 compress: config.musica_compress,
722 compress_level: config.zlib_compression_level,
723 })
724 }
725}
726
727impl<T: Write + Seek> Archive for PazArcWriter<T> {
728 fn new_file<'a>(
729 &'a mut self,
730 name: &str,
731 _size: Option<u64>,
732 ) -> Result<Box<dyn WriteSeek + 'a>> {
733 let entry = self
734 .headers
735 .get_mut(name)
736 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in PAZ archive headers", name))?;
737 if entry.offset != 0 || entry.size != 0 {
738 return Err(anyhow::anyhow!(
739 "File '{}' already exists in PAZ archive",
740 name
741 ));
742 }
743 if let Some(data_key) = &self.arc_key.data_key {
744 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&data_key.bytes)?;
745 entry.offset = self.writer.stream_position()?;
746 let stream = XoredStream::new(&mut self.writer, self.xor_key);
747 let stream = BlowfishEncryptor::new(blowfish, stream);
748 let mut type_key = None;
749 entry.set_is_compressed(self.compress);
750 if self.schema.version > 0 && !self.compress {
751 if let Some(tkey) = self.schema.get_type_key(&entry, self.is_audio) {
752 type_key = Some(tkey.to_string());
753 }
754 }
755 let writer = MemDataKeyWriter {
756 inner: Box::new(stream),
757 cache: MemWriter::new(),
758 type_key,
759 entry,
760 encoding: self.encoding,
761 version: self.schema.version,
762 compress: self.compress,
763 compress_level: self.compress_level,
764 compressed_size: 0,
765 };
766 return Ok(Box::new(writer));
767 } else if let Some(mov_key) = &self.mov_key {
768 entry.offset = self.writer.stream_position()?;
769 let stream = XoredStream::new(&mut self.writer, self.xor_key);
770 if self.schema.version < 1 {
771 let stream = TableEncryptedStream::new(stream, mov_key.clone())?;
772 let writer = MovDataWriter {
773 inner: Box::new(stream),
774 entry,
775 };
776 return Ok(Box::new(writer));
777 }
778 let type_key = self
779 .schema
780 .get_type_key(&entry, self.is_audio)
781 .ok_or_else(|| {
782 anyhow::anyhow!("Data decryption key not found for entry '{}'.", entry.name)
783 })?;
784 let writer = MemMovDataKeyWriter {
785 inner: Box::new(stream),
786 cache: MemWriter::new(),
787 type_key: type_key.to_string(),
788 mov_key: mov_key.clone(),
789 entry,
790 encoding: self.encoding,
791 };
792 return Ok(Box::new(writer));
793 }
794 Err(anyhow::anyhow!("Data encryption key not found."))
795 }
796
797 fn new_file_non_seek<'a>(
798 &'a mut self,
799 name: &str,
800 size: Option<u64>,
801 ) -> Result<Box<dyn Write + 'a>> {
802 if let Some(data_key) = &self.arc_key.data_key {
803 let size = match size {
804 Some(size) => size,
805 None => {
806 return Ok(Box::new(self.new_file(name, None)?));
807 }
808 };
809 let entry = self.headers.get_mut(name).ok_or_else(|| {
810 anyhow::anyhow!("File '{}' not found in PAZ archive headers", name)
811 })?;
812 if entry.offset != 0 || entry.size != 0 {
813 return Err(anyhow::anyhow!(
814 "File '{}' already exists in PAZ archive",
815 name
816 ));
817 }
818 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&data_key.bytes)?;
819 entry.offset = self.writer.stream_position()?;
820 let stream = XoredStream::new(&mut self.writer, self.xor_key);
821 let stream = BlowfishEncryptor::new(blowfish, stream);
822 if self.schema.version > 0 && !self.compress {
823 if let Some(tkey) = self.schema.get_type_key(&entry, self.is_audio) {
824 let key = format!("{} {:08X} {}", entry.name.to_ascii_lowercase(), size, tkey);
825 let key = encode_string(self.encoding, &key, false)?;
826 let mut rc4 = Rc4::new(&key);
827 if self.schema.version >= 2 {
828 let crc = crc32fast::hash(&key);
829 let skip = ((crc >> 12) as i32) & 0xFF;
830 rc4.skip_bytes(skip as usize);
831 }
832 let writer = Rc4Stream::new(stream, rc4);
833 let writer = DateKeyWriter {
834 inner: Box::new(writer),
835 entry,
836 };
837 return Ok(Box::new(writer));
838 }
839 }
840 if self.compress {
841 entry.set_is_compressed(true);
842 let writer = DataKeyComWriter::new(Box::new(stream), entry, self.compress_level);
843 return Ok(Box::new(writer));
844 }
845 let writer = DateKeyWriter {
846 inner: Box::new(stream),
847 entry,
848 };
849 return Ok(Box::new(writer));
850 } else if let Some(mov_key) = &self.mov_key {
851 let size = match size {
852 Some(size) => size,
853 None => {
854 return Ok(Box::new(self.new_file(name, None)?));
855 }
856 };
857 let entry = self.headers.get_mut(name).ok_or_else(|| {
858 anyhow::anyhow!("File '{}' not found in PAZ archive headers", name)
859 })?;
860 if entry.offset != 0 || entry.size != 0 {
861 return Err(anyhow::anyhow!(
862 "File '{}' already exists in PAZ archive",
863 name
864 ));
865 }
866 entry.offset = self.writer.stream_position()?;
867 let stream = XoredStream::new(&mut self.writer, self.xor_key);
868 if self.schema.version < 1 {
869 let stream = TableEncryptedStream::new(stream, mov_key.clone())?;
870 let writer = MovDataWriter {
871 inner: Box::new(stream),
872 entry,
873 };
874 return Ok(Box::new(writer));
875 }
876 let type_key = self
877 .schema
878 .get_type_key(&entry, self.is_audio)
879 .ok_or_else(|| {
880 anyhow::anyhow!("Data decryption key not found for entry '{}'.", entry.name)
881 })?;
882 let key = format!(
883 "{} {:08X} {}",
884 entry.name.to_ascii_lowercase(),
885 size,
886 type_key
887 );
888 let key = encode_string(self.encoding, &key, false)?;
889 let mut rkey = mov_key.clone();
890 let key_len = key.len();
891 for i in 0..0x100 {
892 rkey[i] ^= key[i % key_len];
893 }
894 let mut rc4 = Rc4::new(&rkey);
895 let key_block = rc4.generate_block((size as usize).min(0x10000));
896 let region = StreamRegion::new(stream, entry.offset, entry.offset + size)?;
897 let stream = XoredKeyStream::new(region, key_block, 0);
898 let writer = DateKeyWriter {
899 inner: Box::new(stream),
900 entry,
901 };
902 return Ok(Box::new(writer));
903 }
904 Err(anyhow::anyhow!("Data encryption key not found."))
905 }
906
907 fn write_header(&mut self) -> Result<()> {
908 let start_offset = if self.schema.version > 0 { 0x24 } else { 4 };
909 self.writer.seek(SeekFrom::Start(start_offset))?;
910 {
911 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&self.arc_key.index_key)?;
912 let stream = XoredStream::new(&mut self.writer, self.xor_key);
913 let mut index_stream = BlowfishEncryptor::new(blowfish, stream);
914 index_stream.write_u32(self.headers.len() as u32)?;
915 if let Some(mov_data) = &self.mov_key {
916 index_stream.write_all(mov_data)?;
917 }
918 for entry in self.headers.values() {
919 index_stream.write_struct(entry, false, self.encoding, &None)?;
920 }
921 }
922 Ok(())
923 }
924}
925
926struct DateKeyWriter<'a> {
927 inner: Box<dyn Write + 'a>,
928 entry: &'a mut PazEntry,
929}
930
931impl<'a> Write for DateKeyWriter<'a> {
932 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
933 let writed = self.inner.write(buf)?;
934 self.entry.size += writed as u32;
935 Ok(writed)
936 }
937
938 fn flush(&mut self) -> std::io::Result<()> {
939 self.inner.flush()
940 }
941}
942
943impl<'a> Drop for DateKeyWriter<'a> {
944 fn drop(&mut self) {
945 self.entry.unpacked_size = self.entry.size;
946 self.entry.aligned_size = (self.entry.size + 7) & !7;
947 }
948}
949
950struct DataKeyComWriter<'a> {
951 inner: ZlibEncoder<Box<dyn Write + 'a>>,
952 entry: &'a mut PazEntry,
953}
954
955impl<'a> DataKeyComWriter<'a> {
956 pub fn new(inner: Box<dyn Write + 'a>, entry: &'a mut PazEntry, level: u32) -> Self {
957 DataKeyComWriter {
958 inner: ZlibEncoder::new(inner, flate2::Compression::new(level)),
959 entry,
960 }
961 }
962}
963
964impl<'a> Write for DataKeyComWriter<'a> {
965 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
966 self.inner.write(buf)
967 }
968
969 fn flush(&mut self) -> std::io::Result<()> {
970 self.inner.flush()
971 }
972}
973
974impl<'a> Drop for DataKeyComWriter<'a> {
975 fn drop(&mut self) {
976 if let Err(e) = self.inner.try_finish() {
977 eprintln!(
978 "Error finishing compression for PAZ file entry '{}': {}",
979 self.entry.name, e
980 );
981 crate::COUNTER.inc_error();
982 return;
983 }
984 self.entry.size = self.inner.total_out() as u32;
985 self.entry.unpacked_size = self.inner.total_in() as u32;
986 self.entry.aligned_size = (self.entry.size + 7) & !7;
987 }
988}
989
990trait MyWriteSeek: Write + Seek {}
991impl<T: Write + Seek> MyWriteSeek for T {}
992
993struct MovDataWriter<'a> {
994 inner: Box<dyn MyWriteSeek + 'a>,
995 entry: &'a mut PazEntry,
996}
997
998impl<'a> Write for MovDataWriter<'a> {
999 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1000 self.inner.write(buf)
1001 }
1002
1003 fn flush(&mut self) -> std::io::Result<()> {
1004 self.inner.flush()
1005 }
1006}
1007
1008impl<'a> Seek for MovDataWriter<'a> {
1009 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
1010 self.inner.seek(pos)
1011 }
1012
1013 fn rewind(&mut self) -> std::io::Result<()> {
1014 self.inner.rewind()
1015 }
1016
1017 fn stream_position(&mut self) -> std::io::Result<u64> {
1018 self.inner.stream_position()
1019 }
1020}
1021
1022impl<'a> Drop for MovDataWriter<'a> {
1023 fn drop(&mut self) {
1024 if let Ok(pos) = self.inner.stream_position() {
1025 self.entry.unpacked_size = (pos - self.entry.offset) as u32;
1026 self.entry.size = self.entry.unpacked_size;
1027 self.entry.aligned_size = self.entry.size;
1028 } else {
1029 eprintln!(
1030 "Error getting stream position for PAZ file entry '{}'",
1031 self.entry.name
1032 );
1033 crate::COUNTER.inc_error();
1034 }
1035 }
1036}
1037
1038struct MemDataKeyWriter<'a> {
1039 inner: Box<dyn Write + 'a>,
1040 cache: MemWriter,
1041 type_key: Option<String>,
1042 entry: &'a mut PazEntry,
1043 encoding: Encoding,
1044 version: u32,
1045 compress: bool,
1046 compress_level: u32,
1047 compressed_size: u64,
1048}
1049
1050impl<'a> Write for MemDataKeyWriter<'a> {
1051 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1052 self.cache.write(buf)
1053 }
1054
1055 fn flush(&mut self) -> std::io::Result<()> {
1056 self.cache.flush()
1057 }
1058}
1059
1060impl<'a> Seek for MemDataKeyWriter<'a> {
1061 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
1062 self.cache.seek(pos)
1063 }
1064
1065 fn rewind(&mut self) -> std::io::Result<()> {
1066 self.cache.rewind()
1067 }
1068
1069 fn stream_position(&mut self) -> std::io::Result<u64> {
1070 self.cache.stream_position()
1071 }
1072}
1073
1074impl<'a> Drop for MemDataKeyWriter<'a> {
1075 fn drop(&mut self) {
1076 let data = &self.cache.data;
1077 self.entry.unpacked_size = data.len() as u32;
1078 self.entry.size = self.entry.unpacked_size;
1079 self.entry.aligned_size = (self.entry.size + 7) & !7;
1080 {
1081 let mut stream = if let Some(tkey) = &self.type_key {
1082 let key = format!(
1083 "{} {:08X} {}",
1084 self.entry.name.to_ascii_lowercase(),
1085 self.entry.unpacked_size,
1086 tkey
1087 );
1088 let key = match encode_string(self.encoding, &key, false) {
1089 Ok(key) => key,
1090 Err(e) => {
1091 eprintln!(
1092 "Error encoding key for PAZ file entry '{}': {}",
1093 self.entry.name, e
1094 );
1095 crate::COUNTER.inc_error();
1096 return;
1097 }
1098 };
1099 let mut rc4 = Rc4::new(&key);
1100 if self.version >= 2 {
1101 let crc = crc32fast::hash(&key);
1102 let skip = ((crc >> 12) as i32) & 0xFF;
1103 rc4.skip_bytes(skip as usize);
1104 }
1105 Box::new(Rc4Stream::new(&mut self.inner, rc4)) as Box<dyn Write>
1106 } else if self.compress {
1107 let stream = ZlibEncoder::new(
1108 TrackStream::new(&mut self.inner, &mut self.compressed_size),
1109 flate2::Compression::new(self.compress_level),
1110 );
1111 Box::new(stream) as Box<dyn Write>
1112 } else {
1113 Box::new(&mut self.inner) as Box<dyn Write>
1114 };
1115 if let Err(e) = stream.write_all(&data) {
1116 eprintln!("Error writing PAZ file entry '{}': {}", self.entry.name, e);
1117 crate::COUNTER.inc_error();
1118 }
1119 }
1120 if self.compress {
1121 self.entry.size = self.compressed_size as u32;
1122 self.entry.aligned_size = (self.entry.size + 7) & !7;
1123 }
1124 }
1125}
1126
1127struct MemMovDataKeyWriter<'a> {
1128 inner: Box<dyn MyWriteSeek + 'a>,
1129 cache: MemWriter,
1130 type_key: String,
1131 entry: &'a mut PazEntry,
1132 encoding: Encoding,
1133 mov_key: Vec<u8>,
1134}
1135
1136impl<'a> Write for MemMovDataKeyWriter<'a> {
1137 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1138 self.cache.write(buf)
1139 }
1140
1141 fn flush(&mut self) -> std::io::Result<()> {
1142 self.cache.flush()
1143 }
1144}
1145
1146impl<'a> Seek for MemMovDataKeyWriter<'a> {
1147 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
1148 self.cache.seek(pos)
1149 }
1150
1151 fn rewind(&mut self) -> std::io::Result<()> {
1152 self.cache.rewind()
1153 }
1154
1155 fn stream_position(&mut self) -> std::io::Result<u64> {
1156 self.cache.stream_position()
1157 }
1158}
1159
1160impl<'a> Drop for MemMovDataKeyWriter<'a> {
1161 fn drop(&mut self) {
1162 let data = &self.cache.data;
1163 self.entry.unpacked_size = data.len() as u32;
1164 self.entry.size = self.entry.unpacked_size;
1165 self.entry.aligned_size = self.entry.size;
1166 let key = format!(
1167 "{} {:08X} {}",
1168 self.entry.name.to_ascii_lowercase(),
1169 self.entry.unpacked_size,
1170 self.type_key
1171 );
1172 let key = match encode_string(self.encoding, &key, false) {
1173 Ok(key) => key,
1174 Err(e) => {
1175 eprintln!(
1176 "Error encoding key for PAZ file entry '{}': {}",
1177 self.entry.name, e
1178 );
1179 crate::COUNTER.inc_error();
1180 return;
1181 }
1182 };
1183 let mut rkey = self.mov_key.clone();
1184 let key_len = key.len();
1185 for i in 0..0x100 {
1186 rkey[i] ^= key[i % key_len];
1187 }
1188 let mut rc4 = Rc4::new(&rkey);
1189 let key_block = rc4.generate_block(data.len().min(0x10000));
1190 let region = match StreamRegion::new(
1191 &mut self.inner,
1192 self.entry.offset,
1193 self.entry.offset + self.entry.size as u64,
1194 ) {
1195 Ok(region) => region,
1196 Err(e) => {
1197 eprintln!(
1198 "Error creating stream region for PAZ file entry '{}': {}",
1199 self.entry.name, e
1200 );
1201 crate::COUNTER.inc_error();
1202 return;
1203 }
1204 };
1205 let mut stream = XoredKeyStream::new(region, key_block, 0);
1206 if let Err(e) = stream.write_all(&data) {
1207 eprintln!("Error writing PAZ file entry '{}': {}", self.entry.name, e);
1208 crate::COUNTER.inc_error();
1209 }
1210 }
1211}
1212
1213#[test]
1214fn test_deserialize_paz() {
1215 for (game, schema) in PAZ_SCHEMA.iter() {
1216 println!("Game: {}", game);
1217 println!("Version: {}", schema.version);
1218 for (arc_name, arc_key) in schema.arc_keys.iter() {
1219 println!(" Arc Name: {}", arc_name);
1220 println!(" Index Key: {:02X?}", arc_key.index_key.bytes);
1221 if let Some(data_key) = &arc_key.data_key {
1222 println!(" Data Key: {:02X?}", data_key.bytes);
1223 } else {
1224 println!(" Data Key: None");
1225 }
1226 }
1227 for (type_name, type_key) in schema.type_keys.iter() {
1228 println!(" Type Name: {}, Type Key: {}", type_name, type_key);
1229 }
1230 println!("Signature: {:08X}", schema.signature);
1231 println!("XOR Key: {:02X}", schema.xor_key);
1232 if let Some(title) = &schema.title {
1233 println!("Game Title: {}", title);
1234 }
1235 }
1236}