msg_tool\scripts/
base.rs

1//! Basic traits and types for script.
2use crate::ext::io::*;
3use crate::types::*;
4use anyhow::Result;
5use std::collections::HashMap;
6use std::io::{Read, Seek, Write};
7
8/// A trait for reading and seeking in a stream.
9pub trait ReadSeek: Read + Seek + std::fmt::Debug {}
10
11/// A trait for writing and seeking in a stream.
12pub trait WriteSeek: Write + Seek {}
13
14/// A trait for types that can be displayed in debug format and are also support downcasting.
15pub trait AnyDebug: std::fmt::Debug + std::any::Any {}
16
17impl<T: Read + Seek + std::fmt::Debug> ReadSeek for T {}
18
19impl<T: Write + Seek> WriteSeek for T {}
20
21impl<T: std::fmt::Debug + std::any::Any> AnyDebug for T {}
22
23/// A trait for script builders.
24pub trait ScriptBuilder: std::fmt::Debug {
25    /// Returns the default encoding for the script.
26    fn default_encoding(&self) -> Encoding;
27
28    /// Returns the default encoding for the archive.
29    /// If None, the default encoding should be used.
30    fn default_archive_encoding(&self) -> Option<Encoding> {
31        None
32    }
33
34    /// Returns the default encoding for script files when patching scripts.
35    fn default_patched_encoding(&self) -> Encoding {
36        self.default_encoding()
37    }
38
39    /// Builds a script from the given buffer.
40    ///
41    /// * `buf` - The buffer containing the script data.
42    /// * `filename` - The name of the file from which the script was read.
43    /// * `encoding` - The encoding of the script data.
44    /// * `archive_encoding` - The encoding of the archive, if applicable.
45    /// * `config` - Additional configuration options.
46    /// * `archive` - An optional archive to which the script belongs.
47    fn build_script(
48        &self,
49        buf: Vec<u8>,
50        filename: &str,
51        encoding: Encoding,
52        archive_encoding: Encoding,
53        config: &ExtraConfig,
54        archive: Option<&Box<dyn Script>>,
55    ) -> Result<Box<dyn Script>>;
56
57    /// Builds a script from a file.
58    ///
59    /// * `filename` - The name of the file to read.
60    /// * `encoding` - The encoding of the script data.
61    /// * `archive_encoding` - The encoding of the archive, if applicable.
62    /// * `config` - Additional configuration options.
63    /// * `archive` - An optional archive to which the script belongs.
64    fn build_script_from_file(
65        &self,
66        filename: &str,
67        encoding: Encoding,
68        archive_encoding: Encoding,
69        config: &ExtraConfig,
70        archive: Option<&Box<dyn Script>>,
71    ) -> Result<Box<dyn Script>> {
72        let data = crate::utils::files::read_file(filename)?;
73        self.build_script(data, filename, encoding, archive_encoding, config, archive)
74    }
75
76    /// Builds a script from a reader.
77    ///
78    /// * `reader` - A reader with seek capabilities.
79    /// * `filename` - The name of the file from which the script was read.
80    /// * `encoding` - The encoding of the script data.
81    /// * `archive_encoding` - The encoding of the archive, if applicable.
82    /// * `config` - Additional configuration options.
83    /// * `archive` - An optional archive to which the script belongs.
84    fn build_script_from_reader(
85        &self,
86        mut reader: Box<dyn ReadSeek>,
87        filename: &str,
88        encoding: Encoding,
89        archive_encoding: Encoding,
90        config: &ExtraConfig,
91        archive: Option<&Box<dyn Script>>,
92    ) -> Result<Box<dyn Script>> {
93        let mut data = Vec::new();
94        reader
95            .read_to_end(&mut data)
96            .map_err(|e| anyhow::anyhow!("Failed to read from reader: {}", e))?;
97        self.build_script(data, filename, encoding, archive_encoding, config, archive)
98    }
99
100    /// Returns the extensions supported by this script builder.
101    fn extensions(&self) -> &'static [&'static str];
102
103    /// Checks if the given filename and buffer match this script format.
104    /// * `filename` - The name of the file to check.
105    /// * `buf` - The buffer containing the script data.
106    /// * `buf_len` - The length of the buffer.
107    ///
108    /// Returns a score (0-255) indicating how well the format matches.
109    /// A higher score means a better match.
110    fn is_this_format(&self, _filename: &str, _buf: &[u8], _buf_len: usize) -> Option<u8> {
111        None
112    }
113
114    /// Returns the script type associated with this builder.
115    fn script_type(&self) -> &'static ScriptType;
116
117    /// Returns true if this script is an archive.
118    fn is_archive(&self) -> bool {
119        false
120    }
121
122    /// Creates an archive with the given files.
123    ///
124    /// * `filename` - The path of the archive file to create.
125    /// * `files` - A list of files to include in the archive.
126    /// * `encoding` - The encoding to use for the archive.
127    /// * `config` - Additional configuration options.
128    fn create_archive(
129        &self,
130        _filename: &str,
131        _files: &[&str],
132        _encoding: Encoding,
133        _config: &ExtraConfig,
134    ) -> Result<Box<dyn Archive>> {
135        Err(anyhow::anyhow!(
136            "This script type does not support creating an archive."
137        ))
138    }
139
140    /// Returns true if this script type can create from a file directly.
141    fn can_create_file(&self) -> bool {
142        false
143    }
144
145    /// Creates a new script file.
146    ///
147    /// * `filename` - The path to the input file.
148    /// * `writer` - A writer with seek capabilities to write the script data.
149    /// * `encoding` - The encoding to use for the script data.
150    /// * `file_encoding` - The encoding of the file.
151    /// * `config` - Additional configuration options.
152    fn create_file<'a>(
153        &'a self,
154        _filename: &'a str,
155        _writer: Box<dyn WriteSeek + 'a>,
156        _encoding: Encoding,
157        _file_encoding: Encoding,
158        _config: &ExtraConfig,
159    ) -> Result<()> {
160        Err(anyhow::anyhow!(
161            "This script type does not support creating directly."
162        ))
163    }
164
165    /// Creates a new script file with the given filename.
166    ///
167    /// * `filename` - The path to the input file.
168    /// * `output_filename` - The path to the output file.
169    /// * `encoding` - The encoding to use for the script data.
170    /// * `file_encoding` - The encoding of the file.
171    /// * `config` - Additional configuration options.
172    fn create_file_filename(
173        &self,
174        filename: &str,
175        output_filename: &str,
176        encoding: Encoding,
177        file_encoding: Encoding,
178        config: &ExtraConfig,
179    ) -> Result<()> {
180        let f = std::fs::File::create(output_filename)?;
181        let f = std::io::BufWriter::new(f);
182        self.create_file(filename, Box::new(f), encoding, file_encoding, config)
183    }
184
185    /// Returns true if this script is an image.
186    #[cfg(feature = "image")]
187    fn is_image(&self) -> bool {
188        false
189    }
190
191    /// Returns true if this script type can create from an image file directly.
192    #[cfg(feature = "image")]
193    fn can_create_image_file(&self) -> bool {
194        false
195    }
196
197    /// Creates an image file from the given data.
198    ///
199    /// * `data` - The image data to write.
200    /// * `writer` - A writer with seek capabilities to write the image data.
201    /// * `options` - Additional configuration options.
202    #[cfg(feature = "image")]
203    fn create_image_file<'a>(
204        &'a self,
205        _data: ImageData,
206        _writer: Box<dyn WriteSeek + 'a>,
207        _options: &ExtraConfig,
208    ) -> Result<()> {
209        Err(anyhow::anyhow!(
210            "This script type does not support creating an image file."
211        ))
212    }
213
214    /// Creates an image file from the given data to the specified filename.
215    ///
216    /// * `data` - The image data to write.
217    /// * `filename` - The path to the output file.
218    /// * `options` - Additional configuration options.
219    #[cfg(feature = "image")]
220    fn create_image_file_filename(
221        &self,
222        data: ImageData,
223        filename: &str,
224        options: &ExtraConfig,
225    ) -> Result<()> {
226        let f = std::fs::File::create(filename)?;
227        let f = std::io::BufWriter::new(f);
228        self.create_image_file(data, Box::new(f), options)
229    }
230}
231
232/// A trait to present the file in an archive.
233pub trait ArchiveContent: Read {
234    /// Returns the name of the file in the archive.
235    fn name(&self) -> &str;
236    /// Returns true if the file is a script.
237    fn is_script(&self) -> bool {
238        self.script_type().is_some()
239    }
240    /// Returns the script type if the file is a script.
241    fn script_type(&self) -> Option<&ScriptType> {
242        None
243    }
244    /// Returns the data of the file as a vector of bytes.
245    fn data(&mut self) -> Result<Vec<u8>> {
246        let mut data = Vec::new();
247        self.read_to_end(&mut data)?;
248        Ok(data)
249    }
250    /// Returns a reader that supports reading and seeking.
251    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
252        Ok(Box::new(MemReader::new(self.data()?)))
253    }
254}
255
256/// A trait for script types.
257pub trait Script: std::fmt::Debug + std::any::Any {
258    /// Returns the default output script type for this script.
259    fn default_output_script_type(&self) -> OutputScriptType;
260
261    /// Checks if the given output script type is supported by this script.
262    fn is_output_supported(&self, output: OutputScriptType) -> bool {
263        !matches!(output, OutputScriptType::Custom)
264    }
265
266    /// Returns the output extension for this script when exporting with custom output.
267    fn custom_output_extension<'a>(&'a self) -> &'a str {
268        ""
269    }
270
271    /// Returns the default format options for this script.
272    fn default_format_type(&self) -> FormatOptions;
273
274    /// Returns true if this script can contains multiple message files.
275    fn multiple_message_files(&self) -> bool {
276        false
277    }
278
279    /// Extract messages from this script.
280    fn extract_messages(&self) -> Result<Vec<Message>> {
281        if !self.is_archive() {
282            return Err(anyhow::anyhow!(
283                "This script type does not support extracting messages."
284            ));
285        }
286        Ok(vec![])
287    }
288
289    /// Extract multiple messages from this script.
290    fn extract_multiple_messages(&self) -> Result<HashMap<String, Vec<Message>>> {
291        if !self.multiple_message_files() {
292            return Err(anyhow::anyhow!(
293                "This script type does not support extracting multiple message files."
294            ));
295        }
296        Ok(HashMap::new())
297    }
298
299    /// Import messages into this script.
300    ///
301    /// * `messages` - The messages to import.
302    /// * `file` - A writer with seek capabilities to write the patched scripts.
303    /// * `filename` - The path of the file to write the patched scripts.
304    /// * `encoding` - The encoding to use for the patched scripts.
305    /// * `replacement` - An optional replacement table for message replacements.
306    fn import_messages<'a>(
307        &'a self,
308        _messages: Vec<Message>,
309        _file: Box<dyn WriteSeek + 'a>,
310        _filename: &str,
311        _encoding: Encoding,
312        _replacement: Option<&'a ReplacementTable>,
313    ) -> Result<()> {
314        if !self.is_archive() {
315            return Err(anyhow::anyhow!(
316                "This script type does not support importing messages."
317            ));
318        }
319        Ok(())
320    }
321
322    /// Import multiple messages into this script.
323    ///
324    /// * `messages` - A map of filenames to messages to import.
325    /// * `file` - A writer with seek capabilities to write the patched scripts.
326    /// * `filename` - The path of the file to write the patched scripts.
327    /// * `encoding` - The encoding to use for the patched scripts.
328    /// * `replacement` - An optional replacement table for message replacements.s
329    fn import_multiple_messages<'a>(
330        &'a self,
331        _messages: HashMap<String, Vec<Message>>,
332        _file: Box<dyn WriteSeek + 'a>,
333        _filename: &str,
334        _encoding: Encoding,
335        _replacement: Option<&'a ReplacementTable>,
336    ) -> Result<()> {
337        if !self.multiple_message_files() {
338            return Err(anyhow::anyhow!(
339                "This script type does not support importing multiple message files."
340            ));
341        }
342        Ok(())
343    }
344
345    /// Import messages into this script.
346    ///
347    /// * `messages` - The messages to import.
348    /// * `filename` - The path of the file to write the patched scripts.
349    /// * `encoding` - The encoding to use for the patched scripts.
350    /// * `replacement` - An optional replacement table for message replacements.
351    fn import_messages_filename(
352        &self,
353        messages: Vec<Message>,
354        filename: &str,
355        encoding: Encoding,
356        replacement: Option<&ReplacementTable>,
357    ) -> Result<()> {
358        let f = std::fs::File::create(filename)?;
359        let f = std::io::BufWriter::new(f);
360        self.import_messages(messages, Box::new(f), filename, encoding, replacement)
361    }
362
363    /// Import multiple messages into this script.
364    ///
365    /// * `messages` - A map of filenames to messages to import.
366    /// * `filename` - The path of the file to write the patched scripts.
367    /// * `encoding` - The encoding to use for the patched scripts.
368    /// * `replacement` - An optional replacement table for message replacements.
369    fn import_multiple_messages_filename(
370        &self,
371        messages: HashMap<String, Vec<Message>>,
372        filename: &str,
373        encoding: Encoding,
374        replacement: Option<&ReplacementTable>,
375    ) -> Result<()> {
376        let f = std::fs::File::create(filename)?;
377        let f = std::io::BufWriter::new(f);
378        self.import_multiple_messages(messages, Box::new(f), filename, encoding, replacement)
379    }
380
381    /// Exports data from this script.
382    ///
383    /// * `filename` - The path of the file to write the exported data.
384    /// * `encoding` - The encoding to use for the exported data.
385    fn custom_export(&self, _filename: &std::path::Path, _encoding: Encoding) -> Result<()> {
386        Err(anyhow::anyhow!(
387            "This script type does not support custom export."
388        ))
389    }
390
391    /// Imports data into this script.
392    ///
393    /// * `custom_filename` - The path of the file to import.
394    /// * `file` - A writer with seek capabilities to write the patched scripts.
395    /// * `encoding` - The encoding of the patched scripts.
396    /// * `output_encoding` - The encoding to use for the imported file.
397    fn custom_import<'a>(
398        &'a self,
399        _custom_filename: &'a str,
400        _file: Box<dyn WriteSeek + 'a>,
401        _encoding: Encoding,
402        _output_encoding: Encoding,
403    ) -> Result<()> {
404        Err(anyhow::anyhow!(
405            "This script type does not support custom import."
406        ))
407    }
408
409    /// Imports data into this script.
410    ///
411    /// * `custom_filename` - The path of the file to import.
412    /// * `filename` - The path of the file to write the patched scripts.
413    /// * `encoding` - The encoding of the patched scripts.
414    /// * `output_encoding` - The encoding to use for the imported file.
415    fn custom_import_filename(
416        &self,
417        custom_filename: &str,
418        filename: &str,
419        encoding: Encoding,
420        output_encoding: Encoding,
421    ) -> Result<()> {
422        let f = std::fs::File::create(filename)?;
423        let f = std::io::BufWriter::new(f);
424        self.custom_import(custom_filename, Box::new(f), encoding, output_encoding)
425    }
426
427    /// Returns true if this script is an archive.
428    fn is_archive(&self) -> bool {
429        false
430    }
431
432    /// Returns an iterator over archive filenames.
433    fn iter_archive_filename<'a>(
434        &'a self,
435    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
436        Err(anyhow::anyhow!(
437            "This script type does not support iterating over archive filenames."
438        ))
439    }
440
441    /// Returns an iterator over archive offsets.
442    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
443        Err(anyhow::anyhow!(
444            "This script type does not support iterating over archive offsets."
445        ))
446    }
447
448    /// Opens a file in the archive by its index.
449    fn open_file<'a>(&'a self, _index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
450        Err(anyhow::anyhow!(
451            "This script type does not support opening files."
452        ))
453    }
454
455    /// Opens a file in the archive by its name.
456    ///
457    /// * `name` - The name of the file to open.
458    /// * `ignore_case` - If true, the name comparison will be case-insensitive.
459    fn open_file_by_name<'a>(
460        &'a self,
461        name: &str,
462        ignore_case: bool,
463    ) -> Result<Box<dyn ArchiveContent + 'a>> {
464        for (i, fname) in self.iter_archive_filename()?.enumerate() {
465            if let Ok(fname) = fname {
466                if fname == name || (ignore_case && fname.eq_ignore_ascii_case(name)) {
467                    return self.open_file(i);
468                }
469            }
470        }
471        Err(anyhow::anyhow!(
472            "File with name '{}' not found in archive.",
473            name
474        ))
475    }
476
477    /// Opens a file in the archive by its offset.
478    fn open_file_by_offset<'a>(&'a self, offset: u64) -> Result<Box<dyn ArchiveContent + 'a>> {
479        for (i, off) in self.iter_archive_offset()?.enumerate() {
480            if let Ok(off) = off {
481                if off == offset {
482                    return self.open_file(i);
483                }
484            }
485        }
486        Err(anyhow::anyhow!(
487            "File with offset '{}' not found in archive.",
488            offset
489        ))
490    }
491
492    /// Returns output extension for archive output folder.
493    fn archive_output_ext<'a>(&'a self) -> Option<&'a str> {
494        None
495    }
496
497    #[cfg(feature = "image")]
498    /// Returns true if this script type is an image.
499    fn is_image(&self) -> bool {
500        false
501    }
502
503    #[cfg(feature = "image")]
504    /// Exports the image data from this script.
505    fn export_image(&self) -> Result<ImageData> {
506        Err(anyhow::anyhow!(
507            "This script type does not support to export image."
508        ))
509    }
510
511    #[cfg(feature = "image")]
512    /// Imports an image into this script.
513    ///
514    /// * `data` - The image data to import.
515    /// * `file` - A writer with seek capabilities to write the patched scripts.
516    fn import_image<'a>(&'a self, _data: ImageData, _file: Box<dyn WriteSeek + 'a>) -> Result<()> {
517        Err(anyhow::anyhow!(
518            "This script type does not support to import image."
519        ))
520    }
521
522    #[cfg(feature = "image")]
523    /// Imports an image into this script.
524    ///
525    /// * `data` - The image data to import.
526    /// * `filename` - The path of the file to write the patched scripts.
527    fn import_image_filename(&self, data: ImageData, filename: &str) -> Result<()> {
528        let f = std::fs::File::create(filename)?;
529        let f = std::io::BufWriter::new(f);
530        self.import_image(data, Box::new(f))
531    }
532
533    #[cfg(feature = "image")]
534    /// Returns true if this script is contains multiple images.
535    fn is_multi_image(&self) -> bool {
536        false
537    }
538
539    #[cfg(feature = "image")]
540    /// Exports multiple images from this script.
541    fn export_multi_image<'a>(
542        &'a self,
543    ) -> Result<Box<dyn Iterator<Item = Result<ImageDataWithName>> + 'a>> {
544        Err(anyhow::anyhow!(
545            "This script type does not support to export multi image."
546        ))
547    }
548
549    #[cfg(feature = "image")]
550    /// Imports multiple images into this script.
551    ///
552    /// * `data` - A vector of image data with names to import.
553    /// * `file` - A writer with seek capabilities to write the patched scripts.
554    fn import_multi_image<'a>(
555        &'a self,
556        _data: Vec<ImageDataWithName>,
557        _file: Box<dyn WriteSeek + 'a>,
558    ) -> Result<()> {
559        Err(anyhow::anyhow!(
560            "This script type does not support to import multi image."
561        ))
562    }
563
564    #[cfg(feature = "image")]
565    /// Imports multiple images into this script.
566    ///
567    /// * `data` - A vector of image data with names to import.
568    /// * `filename` - The path of the file to write the patched scripts.
569    fn import_multi_image_filename(
570        &self,
571        data: Vec<ImageDataWithName>,
572        filename: &str,
573    ) -> Result<()> {
574        let f = std::fs::File::create(filename)?;
575        let f = std::io::BufWriter::new(f);
576        self.import_multi_image(data, Box::new(f))
577    }
578
579    /// Returns the extra information for this script.
580    fn extra_info<'a>(&'a self) -> Option<Box<dyn AnyDebug + 'a>> {
581        None
582    }
583}
584
585/// A trait for creating archives.
586pub trait Archive {
587    /// Creates a new file in the archive.
588    ///
589    /// size is optional, if provided, size must be exactly the size of the file to be created.
590    fn new_file<'a>(&'a mut self, name: &str, size: Option<u64>)
591    -> Result<Box<dyn WriteSeek + 'a>>;
592    /// Creates a new file in the archive that does not require seeking.
593    ///
594    /// size is optional, if provided, size must be exactly the size of the file to be created.
595    fn new_file_non_seek<'a>(
596        &'a mut self,
597        name: &str,
598        size: Option<u64>,
599    ) -> Result<Box<dyn Write + 'a>> {
600        self.new_file(name, size)
601            .map(|f| Box::new(f) as Box<dyn Write + 'a>)
602    }
603    /// Writes the header of the archive. (Must be called after writing all files.)
604    fn write_header(&mut self) -> Result<()>;
605}