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