msg_tool\scripts\musica\archive/
paz.rs

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    /// PAZ file signature
35    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                // also insert full title if there are multiple aliases
71                if alias_count > 1 {
72                    table.insert(title.to_string(), game.to_string());
73                }
74            }
75        }
76        table
77    };
78}
79
80/// Get the supported game titles for PAZ archives.
81pub fn get_supported_games() -> Vec<&'static str> {
82    PAZ_SCHEMA.keys().map(|s| s.as_str()).collect()
83}
84
85/// Get the supported game titles for PAZ archives with their full titles.
86pub 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            // Each PAZ entry at least needs 0x18 bytes
362            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)?; // Placeholder for index size
688        {
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}