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