msg_tool\scripts\artemis\archive/
pf2.rs

1//! Artemis Engine PF2 Archive (pf2)
2use super::detect_script_type;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::struct_pack::*;
7use anyhow::Result;
8use msg_tool_macro::*;
9use std::collections::HashMap;
10use std::io::{Read, Seek, SeekFrom, Write};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug)]
14/// The builder for Artemis PF2 archive scripts.
15pub struct ArtemisPf2Builder {}
16
17impl ArtemisPf2Builder {
18    /// Creates a new instance of `ArtemisPf2Builder`.
19    pub fn new() -> Self {
20        ArtemisPf2Builder {}
21    }
22}
23
24impl ScriptBuilder for ArtemisPf2Builder {
25    fn default_encoding(&self) -> Encoding {
26        Encoding::Cp932
27    }
28
29    fn default_archive_encoding(&self) -> Option<Encoding> {
30        Some(Encoding::Cp932)
31    }
32
33    fn build_script(
34        &self,
35        buf: Vec<u8>,
36        filename: &str,
37        _encoding: Encoding,
38        archive_encoding: Encoding,
39        config: &ExtraConfig,
40        _archive: Option<&Box<dyn Script>>,
41    ) -> Result<Box<dyn Script>> {
42        Ok(Box::new(ArtemisPf2::new(
43            MemReader::new(buf),
44            archive_encoding,
45            config,
46            filename,
47        )?))
48    }
49
50    fn build_script_from_file(
51        &self,
52        filename: &str,
53        _encoding: Encoding,
54        archive_encoding: Encoding,
55        config: &ExtraConfig,
56        _archive: Option<&Box<dyn Script>>,
57    ) -> Result<Box<dyn Script>> {
58        let f = std::fs::File::open(filename)?;
59        let f = std::io::BufReader::new(f);
60        Ok(Box::new(ArtemisPf2::new(
61            f,
62            archive_encoding,
63            config,
64            filename,
65        )?))
66    }
67
68    fn build_script_from_reader(
69        &self,
70        reader: Box<dyn ReadSeek>,
71        filename: &str,
72        _encoding: Encoding,
73        archive_encoding: Encoding,
74        config: &ExtraConfig,
75        _archive: Option<&Box<dyn Script>>,
76    ) -> Result<Box<dyn Script>> {
77        Ok(Box::new(ArtemisPf2::new(
78            reader,
79            archive_encoding,
80            config,
81            filename,
82        )?))
83    }
84
85    fn extensions(&self) -> &'static [&'static str] {
86        gen_artemis_arc_ext!()
87    }
88
89    fn is_archive(&self) -> bool {
90        true
91    }
92
93    fn script_type(&self) -> &'static ScriptType {
94        &ScriptType::ArtemisPf2
95    }
96
97    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
98        if buf_len >= 3 && buf.starts_with(b"pf2") {
99            return Some(20);
100        }
101        None
102    }
103
104    fn create_archive(
105        &self,
106        filename: &str,
107        files: &[&str],
108        encoding: Encoding,
109        _config: &ExtraConfig,
110    ) -> Result<Box<dyn Archive>> {
111        let f = std::fs::File::options()
112            .write(true)
113            .read(true)
114            .create(true)
115            .truncate(true)
116            .open(filename)?;
117        Ok(Box::new(ArtemisPf2Writer::new(f, files, encoding)?))
118    }
119}
120
121#[derive(Debug, Clone, StructPack, StructUnpack)]
122struct Pf2EntryHeader {
123    #[pstring(u32)]
124    name: String,
125    // real path str len (?)
126    _unk1: u32,
127    _unk2: u32,
128    _unk3: u32,
129    offset: u32,
130    size: u32,
131}
132
133#[derive(Debug)]
134/// The Artemis PF2 archive script.
135pub struct ArtemisPf2<T: Read + Seek + std::fmt::Debug> {
136    reader: Arc<Mutex<T>>,
137    entries: Vec<Pf2EntryHeader>,
138    output_ext: Option<String>,
139}
140
141impl<T: Read + Seek + std::fmt::Debug> ArtemisPf2<T> {
142    /// Creates a new Artemis PF2 archive script.
143    ///
144    /// * `reader` - The reader for the archive.
145    /// * `archive_encoding` - The encoding used for the archive.
146    /// * `config` - Extra configuration options.
147    /// * `filename` - The name of the archive file.
148    pub fn new(
149        mut reader: T,
150        archive_encoding: Encoding,
151        _config: &ExtraConfig,
152        filename: &str,
153    ) -> Result<Self> {
154        let mut magic = [0; 2];
155        reader.read_exact(&mut magic)?;
156        if &magic != b"pf" {
157            return Err(anyhow::anyhow!(
158                "Invalid Artemis PF2 archive magic: {:?}",
159                magic
160            ));
161        }
162        let version = reader.read_u8()?;
163        if version != b'2' {
164            return Err(anyhow::anyhow!(
165                "Unsupported Artemis PF2 archive version: {}",
166                version
167            ));
168        }
169        let _index_size = reader.read_u32()?;
170        let _reserved = reader.read_u32()?;
171        let file_count = reader.read_u32()?;
172        let mut entries = Vec::with_capacity(file_count as usize);
173        for _ in 0..file_count {
174            let header = reader.read_struct(false, archive_encoding)?;
175            entries.push(header);
176        }
177        let output_ext = std::path::Path::new(filename)
178            .extension()
179            .filter(|s| *s != "pfs")
180            .map(|s| s.to_string_lossy().to_string());
181        Ok(ArtemisPf2 {
182            reader: Arc::new(Mutex::new(reader)),
183            entries,
184            output_ext,
185        })
186    }
187}
188
189impl<T: Read + Seek + std::fmt::Debug + 'static> Script for ArtemisPf2<T> {
190    fn default_output_script_type(&self) -> OutputScriptType {
191        OutputScriptType::Json
192    }
193
194    fn default_format_type(&self) -> FormatOptions {
195        FormatOptions::None
196    }
197
198    fn is_archive(&self) -> bool {
199        true
200    }
201
202    fn iter_archive_filename<'a>(
203        &'a self,
204    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
205        Ok(Box::new(
206            self.entries.iter().map(|header| Ok(header.name.clone())),
207        ))
208    }
209
210    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
211        Ok(Box::new(
212            self.entries.iter().map(|header| Ok(header.offset as u64)),
213        ))
214    }
215
216    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
217        if index >= self.entries.len() {
218            return Err(anyhow::anyhow!(
219                "Index out of bounds: {} (max: {})",
220                index,
221                self.entries.len()
222            ));
223        }
224        let header = &self.entries[index];
225        let mut entry = Pf2Entry {
226            header: header.clone(),
227            reader: self.reader.clone(),
228            pos: 0,
229            script_type: None,
230        };
231        let mut header_buf = [0; 0x20];
232        let readed = entry.read(&mut header_buf)?;
233        entry.pos = 0;
234        entry.script_type = detect_script_type(&header_buf, readed, &entry.header.name);
235        Ok(Box::new(entry))
236    }
237
238    fn archive_output_ext<'a>(&'a self) -> Option<&'a str> {
239        self.output_ext.as_deref()
240    }
241}
242
243struct Pf2Entry<T: Read + Seek> {
244    header: Pf2EntryHeader,
245    reader: Arc<Mutex<T>>,
246    pos: u64,
247    script_type: Option<ScriptType>,
248}
249
250impl<T: Read + Seek> ArchiveContent for Pf2Entry<T> {
251    fn name(&self) -> &str {
252        &self.header.name
253    }
254
255    fn script_type(&self) -> Option<&ScriptType> {
256        self.script_type.as_ref()
257    }
258}
259
260impl<T: Read + Seek> Read for Pf2Entry<T> {
261    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
262        let mut reader = self.reader.lock().map_err(|e| {
263            std::io::Error::new(
264                std::io::ErrorKind::Other,
265                format!("Failed to lock mutex: {}", e),
266            )
267        })?;
268        reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos))?;
269        let remaining = (self.header.size as u64).saturating_sub(self.pos);
270        if remaining == 0 {
271            return Ok(0);
272        }
273        let bytes_to_read = buf.len().min(remaining as usize);
274        let bytes_read = reader.read(&mut buf[..bytes_to_read])?;
275        self.pos += bytes_read as u64;
276        Ok(bytes_read)
277    }
278}
279
280impl<T: Read + Seek> Seek for Pf2Entry<T> {
281    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
282        let new_pos = match pos {
283            SeekFrom::Start(offset) => offset,
284            SeekFrom::End(offset) => {
285                if offset < 0 {
286                    if (-offset) as u64 > self.header.size as u64 {
287                        return Err(std::io::Error::new(
288                            std::io::ErrorKind::InvalidInput,
289                            "Seek from end exceeds file length",
290                        ));
291                    }
292                    self.header.size as u64 - (-offset) as u64
293                } else {
294                    self.header.size as u64 + offset as u64
295                }
296            }
297            SeekFrom::Current(offset) => {
298                if offset < 0 {
299                    if (-offset) as u64 > self.pos {
300                        return Err(std::io::Error::new(
301                            std::io::ErrorKind::InvalidInput,
302                            "Seek from current exceeds current position",
303                        ));
304                    }
305                    self.pos.saturating_sub((-offset) as u64)
306                } else {
307                    self.pos + offset as u64
308                }
309            }
310        };
311        self.pos = new_pos;
312        Ok(self.pos)
313    }
314
315    fn stream_position(&mut self) -> std::io::Result<u64> {
316        Ok(self.pos)
317    }
318}
319
320/// The Artemis PF2 archive writer.
321pub struct ArtemisPf2Writer<T: Write + Seek + Read> {
322    writer: T,
323    headers: HashMap<String, Pf2EntryHeader>,
324    encoding: Encoding,
325    index_size: u32,
326}
327
328impl<T: Write + Seek + Read> ArtemisPf2Writer<T> {
329    /// Creates a new Artemis PF2 archive writer.
330    ///
331    /// * `writer` - The writer for the archive.
332    /// * `files` - The list of files to include in the archive.
333    /// * `encoding` - The encoding used for the archive.
334    pub fn new(mut writer: T, files: &[&str], encoding: Encoding) -> Result<Self> {
335        writer.write_all(b"pf2")?;
336        writer.write_u32(0)?; // Placeholder for index size
337        writer.write_u32(0)?; // Reserved field at offset 0x07
338        writer.write_u32(files.len() as u32)?;
339        let mut headers = HashMap::new();
340        for file in files {
341            let header = Pf2EntryHeader {
342                name: file.to_string(),
343                _unk1: 0x10,
344                _unk2: 0,
345                _unk3: 0,
346                offset: 0,
347                size: 0,
348            };
349            header.pack(&mut writer, false, encoding)?;
350            headers.insert(file.to_string(), header);
351        }
352        let size = writer.stream_position()?;
353        let index_size = size as u32 - 7;
354        writer.write_u32_at(3, index_size)?;
355        writer.write_u32_at(7, 0)?;
356        Ok(ArtemisPf2Writer {
357            writer,
358            headers,
359            encoding,
360            index_size,
361        })
362    }
363}
364
365impl<T: Write + Seek + Read> Archive for ArtemisPf2Writer<T> {
366    fn new_file<'a>(
367        &'a mut self,
368        name: &str,
369        _size: Option<u64>,
370    ) -> Result<Box<dyn WriteSeek + 'a>> {
371        let entry = self
372            .headers
373            .get_mut(name)
374            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
375        if entry.offset != 0 || entry.size != 0 {
376            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
377        }
378        self.writer.seek(SeekFrom::End(0))?;
379        entry.offset = self.writer.stream_position()? as u32;
380        let file = ArtemisPf2File {
381            header: entry,
382            writer: &mut self.writer,
383            pos: 0,
384        };
385        Ok(Box::new(file))
386    }
387
388    fn write_header(&mut self) -> Result<()> {
389        self.writer.seek(SeekFrom::Start(15))?;
390        let mut files = self.headers.values().collect::<Vec<_>>();
391        files.sort_by_key(|d| d.offset);
392        for file in files.iter() {
393            file.pack(&mut self.writer, false, self.encoding)?;
394        }
395        self.writer.write_u32_at(3, self.index_size)?;
396        self.writer.write_u32_at(7, 0)?;
397        Ok(())
398    }
399}
400
401/// The Artemis PF2 archive file writer.
402pub struct ArtemisPf2File<'a, T: Write + Seek> {
403    header: &'a mut Pf2EntryHeader,
404    writer: &'a mut T,
405    pos: u64,
406}
407
408impl<'a, T: Write + Seek> Write for ArtemisPf2File<'a, T> {
409    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
410        self.writer
411            .seek(SeekFrom::Start(self.header.offset as u64 + self.pos))?;
412        let bytes_written = self.writer.write(buf)?;
413        self.pos += bytes_written as u64;
414        self.header.size = self.header.size.max(self.pos as u32);
415        Ok(bytes_written)
416    }
417
418    fn flush(&mut self) -> std::io::Result<()> {
419        self.writer.flush()
420    }
421}
422
423impl<'a, T: Write + Seek> Seek for ArtemisPf2File<'a, T> {
424    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
425        let new_pos = match pos {
426            SeekFrom::Start(offset) => offset,
427            SeekFrom::End(offset) => {
428                if offset < 0 {
429                    if (-offset) as u64 > self.header.size as u64 {
430                        return Err(std::io::Error::new(
431                            std::io::ErrorKind::InvalidInput,
432                            "Seek from end exceeds file length",
433                        ));
434                    }
435                    self.header.size as u64 - (-offset) as u64
436                } else {
437                    self.header.size as u64 + offset as u64
438                }
439            }
440            SeekFrom::Current(offset) => {
441                if offset < 0 {
442                    if (-offset) as u64 > self.pos {
443                        return Err(std::io::Error::new(
444                            std::io::ErrorKind::InvalidInput,
445                            "Seek from current exceeds current position",
446                        ));
447                    }
448                    self.pos.saturating_sub((-offset) as u64)
449                } else {
450                    self.pos + offset as u64
451                }
452            }
453        };
454        self.pos = new_pos;
455        Ok(self.pos)
456    }
457
458    fn stream_position(&mut self) -> std::io::Result<u64> {
459        Ok(self.pos)
460    }
461}