msg_tool\scripts\yaneurao\itufuru/
archive.rs

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