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