msg_tool\scripts/
base.rs

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