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>> {
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>> {
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(
164 &self,
165 reader: Box<dyn ReadSeek>,
166 filename: &str,
167 _encoding: Encoding,
168 archive_encoding: Encoding,
169 config: &ExtraConfig,
170 _archive: Option<&Box<dyn Script>>,
171 ) -> Result<Box<dyn Script>> {
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 {
244 stream: Arc<Mutex<MultipleReadStream>>,
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 PazArc {
257 pub fn new<T: ReadSeek + 'static>(
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)?;
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 Script for PazArc {
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 + '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 script_type(&self) -> Option<&ScriptType> {
539 let ext_name = std::path::Path::new(&self.entry.name)
540 .extension()
541 .and_then(|s| s.to_str())
542 .unwrap_or("")
543 .to_lowercase();
544 match ext_name.as_str() {
545 "sc" => Some(&ScriptType::Musica),
546 _ => None,
547 }
548 }
549}
550
551struct TableEncryptedStream<T> {
552 inner: T,
553 table: Vec<u8>,
554}
555
556impl<T> TableEncryptedStream<T> {
557 pub fn new(inner: T, table: Vec<u8>) -> Result<Self> {
558 if table.len() != 256 {
559 return Err(anyhow::anyhow!(
560 "Table length must be 256, got {}",
561 table.len()
562 ));
563 }
564 Ok(TableEncryptedStream { inner, table })
565 }
566}
567
568impl<T: Read> Read for TableEncryptedStream<T> {
569 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
570 let readed = self.inner.read(buf)?;
571 for i in 0..readed {
572 buf[i] = self.table[buf[i] as usize];
573 }
574 Ok(readed)
575 }
576}
577
578impl<T: Seek> Seek for TableEncryptedStream<T> {
579 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
580 self.inner.seek(pos)
581 }
582
583 fn rewind(&mut self) -> std::io::Result<()> {
584 self.inner.rewind()
585 }
586
587 fn stream_position(&mut self) -> std::io::Result<u64> {
588 self.inner.stream_position()
589 }
590}
591
592impl<T: Write> Write for TableEncryptedStream<T> {
593 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
594 let mut encrypted_buf = vec![0u8; buf.len()];
595 for i in 0..buf.len() {
596 encrypted_buf[i] = self.table[buf[i] as usize];
597 }
598 self.inner.write(&encrypted_buf)
599 }
600
601 fn flush(&mut self) -> std::io::Result<()> {
602 self.inner.flush()
603 }
604}
605
606pub struct PazArcWriter<T: Write + Seek> {
607 writer: T,
608 headers: HashMap<String, PazEntry>,
609 encoding: Encoding,
610 is_audio: bool,
611 mov_key: Option<Vec<u8>>,
612 schema: Schema,
613 arc_key: ArcKey,
614 xor_key: u8,
615 compress: bool,
616 compress_level: u32,
617}
618
619impl<T: Write + Seek> PazArcWriter<T> {
620 pub fn new(
621 mut writer: T,
622 files: &[&str],
623 encoding: Encoding,
624 filename: &str,
625 config: &ExtraConfig,
626 ) -> Result<Self> {
627 let schema = config.musica_game_title.as_ref().ok_or_else(|| {
628 anyhow::anyhow!(
629 "Game title not specified. Use --musica-game-title to specify the game title."
630 )
631 })?;
632 let schema = query_paz_schema(schema).ok_or_else(|| {
633 anyhow::anyhow!("Unsupported game title '{}' for PAZ archive", schema)
634 })?;
635 let arc_name = std::path::Path::new(filename)
636 .file_stem()
637 .and_then(|s| s.to_str())
638 .ok_or_else(|| anyhow::anyhow!("Invalid filename"))?
639 .to_lowercase();
640 let is_audio = AUDIO_PAZ_NAMES.contains(&arc_name.as_str());
641 let is_video = arc_name == "mov";
642 let arc_key = schema.arc_keys.get(&arc_name).ok_or_else(|| {
643 anyhow::anyhow!(
644 "No ARC key found for archive name '{}' in game schema",
645 arc_name
646 )
647 })?;
648 let mov_key = if is_video {
649 let mut key = vec![0u8; 0x100];
650 for i in 0..0x100 {
651 key[i] = i as u8;
652 }
653 Some(key)
654 } else {
655 None
656 };
657 let start_offset = if schema.version > 0 { 0x20 } else { 0 };
658 if start_offset > 0 {
659 if schema.signature != 0 {
660 writer.write_u32(schema.signature)?;
661 }
662 writer.seek(SeekFrom::Start(start_offset))?;
663 }
664 let mut entries = HashMap::new();
665 for file in files {
666 let entry = PazEntry {
667 name: file.to_string(),
668 offset: 0,
669 unpacked_size: 0,
670 size: 0,
671 aligned_size: 0,
672 flags: 0,
673 };
674 entries.insert(file.to_string(), entry);
675 }
676 let xor_key = if let Some(xor_key) = config.musica_xor_key {
677 xor_key
678 } else {
679 schema.xor_key
680 };
681 if xor_key == 0 {
682 eprintln!(
683 "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."
684 );
685 crate::COUNTER.inc_warning();
686 }
687 writer.write_u32(0)?; {
689 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&arc_key.index_key)?;
690 let stream = XoredStream::new(&mut writer, xor_key);
691 let mut index_stream = BlowfishEncryptor::new(blowfish, stream);
692 index_stream.write_u32(entries.len() as u32)?;
693 if let Some(mov_data) = &mov_key {
694 index_stream.write_all(mov_data)?;
695 }
696 for entry in entries.values() {
697 index_stream.write_struct(entry, false, encoding)?;
698 }
699 }
700 let index_end = writer.stream_position()?;
701 let index_size = (index_end - start_offset - 4) as u32;
702 if xor_key != 0 {
703 let mut stream = XoredStream::new(&mut writer, xor_key);
704 stream.write_u32_at(start_offset, index_size)?;
705 } else {
706 writer.write_u32_at(start_offset, index_size)?;
707 };
708 Ok(PazArcWriter {
709 writer,
710 headers: entries,
711 encoding,
712 is_audio,
713 mov_key,
714 schema: schema.clone(),
715 arc_key: arc_key.clone(),
716 xor_key,
717 compress: config.musica_compress,
718 compress_level: config.zlib_compression_level,
719 })
720 }
721}
722
723impl<T: Write + Seek> Archive for PazArcWriter<T> {
724 fn new_file<'a>(
725 &'a mut self,
726 name: &str,
727 _size: Option<u64>,
728 ) -> Result<Box<dyn WriteSeek + 'a>> {
729 let entry = self
730 .headers
731 .get_mut(name)
732 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in PAZ archive headers", name))?;
733 if entry.offset != 0 || entry.size != 0 {
734 return Err(anyhow::anyhow!(
735 "File '{}' already exists in PAZ archive",
736 name
737 ));
738 }
739 if let Some(data_key) = &self.arc_key.data_key {
740 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&data_key.bytes)?;
741 entry.offset = self.writer.stream_position()?;
742 let stream = XoredStream::new(&mut self.writer, self.xor_key);
743 let stream = BlowfishEncryptor::new(blowfish, stream);
744 let mut type_key = None;
745 entry.set_is_compressed(self.compress);
746 if self.schema.version > 0 && !self.compress {
747 if let Some(tkey) = self.schema.get_type_key(&entry, self.is_audio) {
748 type_key = Some(tkey.to_string());
749 }
750 }
751 let writer = MemDataKeyWriter {
752 inner: Box::new(stream),
753 cache: MemWriter::new(),
754 type_key,
755 entry,
756 encoding: self.encoding,
757 version: self.schema.version,
758 compress: self.compress,
759 compress_level: self.compress_level,
760 compressed_size: 0,
761 };
762 return Ok(Box::new(writer));
763 } else if let Some(mov_key) = &self.mov_key {
764 entry.offset = self.writer.stream_position()?;
765 let stream = XoredStream::new(&mut self.writer, self.xor_key);
766 if self.schema.version < 1 {
767 let stream = TableEncryptedStream::new(stream, mov_key.clone())?;
768 let writer = MovDataWriter {
769 inner: Box::new(stream),
770 entry,
771 };
772 return Ok(Box::new(writer));
773 }
774 let type_key = self
775 .schema
776 .get_type_key(&entry, self.is_audio)
777 .ok_or_else(|| {
778 anyhow::anyhow!("Data decryption key not found for entry '{}'.", entry.name)
779 })?;
780 let writer = MemMovDataKeyWriter {
781 inner: Box::new(stream),
782 cache: MemWriter::new(),
783 type_key: type_key.to_string(),
784 mov_key: mov_key.clone(),
785 entry,
786 encoding: self.encoding,
787 };
788 return Ok(Box::new(writer));
789 }
790 Err(anyhow::anyhow!("Data encryption key not found."))
791 }
792
793 fn new_file_non_seek<'a>(
794 &'a mut self,
795 name: &str,
796 size: Option<u64>,
797 ) -> Result<Box<dyn Write + 'a>> {
798 if let Some(data_key) = &self.arc_key.data_key {
799 let size = match size {
800 Some(size) => size,
801 None => {
802 return Ok(Box::new(self.new_file(name, None)?));
803 }
804 };
805 let entry = self.headers.get_mut(name).ok_or_else(|| {
806 anyhow::anyhow!("File '{}' not found in PAZ archive headers", name)
807 })?;
808 if entry.offset != 0 || entry.size != 0 {
809 return Err(anyhow::anyhow!(
810 "File '{}' already exists in PAZ archive",
811 name
812 ));
813 }
814 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&data_key.bytes)?;
815 entry.offset = self.writer.stream_position()?;
816 let stream = XoredStream::new(&mut self.writer, self.xor_key);
817 let stream = BlowfishEncryptor::new(blowfish, stream);
818 if self.schema.version > 0 && !self.compress {
819 if let Some(tkey) = self.schema.get_type_key(&entry, self.is_audio) {
820 let key = format!("{} {:08X} {}", entry.name.to_ascii_lowercase(), size, tkey);
821 let key = encode_string(self.encoding, &key, false)?;
822 let mut rc4 = Rc4::new(&key);
823 if self.schema.version >= 2 {
824 let crc = crc32fast::hash(&key);
825 let skip = ((crc >> 12) as i32) & 0xFF;
826 rc4.skip_bytes(skip as usize);
827 }
828 let writer = Rc4Stream::new(stream, rc4);
829 let writer = DateKeyWriter {
830 inner: Box::new(writer),
831 entry,
832 };
833 return Ok(Box::new(writer));
834 }
835 }
836 if self.compress {
837 entry.set_is_compressed(true);
838 let writer = DataKeyComWriter::new(Box::new(stream), entry, self.compress_level);
839 return Ok(Box::new(writer));
840 }
841 let writer = DateKeyWriter {
842 inner: Box::new(stream),
843 entry,
844 };
845 return Ok(Box::new(writer));
846 } else if let Some(mov_key) = &self.mov_key {
847 let size = match size {
848 Some(size) => size,
849 None => {
850 return Ok(Box::new(self.new_file(name, None)?));
851 }
852 };
853 let entry = self.headers.get_mut(name).ok_or_else(|| {
854 anyhow::anyhow!("File '{}' not found in PAZ archive headers", name)
855 })?;
856 if entry.offset != 0 || entry.size != 0 {
857 return Err(anyhow::anyhow!(
858 "File '{}' already exists in PAZ archive",
859 name
860 ));
861 }
862 entry.offset = self.writer.stream_position()?;
863 let stream = XoredStream::new(&mut self.writer, self.xor_key);
864 if self.schema.version < 1 {
865 let stream = TableEncryptedStream::new(stream, mov_key.clone())?;
866 let writer = MovDataWriter {
867 inner: Box::new(stream),
868 entry,
869 };
870 return Ok(Box::new(writer));
871 }
872 let type_key = self
873 .schema
874 .get_type_key(&entry, self.is_audio)
875 .ok_or_else(|| {
876 anyhow::anyhow!("Data decryption key not found for entry '{}'.", entry.name)
877 })?;
878 let key = format!(
879 "{} {:08X} {}",
880 entry.name.to_ascii_lowercase(),
881 size,
882 type_key
883 );
884 let key = encode_string(self.encoding, &key, false)?;
885 let mut rkey = mov_key.clone();
886 let key_len = key.len();
887 for i in 0..0x100 {
888 rkey[i] ^= key[i % key_len];
889 }
890 let mut rc4 = Rc4::new(&rkey);
891 let key_block = rc4.generate_block((size as usize).min(0x10000));
892 let region = StreamRegion::new(stream, entry.offset, entry.offset + size)?;
893 let stream = XoredKeyStream::new(region, key_block, 0);
894 let writer = DateKeyWriter {
895 inner: Box::new(stream),
896 entry,
897 };
898 return Ok(Box::new(writer));
899 }
900 Err(anyhow::anyhow!("Data encryption key not found."))
901 }
902
903 fn write_header(&mut self) -> Result<()> {
904 let start_offset = if self.schema.version > 0 { 0x24 } else { 4 };
905 self.writer.seek(SeekFrom::Start(start_offset))?;
906 {
907 let blowfish: Blowfish<byteorder::LE> = Blowfish::new(&self.arc_key.index_key)?;
908 let stream = XoredStream::new(&mut self.writer, self.xor_key);
909 let mut index_stream = BlowfishEncryptor::new(blowfish, stream);
910 index_stream.write_u32(self.headers.len() as u32)?;
911 if let Some(mov_data) = &self.mov_key {
912 index_stream.write_all(mov_data)?;
913 }
914 for entry in self.headers.values() {
915 index_stream.write_struct(entry, false, self.encoding)?;
916 }
917 }
918 Ok(())
919 }
920}
921
922struct DateKeyWriter<'a> {
923 inner: Box<dyn Write + 'a>,
924 entry: &'a mut PazEntry,
925}
926
927impl<'a> Write for DateKeyWriter<'a> {
928 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
929 let writed = self.inner.write(buf)?;
930 self.entry.size += writed as u32;
931 Ok(writed)
932 }
933
934 fn flush(&mut self) -> std::io::Result<()> {
935 self.inner.flush()
936 }
937}
938
939impl<'a> Drop for DateKeyWriter<'a> {
940 fn drop(&mut self) {
941 self.entry.unpacked_size = self.entry.size;
942 self.entry.aligned_size = (self.entry.size + 7) & !7;
943 }
944}
945
946struct DataKeyComWriter<'a> {
947 inner: ZlibEncoder<Box<dyn Write + 'a>>,
948 entry: &'a mut PazEntry,
949}
950
951impl<'a> DataKeyComWriter<'a> {
952 pub fn new(inner: Box<dyn Write + 'a>, entry: &'a mut PazEntry, level: u32) -> Self {
953 DataKeyComWriter {
954 inner: ZlibEncoder::new(inner, flate2::Compression::new(level)),
955 entry,
956 }
957 }
958}
959
960impl<'a> Write for DataKeyComWriter<'a> {
961 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
962 self.inner.write(buf)
963 }
964
965 fn flush(&mut self) -> std::io::Result<()> {
966 self.inner.flush()
967 }
968}
969
970impl<'a> Drop for DataKeyComWriter<'a> {
971 fn drop(&mut self) {
972 if let Err(e) = self.inner.try_finish() {
973 eprintln!(
974 "Error finishing compression for PAZ file entry '{}': {}",
975 self.entry.name, e
976 );
977 crate::COUNTER.inc_error();
978 return;
979 }
980 self.entry.size = self.inner.total_out() as u32;
981 self.entry.unpacked_size = self.inner.total_in() as u32;
982 self.entry.aligned_size = (self.entry.size + 7) & !7;
983 }
984}
985
986trait MyWriteSeek: Write + Seek {}
987impl<T: Write + Seek> MyWriteSeek for T {}
988
989struct MovDataWriter<'a> {
990 inner: Box<dyn MyWriteSeek + 'a>,
991 entry: &'a mut PazEntry,
992}
993
994impl<'a> Write for MovDataWriter<'a> {
995 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
996 self.inner.write(buf)
997 }
998
999 fn flush(&mut self) -> std::io::Result<()> {
1000 self.inner.flush()
1001 }
1002}
1003
1004impl<'a> Seek for MovDataWriter<'a> {
1005 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
1006 self.inner.seek(pos)
1007 }
1008
1009 fn rewind(&mut self) -> std::io::Result<()> {
1010 self.inner.rewind()
1011 }
1012
1013 fn stream_position(&mut self) -> std::io::Result<u64> {
1014 self.inner.stream_position()
1015 }
1016}
1017
1018impl<'a> Drop for MovDataWriter<'a> {
1019 fn drop(&mut self) {
1020 if let Ok(pos) = self.inner.stream_position() {
1021 self.entry.unpacked_size = (pos - self.entry.offset) as u32;
1022 self.entry.size = self.entry.unpacked_size;
1023 self.entry.aligned_size = self.entry.size;
1024 } else {
1025 eprintln!(
1026 "Error getting stream position for PAZ file entry '{}'",
1027 self.entry.name
1028 );
1029 crate::COUNTER.inc_error();
1030 }
1031 }
1032}
1033
1034struct MemDataKeyWriter<'a> {
1035 inner: Box<dyn Write + 'a>,
1036 cache: MemWriter,
1037 type_key: Option<String>,
1038 entry: &'a mut PazEntry,
1039 encoding: Encoding,
1040 version: u32,
1041 compress: bool,
1042 compress_level: u32,
1043 compressed_size: u64,
1044}
1045
1046impl<'a> Write for MemDataKeyWriter<'a> {
1047 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1048 self.cache.write(buf)
1049 }
1050
1051 fn flush(&mut self) -> std::io::Result<()> {
1052 self.cache.flush()
1053 }
1054}
1055
1056impl<'a> Seek for MemDataKeyWriter<'a> {
1057 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
1058 self.cache.seek(pos)
1059 }
1060
1061 fn rewind(&mut self) -> std::io::Result<()> {
1062 self.cache.rewind()
1063 }
1064
1065 fn stream_position(&mut self) -> std::io::Result<u64> {
1066 self.cache.stream_position()
1067 }
1068}
1069
1070impl<'a> Drop for MemDataKeyWriter<'a> {
1071 fn drop(&mut self) {
1072 let data = &self.cache.data;
1073 self.entry.unpacked_size = data.len() as u32;
1074 self.entry.size = self.entry.unpacked_size;
1075 self.entry.aligned_size = (self.entry.size + 7) & !7;
1076 {
1077 let mut stream = if let Some(tkey) = &self.type_key {
1078 let key = format!(
1079 "{} {:08X} {}",
1080 self.entry.name.to_ascii_lowercase(),
1081 self.entry.unpacked_size,
1082 tkey
1083 );
1084 let key = match encode_string(self.encoding, &key, false) {
1085 Ok(key) => key,
1086 Err(e) => {
1087 eprintln!(
1088 "Error encoding key for PAZ file entry '{}': {}",
1089 self.entry.name, e
1090 );
1091 crate::COUNTER.inc_error();
1092 return;
1093 }
1094 };
1095 let mut rc4 = Rc4::new(&key);
1096 if self.version >= 2 {
1097 let crc = crc32fast::hash(&key);
1098 let skip = ((crc >> 12) as i32) & 0xFF;
1099 rc4.skip_bytes(skip as usize);
1100 }
1101 Box::new(Rc4Stream::new(&mut self.inner, rc4)) as Box<dyn Write>
1102 } else if self.compress {
1103 let stream = ZlibEncoder::new(
1104 TrackStream::new(&mut self.inner, &mut self.compressed_size),
1105 flate2::Compression::new(self.compress_level),
1106 );
1107 Box::new(stream) as Box<dyn Write>
1108 } else {
1109 Box::new(&mut self.inner) as Box<dyn Write>
1110 };
1111 if let Err(e) = stream.write_all(&data) {
1112 eprintln!("Error writing PAZ file entry '{}': {}", self.entry.name, e);
1113 crate::COUNTER.inc_error();
1114 }
1115 }
1116 if self.compress {
1117 self.entry.size = self.compressed_size as u32;
1118 self.entry.aligned_size = (self.entry.size + 7) & !7;
1119 }
1120 }
1121}
1122
1123struct MemMovDataKeyWriter<'a> {
1124 inner: Box<dyn MyWriteSeek + 'a>,
1125 cache: MemWriter,
1126 type_key: String,
1127 entry: &'a mut PazEntry,
1128 encoding: Encoding,
1129 mov_key: Vec<u8>,
1130}
1131
1132impl<'a> Write for MemMovDataKeyWriter<'a> {
1133 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1134 self.cache.write(buf)
1135 }
1136
1137 fn flush(&mut self) -> std::io::Result<()> {
1138 self.cache.flush()
1139 }
1140}
1141
1142impl<'a> Seek for MemMovDataKeyWriter<'a> {
1143 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
1144 self.cache.seek(pos)
1145 }
1146
1147 fn rewind(&mut self) -> std::io::Result<()> {
1148 self.cache.rewind()
1149 }
1150
1151 fn stream_position(&mut self) -> std::io::Result<u64> {
1152 self.cache.stream_position()
1153 }
1154}
1155
1156impl<'a> Drop for MemMovDataKeyWriter<'a> {
1157 fn drop(&mut self) {
1158 let data = &self.cache.data;
1159 self.entry.unpacked_size = data.len() as u32;
1160 self.entry.size = self.entry.unpacked_size;
1161 self.entry.aligned_size = self.entry.size;
1162 let key = format!(
1163 "{} {:08X} {}",
1164 self.entry.name.to_ascii_lowercase(),
1165 self.entry.unpacked_size,
1166 self.type_key
1167 );
1168 let key = match encode_string(self.encoding, &key, false) {
1169 Ok(key) => key,
1170 Err(e) => {
1171 eprintln!(
1172 "Error encoding key for PAZ file entry '{}': {}",
1173 self.entry.name, e
1174 );
1175 crate::COUNTER.inc_error();
1176 return;
1177 }
1178 };
1179 let mut rkey = self.mov_key.clone();
1180 let key_len = key.len();
1181 for i in 0..0x100 {
1182 rkey[i] ^= key[i % key_len];
1183 }
1184 let mut rc4 = Rc4::new(&rkey);
1185 let key_block = rc4.generate_block(data.len().min(0x10000));
1186 let region = match StreamRegion::new(
1187 &mut self.inner,
1188 self.entry.offset,
1189 self.entry.offset + self.entry.size as u64,
1190 ) {
1191 Ok(region) => region,
1192 Err(e) => {
1193 eprintln!(
1194 "Error creating stream region for PAZ file entry '{}': {}",
1195 self.entry.name, e
1196 );
1197 crate::COUNTER.inc_error();
1198 return;
1199 }
1200 };
1201 let mut stream = XoredKeyStream::new(region, key_block, 0);
1202 if let Err(e) = stream.write_all(&data) {
1203 eprintln!("Error writing PAZ file entry '{}': {}", self.entry.name, e);
1204 crate::COUNTER.inc_error();
1205 }
1206 }
1207}
1208
1209#[test]
1210fn test_deserialize_paz() {
1211 for (game, schema) in PAZ_SCHEMA.iter() {
1212 println!("Game: {}", game);
1213 println!("Version: {}", schema.version);
1214 for (arc_name, arc_key) in schema.arc_keys.iter() {
1215 println!(" Arc Name: {}", arc_name);
1216 println!(" Index Key: {:02X?}", arc_key.index_key.bytes);
1217 if let Some(data_key) = &arc_key.data_key {
1218 println!(" Data Key: {:02X?}", data_key.bytes);
1219 } else {
1220 println!(" Data Key: None");
1221 }
1222 }
1223 for (type_name, type_key) in schema.type_keys.iter() {
1224 println!(" Type Name: {}, Type Key: {}", type_name, type_key);
1225 }
1226 println!("Signature: {:08X}", schema.signature);
1227 println!("XOR Key: {:02X}", schema.xor_key);
1228 if let Some(title) = &schema.title {
1229 println!("Game Title: {}", title);
1230 }
1231 }
1232}