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 + Send + Sync>> {
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 + Send + Sync>> {
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<'a>(
69        &self,
70        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
71        filename: &str,
72        _encoding: Encoding,
73        archive_encoding: Encoding,
74        config: &ExtraConfig,
75        _archive: Option<&Box<dyn Script>>,
76    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
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<'a, T: Read + Seek + std::fmt::Debug + Send + Sync + 'a> {
136    reader: Arc<Mutex<T>>,
137    entries: Vec<Pf2EntryHeader>,
138    output_ext: Option<String>,
139    _mark: std::marker::PhantomData<&'a ()>,
140}
141
142impl<'a, T: Read + Seek + std::fmt::Debug + Send + Sync + 'a> ArtemisPf2<'a, T> {
143    /// Creates a new Artemis PF2 archive script.
144    ///
145    /// * `reader` - The reader for the archive.
146    /// * `archive_encoding` - The encoding used for the archive.
147    /// * `config` - Extra configuration options.
148    /// * `filename` - The name of the archive file.
149    pub fn new(
150        mut reader: T,
151        archive_encoding: Encoding,
152        _config: &ExtraConfig,
153        filename: &str,
154    ) -> Result<Self> {
155        let mut magic = [0; 2];
156        reader.read_exact(&mut magic)?;
157        if &magic != b"pf" {
158            return Err(anyhow::anyhow!(
159                "Invalid Artemis PF2 archive magic: {:?}",
160                magic
161            ));
162        }
163        let version = reader.read_u8()?;
164        if version != b'2' {
165            return Err(anyhow::anyhow!(
166                "Unsupported Artemis PF2 archive version: {}",
167                version
168            ));
169        }
170        let _index_size = reader.read_u32()?;
171        let _reserved = reader.read_u32()?;
172        let file_count = reader.read_u32()?;
173        let mut entries = Vec::with_capacity(file_count as usize);
174        for _ in 0..file_count {
175            let header = reader.read_struct(false, archive_encoding, &None)?;
176            entries.push(header);
177        }
178        let output_ext = std::path::Path::new(filename)
179            .extension()
180            .filter(|s| *s != "pfs")
181            .map(|s| s.to_string_lossy().to_string());
182        Ok(ArtemisPf2 {
183            reader: Arc::new(Mutex::new(reader)),
184            entries,
185            output_ext,
186            _mark: std::marker::PhantomData,
187        })
188    }
189}
190
191impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script for ArtemisPf2<'b, T> {
192    fn default_output_script_type(&self) -> OutputScriptType {
193        OutputScriptType::Json
194    }
195
196    fn default_format_type(&self) -> FormatOptions {
197        FormatOptions::None
198    }
199
200    fn is_archive(&self) -> bool {
201        true
202    }
203
204    fn iter_archive_filename<'a>(
205        &'a self,
206    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
207        Ok(Box::new(
208            self.entries.iter().map(|header| Ok(header.name.clone())),
209        ))
210    }
211
212    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
213        Ok(Box::new(
214            self.entries.iter().map(|header| Ok(header.offset as u64)),
215        ))
216    }
217
218    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
219        if index >= self.entries.len() {
220            return Err(anyhow::anyhow!(
221                "Index out of bounds: {} (max: {})",
222                index,
223                self.entries.len()
224            ));
225        }
226        let header = &self.entries[index];
227        let mut entry = Pf2Entry {
228            header: header.clone(),
229            reader: self.reader.clone(),
230            pos: 0,
231            script_type: None,
232        };
233        let mut header_buf = [0; 0x20];
234        let readed = entry.read(&mut header_buf)?;
235        entry.pos = 0;
236        entry.script_type = detect_script_type(&header_buf, readed, &entry.header.name);
237        Ok(Box::new(entry))
238    }
239
240    fn archive_output_ext<'a>(&'a self) -> Option<&'a str> {
241        self.output_ext.as_deref()
242    }
243}
244
245#[derive(Debug)]
246struct Pf2Entry<T: Read + Seek + Send + Sync + std::fmt::Debug> {
247    header: Pf2EntryHeader,
248    reader: Arc<Mutex<T>>,
249    pos: u64,
250    script_type: Option<ScriptType>,
251}
252
253impl<T: Read + Seek + Send + Sync + std::fmt::Debug> ArchiveContent for Pf2Entry<T> {
254    fn name(&self) -> &str {
255        &self.header.name
256    }
257
258    fn size(&self) -> Option<u64> {
259        Some(self.header.size as u64)
260    }
261
262    fn script_type(&self) -> Option<&ScriptType> {
263        self.script_type.as_ref()
264    }
265
266    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
267        Ok(Box::new(self))
268    }
269}
270
271impl<T: Read + Seek + Send + Sync + std::fmt::Debug> Read for Pf2Entry<T> {
272    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
273        let mut reader = self.reader.lock().map_err(|e| {
274            std::io::Error::new(
275                std::io::ErrorKind::Other,
276                format!("Failed to lock mutex: {}", e),
277            )
278        })?;
279        reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos))?;
280        let remaining = (self.header.size as u64).saturating_sub(self.pos);
281        if remaining == 0 {
282            return Ok(0);
283        }
284        let bytes_to_read = buf.len().min(remaining as usize);
285        let bytes_read = reader.read(&mut buf[..bytes_to_read])?;
286        self.pos += bytes_read as u64;
287        Ok(bytes_read)
288    }
289}
290
291impl<T: Read + Seek + Send + Sync + std::fmt::Debug> Seek for Pf2Entry<T> {
292    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
293        let new_pos = match pos {
294            SeekFrom::Start(offset) => offset,
295            SeekFrom::End(offset) => {
296                if offset < 0 {
297                    if (-offset) as u64 > self.header.size as u64 {
298                        return Err(std::io::Error::new(
299                            std::io::ErrorKind::InvalidInput,
300                            "Seek from end exceeds file length",
301                        ));
302                    }
303                    self.header.size as u64 - (-offset) as u64
304                } else {
305                    self.header.size as u64 + offset as u64
306                }
307            }
308            SeekFrom::Current(offset) => {
309                if offset < 0 {
310                    if (-offset) as u64 > self.pos {
311                        return Err(std::io::Error::new(
312                            std::io::ErrorKind::InvalidInput,
313                            "Seek from current exceeds current position",
314                        ));
315                    }
316                    self.pos.saturating_sub((-offset) as u64)
317                } else {
318                    self.pos + offset as u64
319                }
320            }
321        };
322        self.pos = new_pos;
323        Ok(self.pos)
324    }
325
326    fn stream_position(&mut self) -> std::io::Result<u64> {
327        Ok(self.pos)
328    }
329}
330
331/// The Artemis PF2 archive writer.
332pub struct ArtemisPf2Writer<T: Write + Seek + Read> {
333    writer: T,
334    headers: HashMap<String, Pf2EntryHeader>,
335    encoding: Encoding,
336    index_size: u32,
337}
338
339impl<T: Write + Seek + Read> ArtemisPf2Writer<T> {
340    /// Creates a new Artemis PF2 archive writer.
341    ///
342    /// * `writer` - The writer for the archive.
343    /// * `files` - The list of files to include in the archive.
344    /// * `encoding` - The encoding used for the archive.
345    pub fn new(mut writer: T, files: &[&str], encoding: Encoding) -> Result<Self> {
346        writer.write_all(b"pf2")?;
347        writer.write_u32(0)?; // Placeholder for index size
348        writer.write_u32(0)?; // Reserved field at offset 0x07
349        writer.write_u32(files.len() as u32)?;
350        let mut headers = HashMap::new();
351        for file in files {
352            let header = Pf2EntryHeader {
353                name: file.to_string(),
354                _unk1: 0x10,
355                _unk2: 0,
356                _unk3: 0,
357                offset: 0,
358                size: 0,
359            };
360            header.pack(&mut writer, false, encoding, &None)?;
361            headers.insert(file.to_string(), header);
362        }
363        let size = writer.stream_position()?;
364        let index_size = size as u32 - 7;
365        writer.write_u32_at(3, index_size)?;
366        writer.write_u32_at(7, 0)?;
367        Ok(ArtemisPf2Writer {
368            writer,
369            headers,
370            encoding,
371            index_size,
372        })
373    }
374}
375
376impl<T: Write + Seek + Read> Archive for ArtemisPf2Writer<T> {
377    fn new_file<'a>(
378        &'a mut self,
379        name: &str,
380        _size: Option<u64>,
381    ) -> Result<Box<dyn WriteSeek + 'a>> {
382        let entry = self
383            .headers
384            .get_mut(name)
385            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
386        if entry.offset != 0 || entry.size != 0 {
387            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
388        }
389        self.writer.seek(SeekFrom::End(0))?;
390        entry.offset = self.writer.stream_position()? as u32;
391        let file = ArtemisPf2File {
392            header: entry,
393            writer: &mut self.writer,
394            pos: 0,
395        };
396        Ok(Box::new(file))
397    }
398
399    fn write_header(&mut self) -> Result<()> {
400        self.writer.seek(SeekFrom::Start(15))?;
401        let mut files = self.headers.values().collect::<Vec<_>>();
402        files.sort_by_key(|d| d.offset);
403        for file in files.iter() {
404            file.pack(&mut self.writer, false, self.encoding, &None)?;
405        }
406        self.writer.write_u32_at(3, self.index_size)?;
407        self.writer.write_u32_at(7, 0)?;
408        Ok(())
409    }
410}
411
412/// The Artemis PF2 archive file writer.
413pub struct ArtemisPf2File<'a, T: Write + Seek> {
414    header: &'a mut Pf2EntryHeader,
415    writer: &'a mut T,
416    pos: u64,
417}
418
419impl<'a, T: Write + Seek> Write for ArtemisPf2File<'a, T> {
420    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
421        self.writer
422            .seek(SeekFrom::Start(self.header.offset as u64 + self.pos))?;
423        let bytes_written = self.writer.write(buf)?;
424        self.pos += bytes_written as u64;
425        self.header.size = self.header.size.max(self.pos as u32);
426        Ok(bytes_written)
427    }
428
429    fn flush(&mut self) -> std::io::Result<()> {
430        self.writer.flush()
431    }
432}
433
434impl<'a, T: Write + Seek> Seek for ArtemisPf2File<'a, T> {
435    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
436        let new_pos = match pos {
437            SeekFrom::Start(offset) => offset,
438            SeekFrom::End(offset) => {
439                if offset < 0 {
440                    if (-offset) as u64 > self.header.size as u64 {
441                        return Err(std::io::Error::new(
442                            std::io::ErrorKind::InvalidInput,
443                            "Seek from end exceeds file length",
444                        ));
445                    }
446                    self.header.size as u64 - (-offset) as u64
447                } else {
448                    self.header.size as u64 + offset as u64
449                }
450            }
451            SeekFrom::Current(offset) => {
452                if offset < 0 {
453                    if (-offset) as u64 > self.pos {
454                        return Err(std::io::Error::new(
455                            std::io::ErrorKind::InvalidInput,
456                            "Seek from current exceeds current position",
457                        ));
458                    }
459                    self.pos.saturating_sub((-offset) as u64)
460                } else {
461                    self.pos + offset as u64
462                }
463            }
464        };
465        self.pos = new_pos;
466        Ok(self.pos)
467    }
468
469    fn stream_position(&mut self) -> std::io::Result<u64> {
470        Ok(self.pos)
471    }
472}