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}