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