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}