rpfm_lib/files/mod.rs
1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! This module contains the definition of RFile, the file abstraction used by this lib to decode/encode files.
12//!
13//! There is an additional type: [`Unknown`]. This type is used as a wildcard,
14//! so you can get the raw data of any non-supported file type and manipulate it yourself in a safe way.
15//!
16//! For more information about specific file types, including their binary format spec, please
17//! **check their respective documentation**.
18//!
19//! # Core Abstractions
20//!
21//! ## RFile
22//!
23//! The [`RFile`] struct is the central abstraction for all files in rpfm_lib. It can represent:
24//! - Files within PackFiles (or any [`Container`])
25//! - Files on the filesystem
26//! - Files in memory only
27//!
28//! Key features:
29//! - **Lazy Loading**: Files can be loaded on-demand to reduce memory usage
30//! - **Type Detection**: Automatically identifies file types based on path and content
31//! - **Caching**: Supports caching decoded data or raw bytes
32//! - **Metadata**: Tracks path, timestamp, container name, and file type
33//!
34//! ## File States
35//!
36//! Files can be in one of three internal states:
37//! - **OnDisk**: Data not yet loaded (lazy loading)
38//! - **Cached**: Raw bytes loaded but not decoded
39//! - **Decoded**: Fully parsed into a type-specific structure
40//!
41//! ## Decoding/Encoding
42//!
43//! All file types implement the [`Decodeable`] and [`Encodeable`] traits:
44//! - **Decodeable**: Parse binary data into structured format
45//! - **Encodeable**: Serialize structured format back to binary
46//!
47//! Extra data can be passed via [`DecodeableExtraData`] and [`EncodeableExtraData`]
48//! to provide context like schemas, game info, or file names.
49//!
50//! # Known file types
51//!
52//! | File Type | Decoding Supported | Encoding Supported |
53//! | ---------------------- | ------------------ | ------------------ |
54//! | [`Anim`] | Limited | Limited |
55//! | [`AnimFragmentBattle`] | Limited | Limited |
56//! | [`AnimPack`] | Yes | Yes |
57//! | [`AnimsTable`] | Yes | Yes |
58//! | [`Atlas`] | Yes | Yes |
59//! | [`Audio`] | No | No |
60//! | [`BMD`] | Limited | Limited |
61//! | [`BMD_Vegetation`] | Limited | Limited |
62//! | [`CS2_Collision`] | Limited | Limited |
63//! | [`CS2_Parsed`] | Limited | Limited |
64//! | [`Dat`] | Limited | Limited |
65//! | [`DB`] | Yes | Yes |
66//! | [`ESF`] | Limited | Limited |
67//! | [`Font`] | Limited | Limited |
68//! | [`GroupFormations`] | Limited | Limited |
69//! | [`HLSL_Compiled`] | Limited | Limited |
70//! | [`Image`] | Limited | Limited |
71//! | [`Loc`] | Yes | Yes |
72//! | [`MatchedCombat`] | Yes | Yes |
73//! | [`Pack`] | Yes | Yes |
74//! | [`PortraitSettings`] | Yes | Yes |
75//! | [`RigidModel`] | No | No |
76//! | [`SoundBank`] | No | No |
77//! | SoundBankDatabase | Limited | Limited |
78//! | SoundEvents | Limited | Limited |
79//! | [`Text`] | Yes | Yes |
80//! | [`TileDatabase`] | Yes | Yes |
81//! | [`UIC`] | No | No |
82//! | [`UnitVariant`] | Yes | Yes |
83//! | [`Video`] | Yes | Yes |
84//! | VMD | Yes | Yes |
85//! | WSModel | Yes | Yes |
86//!
87//! # Example Usage
88//!
89//! ```ignore
90//! use rpfm_lib::files::{RFile, Decodeable, db::DB, DecodeableExtraData, table::Table};
91//! use rpfm_lib::schema::Schema;
92//! use std::path::Path;
93//!
94//! // Load a DB table from disk
95//! let schema = Schema::load(Path::new("path/to/schema.ron"), None).unwrap();
96//! let mut extra = DecodeableExtraData::default();
97//! extra.set_schema(Some(&schema));
98//! extra.set_table_name(Some("units_tables"));
99//!
100//! let rfile = RFile::from_disk(Path::new("units"), &extra).unwrap();
101//!
102//! // Access the decoded data
103//! if let Some(db) = rfile.decoded().and_then(|d| d.db()) {
104//! println!("Table has {} rows", db.table().len());
105//! }
106//! ```
107//!
108//! [`Anim`]: crate::files::anim::Anim
109//! [`AnimFragmentBattle`]: crate::files::anim_fragment_battle::AnimFragmentBattle
110//! [`AnimPack`]: crate::files::animpack::AnimPack
111//! [`AnimsTable`]: crate::files::anims_table::AnimsTable
112//! [`Atlas`]: crate::files::atlas::Atlas
113//! [`Audio`]: crate::files::audio::Audio
114//! [`BMD`]: crate::files::bmd::Bmd
115//! [`BMD_Vegetation`]: crate::files::bmd_vegetation::BmdVegetation
116//! [`CS2_Collision`]: crate::files::cs2_collision::Cs2Collision
117//! [`CS2_Parsed`]: crate::files::cs2_parsed::Cs2Parsed
118//! [`Dat`]: crate::files::dat::Dat
119//! [`DB`]: crate::files::db::DB
120//! [`ESF`]: crate::files::esf::ESF
121//! [`Font`]: crate::files::font::Font
122//! [`GroupFormations`]: crate::files::group_formations::GroupFormations
123//! [`HLSL_Compiled`]: crate::files::hlsl_compiled::HlslCompiled
124//! [`Image`]: crate::files::image::Image
125//! [`Loc`]: crate::files::loc::Loc
126//! [`MatchedCombat`]: crate::files::matched_combat::MatchedCombat
127//! [`Pack`]: crate::files::pack::Pack
128//! [`PortraitSettings`]: crate::files::portrait_settings::PortraitSettings
129//! [`RigidModel`]: crate::files::rigidmodel::RigidModel
130//! [`SoundBank`]: crate::files::sound_bank::SoundBank
131//! [`SoundEvents`]: crate::files::sound_events::SoundEvents
132//! [`Text`]: crate::files::text::Text
133//! [`TileDatabase`]: crate::files::tile_database::TileDatabase
134//! [`UIC`]: crate::files::uic::UIC
135//! [`UnitVariant`]: crate::files::unit_variant::UnitVariant
136//! [`Unknown`]: crate::files::unknown::Unknown
137//! [`Video`]: crate::files::video::Video
138
139// File decoders intentionally use the `let mut x = T::default(); x.field = …;` pattern
140// to make debugging them easier.
141#![allow(clippy::field_reassign_with_default)]
142
143use crc_fast::{checksum, CrcAlgorithm};
144use csv::{QuoteStyle, ReaderBuilder, WriterBuilder};
145use getset::*;
146use log::warn;
147use rayon::prelude::*;
148use serde_derive::{Serialize, Deserialize};
149
150use std::cmp::Ordering;
151use std::collections::{HashMap, HashSet};
152use std::{fmt, fmt::{Debug, Display}};
153use std::fs::{DirBuilder, File};
154use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, BufWriter, Write};
155use std::path::{Path, PathBuf};
156
157use crate::binary::{ReadBytes, WriteBytes};
158use crate::compression::{CompressionFormat, Decompressible};
159use crate::encryption::Decryptable;
160use crate::error::{Result, RLibError};
161use crate::games::{GameInfo, pfh_version::PFHVersion, supported_games::*};
162use crate::{REGEX_CEO_DB, REGEX_DB, REGEX_PORTRAIT_SETTINGS};
163use crate::schema::{Schema, Definition};
164use crate::utils::*;
165
166use self::anim::Anim;
167use self::anim_fragment_battle::AnimFragmentBattle;
168use self::animpack::AnimPack;
169use self::anims_table::AnimsTable;
170use self::atlas::Atlas;
171use self::audio::Audio;
172use self::bmd::Bmd;
173use self::bmd_vegetation::BmdVegetation;
174use self::dat::Dat;
175use self::db::DB;
176use self::esf::ESF;
177use self::font::Font;
178use self::group_formations::GroupFormations;
179use self::hlsl_compiled::HlslCompiled;
180use self::image::Image;
181use self::loc::Loc;
182use self::matched_combat::MatchedCombat;
183use self::pack::{Pack, RESERVED_NAME_SETTINGS, RESERVED_NAME_NOTES, RESERVED_NAME_DEPENDENCIES_MANAGER, RESERVED_NAME_DEPENDENCIES_MANAGER_V2};
184use self::portrait_settings::PortraitSettings;
185use self::rigidmodel::RigidModel;
186use self::sound_bank::SoundBank;
187use self::text::Text;
188use self::uic::UIC;
189use self::unit_variant::UnitVariant;
190use self::unknown::Unknown;
191use self::video::Video;
192
193pub mod anim;
194pub mod anim_fragment_battle;
195pub mod animpack;
196pub mod anims_table;
197pub mod atlas;
198pub mod audio;
199#[allow(dead_code)]pub mod bmd;
200#[allow(dead_code)]pub mod bmd_vegetation;
201pub mod cs2_collision;
202pub mod cs2_parsed;
203pub mod dat;
204pub mod db;
205#[allow(dead_code)]pub mod esf;
206pub mod font;
207pub mod group_formations;
208pub mod hlsl_compiled;
209pub mod image;
210pub mod loc;
211pub mod matched_combat;
212pub mod pack;
213pub mod portrait_settings;
214pub mod rigidmodel;
215#[allow(dead_code)]pub mod sound_bank;
216pub mod sound_bank_database;
217pub mod sound_events;
218pub mod table;
219pub mod text;
220pub mod tile_database;
221pub mod uic;
222pub mod unit_variant;
223pub mod unknown;
224pub mod video;
225
226#[cfg(test)] mod rfile_test;
227
228//---------------------------------------------------------------------------//
229// Enum & Structs
230//---------------------------------------------------------------------------//
231
232/// Central file abstraction for rpfm_lib.
233///
234/// Represents a file that can exist in multiple locations and states:
235/// - Inside a PackFile or other container
236/// - On the filesystem
237/// - In memory only
238///
239/// # Lazy Loading
240///
241/// RFile supports lazy loading to minimize memory usage. Files can remain on disk
242/// until their data is actually needed, at which point they're loaded into memory
243/// automatically.
244///
245/// # File States
246///
247/// Internally, RFile can be in three states:
248/// - **OnDisk**: Metadata loaded, data still on disk (minimal memory)
249/// - **Cached**: Raw bytes loaded, not yet decoded
250/// - **Decoded**: Fully parsed into type-specific structure
251///
252/// # Type Detection
253///
254/// File types are detected based on:
255/// - File extension and path patterns
256/// - Magic numbers and header bytes
257/// - Container metadata
258///
259/// # Metadata
260///
261/// Each RFile tracks:
262/// - **path**: Location within container or filesystem (may be empty for memory-only files)
263/// - **timestamp**: Last modified time (optional)
264/// - **file_type**: Detected or specified file type
265/// - **container_name**: Source container name if from a container
266///
267/// # Example
268///
269/// ```ignore
270/// use rpfm_lib::files::{RFile, FileType};
271/// use std::path::Path;
272///
273/// // Load a file from disk
274/// let rfile = RFile::from_disk(Path::new("units.loc"), &None).unwrap();
275///
276/// // Check file type
277/// assert_eq!(*rfile.file_type(), FileType::Loc);
278///
279/// // Decode the file
280/// let decoded = rfile.decode(&None, false, false).unwrap();
281/// ```
282#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
283pub struct RFile {
284
285 /// Path of the file within a container or filesystem.
286 ///
287 /// Empty string for memory-only files.
288 path: String,
289
290 /// Last modified timestamp (Unix epoch).
291 timestamp: Option<u64>,
292
293 /// Detected or specified file type.
294 file_type: FileType,
295
296 /// Name of the source container, if applicable.
297 container_name: Option<String>,
298
299 /// Internal data storage (OnDisk, Cached, or Decoded).
300 ///
301 /// Use RFile methods instead of accessing directly.
302 data: RFileInnerData,
303}
304
305/// Internal data storage states for RFile.
306///
307/// Represents the three possible states of file data in memory:
308/// - **Decoded**: Fully parsed into structured format (highest memory, fastest access)
309/// - **Cached**: Raw bytes in memory (medium memory, requires decoding)
310/// - **OnDisk**: Metadata only, data on disk (lowest memory, requires loading + decoding)
311///
312/// This enum is internal and should not be used directly. Use RFile's public methods instead.
313///
314/// # State Transitions
315///
316/// ```text
317/// OnDisk → load() → Cached → decode() → Decoded
318/// ↑ ↓
319/// └───── encode() ──────┘
320/// ```
321#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
322enum RFileInnerData {
323
324 /// File data loaded and decoded into type-specific structure.
325 ///
326 /// Ready for immediate use. Highest memory usage.
327 Decoded(Box<RFileDecoded>),
328
329 /// Raw bytes loaded into memory but not decoded.
330 ///
331 /// Requires decoding before use. Medium memory usage.
332 Cached(Vec<u8>),
333
334 /// File data still on disk, only metadata in memory.
335 ///
336 /// Requires loading and decoding before use. Lowest memory usage.
337 OnDisk(OnDisk)
338}
339
340/// This struct represents a file on disk, which data has not been loaded to memory yet.
341///
342/// This may be a file directly on disk, or one inside another file (like inside a [Container]).
343///
344/// This is internal only. Users should not use it directly.
345#[derive(Clone, Debug, PartialEq, Getters, Serialize, Deserialize)]
346struct OnDisk {
347
348 /// Path of the file on disk where the data is.
349 ///
350 /// This may be a singular file or a file containing it
351 path: String,
352
353 /// Last modified date of the file that contains the data.
354 ///
355 /// This is used to both, get the last modified data into the file's metadata
356 /// and to check if the file has been manipulated since we created the OnDisk of it.
357 timestamp: u64,
358
359 /// Offset of the start of the file's data.
360 ///
361 /// `0` if the whole file is the data we want.
362 start: u64,
363
364 /// Size in bytes of the file's data.
365 size: u64,
366
367 /// Is the data compressed?.
368 is_compressed: bool,
369
370 /// Is the data encrypted? And if so, with which format?.
371 is_encrypted: Option<PFHVersion>,
372}
373
374/// This enum allow us to store any kind of decoded file type on a common place.
375#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
376pub enum RFileDecoded {
377 Anim(Anim),
378 AnimFragmentBattle(AnimFragmentBattle),
379 AnimPack(AnimPack),
380 AnimsTable(AnimsTable),
381 Atlas(Atlas),
382 Audio(Audio),
383 BMD(Box<Bmd>),
384 BMDVegetation(BmdVegetation),
385 Dat(Dat),
386 DB(DB),
387 ESF(ESF),
388 Font(Font),
389 GroupFormations(GroupFormations),
390 HlslCompiled(HlslCompiled),
391 Image(Image),
392 Loc(Loc),
393 MatchedCombat(MatchedCombat),
394 Pack(Pack),
395 PortraitSettings(PortraitSettings),
396 RigidModel(RigidModel),
397 SoundBank(SoundBank),
398 Text(Text),
399 UIC(UIC),
400 UnitVariant(UnitVariant),
401 Unknown(Unknown),
402 Video(Video),
403 VMD(Text),
404 WSModel(Text),
405}
406
407/// Known file types in Total War games.
408///
409/// Categorizes files by their format and purpose. Each variant corresponds to a dedicated
410/// submodule that implements parsing and encoding for that file type.
411///
412/// # Type Detection
413///
414/// File types are determined by:
415/// - **Extension matching**: Primary method for most file types
416/// - **Path patterns**: For files with special naming (e.g., DB tables)
417/// - **Magic numbers**: For format disambiguation when needed
418///
419/// # Support Levels
420///
421/// - **Full**: Complete decoding and encoding support
422/// - **Partial**: Read support with limitations
423/// - **Passthrough**: Raw data only (use [`Unknown`] for custom handling)
424///
425/// See the module-level documentation for a complete support matrix.
426///
427/// # Unknown Type
428///
429/// [`FileType::Unknown`] is the default fallback for unrecognized files. These files
430/// can still be read and written using the [`Unknown`] file type, which provides
431/// access to raw bytes.
432#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
433pub enum FileType {
434 Anim,
435 AnimFragmentBattle,
436 AnimPack,
437 AnimsTable,
438 Atlas,
439 Audio,
440 BMD,
441 BMDVegetation,
442 Dat,
443 DB,
444 ESF,
445 Font,
446 GroupFormations,
447 HlslCompiled,
448 Image,
449 Loc,
450 MatchedCombat,
451 Pack,
452 PortraitSettings,
453 RigidModel,
454 SoundBank,
455 Text,
456 UIC,
457 UnitVariant,
458 Video,
459 VMD,
460 WSModel,
461
462 #[default]
463 Unknown,
464}
465
466/// This enum represents a ***Path*** inside a [Container].
467#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
468pub enum ContainerPath {
469
470 /// This variant represents the path of a single file.
471 File(String),
472
473 /// This variant represents the path of a single folder.
474 ///
475 /// If this is empty, it represents the root of the container.
476 Folder(String),
477}
478
479/// Additional context data for [`Decodeable::decode()`] operations.
480///
481/// This structure provides optional configuration and metadata that decoders may need
482/// to properly interpret binary data. Different file types use different subsets of
483/// these fields.
484///
485/// # Field Categories
486///
487/// - **Configuration toggles**: Control decoder behavior (lazy loading, encryption status, etc.)
488/// - **OnDisk-related data**: File location and timestamp information
489/// - **Table-related data**: Database table-specific context
490/// - **Image-related data**: Image format detection flags
491/// - **General-purpose data**: Game info, file names, sizes
492///
493/// # Usage
494///
495/// ```ignore
496/// use rpfm_lib::files::DecodeableExtraData;
497///
498/// // For decoding a DB table
499/// let extra_data = DecodeableExtraData::default()
500/// .set_schema(Some(&schema))
501/// .set_game_info(Some(&game_info))
502/// .set_table_name(Some("units_tables"))
503/// .set_return_incomplete(true);
504/// ```
505///
506/// # Required Fields by Type
507///
508/// - **DB Tables**: Require `schema` and optionally `table_name` for fragments
509/// - **Containers (PackFiles)**: Use `lazy_load`, `disk_file_path`, `disk_file_offset`
510/// - **Images**: Use `is_dds` to enable DDS-specific decoding
511/// - **Generic files**: May use `game_info`, `file_name`, `data_size`
512///
513/// For specific requirements, consult each file type's documentation.
514#[derive(Clone, Debug, Default, Getters, Setters)]
515#[getset(get = "pub", set = "pub")]
516pub struct DecodeableExtraData<'a> {
517
518 //-----------------------//
519 // Configuration toggles //
520 //-----------------------//
521
522 /// Enable lazy loading for container files.
523 ///
524 /// When `true`, [`Container`] implementors will defer loading file data until accessed,
525 /// storing only metadata initially. This reduces memory usage for large containers.
526 lazy_load: bool,
527
528 /// Indicates whether the source data was encrypted.
529 ///
530 /// This is informational only - data reaching decode functions should already be decrypted.
531 /// Used for metadata tracking and logging.
532 is_encrypted: bool,
533
534 /// Allow returning partial data on decode errors (DB tables only).
535 ///
536 /// When `true`, table decoders will return successfully decoded rows even if later
537 /// rows fail to decode. When `false`, any decode error fails the entire operation.
538 return_incomplete: bool,
539
540 /// Schema definition for decoding DB tables and Loc files.
541 ///
542 /// Required for decoding database tables, as the schema defines the table structure,
543 /// column types, and versioning information.
544 schema: Option<&'a Schema>,
545
546 //----------------------------//
547 // OnDisk-related config data //
548 //----------------------------//
549
550 /// Absolute path to the file on disk.
551 ///
552 /// Used by lazy-loading containers to know where to read data from when needed.
553 /// Should be `None` for in-memory data.
554 disk_file_path: Option<&'a str>,
555
556 /// Byte offset within the disk file where this file's data begins.
557 ///
558 /// Used for files embedded within larger containers (e.g., individual files in a PackFile).
559 /// Set to `0` for standalone files.
560 disk_file_offset: u64,
561
562 /// File modification timestamp (Unix epoch seconds).
563 ///
564 /// Preserved from the original file for metadata tracking.
565 timestamp: u64,
566
567 //----------------------------//
568 // Table-related config data //
569 //----------------------------//
570
571 /// Name of the table (without extension or path).
572 ///
573 /// Used when decoding table fragments that don't have the full path context.
574 /// For example, when decoding a table from a loose file or unnamed buffer.
575 table_name: Option<&'a str>,
576
577 //----------------------------//
578 // Image-Related config data //
579 //----------------------------//
580
581 /// Flag indicating the image is in DDS (DirectDraw Surface) format.
582 ///
583 /// When `true`, enables DDS-specific decoding and conversion to PNG for display.
584 is_dds: bool,
585
586 //------------------------------//
587 // General-purpose config data //
588 //------------------------------//
589
590 /// Complete game information context.
591 ///
592 /// Provides game-specific settings, version info, and feature flags that may
593 /// affect decoding behavior (e.g., format variations between game versions).
594 game_info: Option<&'a GameInfo>,
595
596 /// Original filename (with extension).
597 ///
598 /// Used for file type detection, logging, and error messages.
599 file_name: Option<&'a str>,
600
601 /// Total size of the file data in bytes.
602 ///
603 /// May differ from buffer size if dealing with partial data or compressed streams.
604 data_size: u64,
605
606 /// Skip automatic path cache generation for containers.
607 ///
608 /// When `true`, container decoders won't build the lowercase path cache automatically.
609 /// You must manually call [`Container::paths_cache_generate()`] after decoding or
610 /// case-insensitive lookups will fail.
611 ///
612 /// This is an optimization for bulk operations where you'll rebuild the cache once
613 /// at the end instead of incrementally.
614 skip_path_cache_generation: bool,
615}
616
617/// Additional context data for [`Encodeable::encode()`] operations.
618///
619/// This structure provides optional configuration and metadata that encoders may need
620/// to properly serialize structured data to binary format. Different file types use
621/// different subsets of these fields.
622///
623/// # Field Categories
624///
625/// - **Configuration toggles**: Control encoder behavior (test mode, date handling, GUIDs)
626/// - **Optional config data**: Game info, compression settings
627///
628/// # Usage
629///
630/// ```ignore
631/// use rpfm_lib::files::EncodeableExtraData;
632/// use rpfm_lib::compression::CompressionFormat;
633///
634/// // For encoding a DB table with GUID
635/// let extra_data = EncodeableExtraData::default()
636/// .set_game_info(Some(&game_info))
637/// .set_table_has_guid(true)
638/// .set_regenerate_table_guid(true);
639///
640/// // For encoding with compression
641/// let extra_data = EncodeableExtraData::default()
642/// .set_compression_format(CompressionFormat::Lz4)
643/// .set_game_info(Some(&game_info));
644/// ```
645///
646/// # Common Configurations
647///
648/// - **DB Tables**: Use `table_has_guid` and `regenerate_table_guid` to control GUID handling
649/// - **Containers (PackFiles)**: Use `compression_format` to enable compression
650/// - **Testing**: Use `test_mode` and `nullify_dates` for deterministic output
651/// - **ESF Files**: Use `disable_compression` for nested encoding
652///
653/// For specific requirements, consult each file type's documentation.
654#[derive(Clone, Default, Getters, Setters)]
655#[getset(get = "pub", set = "pub")]
656pub struct EncodeableExtraData<'a> {
657
658 //-----------------------//
659 // Configuration toggles //
660 //-----------------------//
661
662 /// Enable test mode for deterministic output.
663 ///
664 /// When `true`, encoders may skip randomization or use fixed values for fields
665 /// that would normally vary (like auto-generated IDs), making output reproducible
666 /// for testing purposes.
667 test_mode: bool,
668
669 /// Zero out all date and timestamp fields.
670 ///
671 /// When `true`, any date or timestamp fields are written as `0` instead of their
672 /// actual values. Used in conjunction with `test_mode` for reproducible output,
673 /// or when dates should be reset.
674 nullify_dates: bool,
675
676 /// Include a GUID in the DB table header.
677 ///
678 /// When `true`, table encoders will write a GUID (Globally Unique Identifier) in
679 /// the table header. Required for Shogun 2 and newer games. Must be `false` for Empire
680 /// and Napoleon, as including a GUID crashes those games on load.
681 table_has_guid: bool,
682
683 /// Generate a new GUID for the table instead of preserving the existing one.
684 ///
685 /// When `true`, a fresh random GUID is generated. When `false`, the existing GUID
686 /// (if any) is preserved. Only meaningful when `table_has_guid` is also `true`.
687 regenerate_table_guid: bool,
688
689 //-----------------------//
690 // Optional config data //
691 //-----------------------//
692
693 /// Complete game information context.
694 ///
695 /// Provides game-specific settings, version info, and feature flags that may
696 /// affect encoding behavior (e.g., format variations between game versions).
697 game_info: Option<&'a GameInfo>,
698
699 /// Compression format to use when writing files.
700 ///
701 /// Specifies which compression algorithm to apply. Common formats include:
702 /// - [`CompressionFormat::None`]: No compression
703 /// - [`CompressionFormat::Lz4`]: Fast compression
704 /// - [`CompressionFormat::Zstd`]: Modern compression (best ratio)
705 /// - [`CompressionFormat::Lzma1`]: Legacy compression (older games)
706 ///
707 /// The game info may override this based on what the target game supports.
708 compression_format: CompressionFormat,
709
710 /// Disable compression for nested encoding operations.
711 ///
712 /// When `true`, prevents compression even if `compression_format` is set. Used for
713 /// ESF (Empire Save File) encoding where the outer container handles compression
714 /// and inner structures should remain uncompressed to avoid double-compression.
715 disable_compression: bool
716}
717
718//---------------------------------------------------------------------------//
719// Trait Definitions
720//---------------------------------------------------------------------------//
721
722/// Generic trait for decoding binary data into structured types.
723///
724/// This trait provides a standardized interface for deserializing binary data from any
725/// source implementing [`ReadBytes`]. All Total War file types
726/// in RPFM implement this trait to enable consistent decoding behavior.
727///
728/// # Type Parameters
729///
730/// The trait is object-safe and requires `Send + Sync` to enable concurrent decoding operations.
731///
732/// # Examples
733///
734/// ```ignore
735/// use rpfm_lib::files::{Decodeable, DecodeableExtraData};
736/// use rpfm_lib::binary::ReadBytes;
737///
738/// let data = &[0x01, 0x02, 0x03, 0x04];
739/// let extra_data = None;
740/// let decoded = MyType::decode(&mut data.as_slice(), &extra_data)?;
741/// ```
742pub trait Decodeable: Send + Sync {
743
744 /// Decodes binary data into the implementing type.
745 ///
746 /// This method reads from any source implementing [`ReadBytes`]
747 /// and constructs an instance of the implementing type.
748 ///
749 /// # Parameters
750 ///
751 /// - `data`: A mutable reference to a type implementing [`ReadBytes`]
752 /// - `extra_data`: Optional additional context needed for decoding (schemas, game version, etc.)
753 ///
754 /// # Returns
755 ///
756 /// Returns `Ok(Self)` on successful decoding, or an error if the data is malformed or
757 /// insufficient context was provided.
758 ///
759 /// # Errors
760 ///
761 /// This function may return errors if:
762 /// - The binary data is corrupted or malformed
763 /// - Required schema information is missing from `extra_data`
764 /// - The data stream ends unexpectedly
765 fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> where Self: Sized;
766}
767
768/// Generic trait for encoding structured types into binary data.
769///
770/// This trait provides a standardized interface for serializing structured data to any
771/// destination implementing [`WriteBytes`]. All Total War file types
772/// in RPFM implement this trait to enable consistent encoding behavior.
773///
774/// # Type Parameters
775///
776/// The trait is object-safe and requires `Send + Sync` to enable concurrent encoding operations.
777///
778/// # Examples
779///
780/// ```ignore
781/// use rpfm_lib::files::{Encodeable, EncodeableExtraData};
782/// use rpfm_lib::binary::WriteBytes;
783///
784/// let mut buffer = Vec::new();
785/// let extra_data = None;
786/// my_instance.encode(&mut buffer, &extra_data)?;
787/// ```
788pub trait Encodeable: Send + Sync {
789
790 /// Encodes the implementing type into binary data.
791 ///
792 /// This method writes to any destination implementing [`WriteBytes`],
793 /// serializing the instance's data in the appropriate Total War file format.
794 ///
795 /// # Parameters
796 ///
797 /// - `buffer`: A mutable reference to a type implementing [`WriteBytes`]
798 /// - `extra_data`: Optional additional context needed for encoding (schemas, game version, etc.)
799 ///
800 /// # Returns
801 ///
802 /// Returns `Ok(())` on successful encoding, or an error if the encoding process fails.
803 ///
804 /// # Errors
805 ///
806 /// This function may return errors if:
807 /// - Writing to the buffer fails
808 /// - Required schema information is missing from `extra_data`
809 /// - The data contains invalid values that cannot be serialized
810 fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()>;
811}
812
813/// Interface for working with container-like files.
814///
815/// This trait provides a unified API for manipulating file containers such as PackFiles,
816/// allowing implementors to store, retrieve, and manage collections of [`RFile`]s with
817/// hierarchical path structures.
818///
819/// # Implementors
820///
821/// - `Pack`: Total War PackFile containers (`.pack` files)
822/// - Other container formats that store multiple files
823///
824/// # Core Operations
825///
826/// The trait provides several categories of operations:
827///
828/// - **File access**: Get references to files by path, type, or pattern
829/// - **Insertion**: Add files from disk or [`RFile`] instances
830/// - **Extraction**: Write files to disk, optionally as TSV for DB/Loc files
831/// - **Removal**: Delete files or folders by path
832/// - **Queries**: Check existence, list folders, filter by type
833///
834/// # Path Handling
835///
836/// All paths use forward slashes (`/`) as separators, regardless of OS. Paths starting
837/// with `/` are automatically normalized by removing the leading slash.
838pub trait Container {
839
840 /// Extracts files from the container to disk.
841 ///
842 /// This method writes files matching the provided [`ContainerPath`] to the filesystem,
843 /// optionally preserving the container's folder structure.
844 ///
845 /// # Parameters
846 ///
847 /// - `container_path`: Path to file or folder within the container to extract
848 /// - `destination_path`: Target directory on disk where files will be written
849 /// - `keep_container_path_structure`: If `true`, preserves the container's folder hierarchy
850 /// - `schema`: If provided, attempts to export DB/Loc files as TSV (falls back to binary on error)
851 /// - `case_insensitive`: Enable case-insensitive folder matching (file extraction is always case-sensitive)
852 /// - `keys_first`: When exporting to TSV, place key columns first
853 /// - `extra_data`: Optional encoding context for binary files
854 ///
855 /// # Returns
856 ///
857 /// Returns a list of paths to the extracted files on disk.
858 ///
859 /// # Errors
860 ///
861 /// Returns an error if:
862 /// - The specified container path doesn't exist
863 /// - Disk I/O operations fail
864 /// - File decoding fails (for TSV export)
865 ///
866 /// # Examples
867 ///
868 /// ```ignore
869 /// // Extract a single file, preserving structure
870 /// let paths = container.extract(
871 /// ContainerPath::File("db/units_tables/units.bin".to_string()),
872 /// Path::new("./output"),
873 /// true, // Keep structure
874 /// &Some(schema),
875 /// false, // Case-sensitive
876 /// true, // Keys first
877 /// &None,
878 /// )?;
879 ///
880 /// // Extract entire folder as TSV
881 /// let paths = container.extract(
882 /// ContainerPath::Folder("db/".to_string()),
883 /// Path::new("./output"),
884 /// false, // Flat extraction
885 /// &Some(schema),
886 /// true, // Case-insensitive
887 /// true,
888 /// &None,
889 /// )?;
890 /// ```
891 #[allow(clippy::too_many_arguments)]
892 fn extract(&mut self,
893 container_path: ContainerPath,
894 destination_path: &Path,
895 keep_container_path_structure: bool,
896 schema: &Option<Schema>,
897 case_insensitive: bool,
898 keys_first: bool,
899 extra_data: &Option<EncodeableExtraData>,
900 ) -> Result<Vec<PathBuf>> {
901
902 let mut extracted_paths = vec![];
903 match container_path {
904 ContainerPath::File(mut container_path) => {
905 if container_path.starts_with('/') {
906 container_path.remove(0);
907 }
908
909 let destination_path = if keep_container_path_structure {
910 destination_path.to_owned().join(&container_path)
911 } else {
912 destination_path.to_owned()
913 };
914
915 let mut destination_folder = destination_path.to_owned();
916 destination_folder.pop();
917 DirBuilder::new().recursive(true).create(&destination_folder)?;
918
919 let rfile = self.files_mut().get_mut(&container_path).ok_or_else(|| RLibError::FileNotFound(container_path.to_string()))?;
920
921 // If we want to extract as tsv and we got a db/loc, export to tsv.
922 if let Some(schema) = schema {
923 if rfile.file_type() == FileType::DB || rfile.file_type() == FileType::Loc {
924 let mut destination_path_tsv = destination_path.to_owned();
925
926 // Make sure to NOT replace the extension if there is one, only append to it.
927 match destination_path_tsv.extension() {
928 Some(extension) => {
929 let extension = format!("{}.tsv", extension.to_string_lossy());
930 destination_path_tsv.set_extension(extension)
931 },
932 None => destination_path_tsv.set_extension("tsv"),
933 };
934
935 let result = rfile.tsv_export_to_path(&destination_path_tsv, schema, keys_first);
936
937 // If it fails to extract as tsv, extract as binary.
938 if result.is_err() {
939 warn!("File with path {} failed to extract as TSV. Extracting it as binary.", rfile.path_in_container_raw());
940
941 let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
942 extracted_paths.push(extracted_path);
943 } else {
944 extracted_paths.push(destination_path_tsv);
945 result?;
946 }
947 } else {
948 let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
949 extracted_paths.push(extracted_path);
950 }
951 }
952
953 // Otherwise, just write the binary data to disk.
954 else {
955 let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
956 extracted_paths.push(extracted_path);
957 }
958 }
959 ContainerPath::Folder(mut container_path) => {
960 if container_path.starts_with('/') {
961 container_path.remove(0);
962 }
963
964 let mut rfiles = self.files_by_path_mut(&ContainerPath::Folder(container_path.clone()), case_insensitive);
965 for rfile in &mut rfiles {
966 let container_path = rfile.path_in_container_raw();
967 let destination_path = if keep_container_path_structure {
968 destination_path.to_owned().join(container_path)
969 } else {
970 destination_path.to_owned()
971 };
972
973 let mut destination_folder = destination_path.to_owned();
974 destination_folder.pop();
975 DirBuilder::new().recursive(true).create(&destination_folder)?;
976
977 // If we want to extract as tsv and we got a db/loc, export to tsv.
978 if let Some(schema) = schema {
979 if rfile.file_type() == FileType::DB || rfile.file_type() == FileType::Loc {
980 let mut destination_path_tsv = destination_path.to_owned();
981
982 // Make sure to NOT replace the extension if there is one, only append to it.
983 match destination_path_tsv.extension() {
984 Some(extension) => {
985 let extension = format!("{}.tsv", extension.to_string_lossy());
986 destination_path_tsv.set_extension(extension)
987 },
988 None => destination_path_tsv.set_extension("tsv"),
989 };
990
991 let result = rfile.tsv_export_to_path(&destination_path_tsv, schema, keys_first);
992
993 // If it fails to extract as tsv, extract as binary.
994 if result.is_err() {
995 warn!("File with path {} failed to extract as TSV. Extracting it as binary.", rfile.path_in_container_raw());
996
997 let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
998 extracted_paths.push(extracted_path);
999 } else {
1000 extracted_paths.push(destination_path_tsv);
1001 result?;
1002 }
1003 } else {
1004 let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
1005 extracted_paths.push(extracted_path);
1006 }
1007 }
1008
1009 // Otherwise, just write the binary data to disk.
1010 else {
1011 let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
1012 extracted_paths.push(extracted_path);
1013 }
1014
1015 }
1016
1017 // If we're extracting the whole container, also extract any relevant metadata file associated with it.
1018 if container_path.is_empty() {
1019 extracted_paths.append(&mut self.extract_metadata(destination_path)?);
1020 }
1021 }
1022 }
1023
1024 Ok(extracted_paths)
1025 }
1026
1027 /// Extracts container metadata as `.json` files.
1028 ///
1029 /// This method writes any metadata associated with the container (such as pack settings,
1030 /// notes, or configuration) to the specified destination directory.
1031 ///
1032 /// # Parameters
1033 ///
1034 /// - `destination_path`: Directory where metadata files will be written
1035 ///
1036 /// # Returns
1037 ///
1038 /// Returns a list of paths to the extracted metadata files.
1039 ///
1040 /// # Default Implementation
1041 ///
1042 /// The default implementation does nothing and returns an empty list. Container types
1043 /// with metadata should override this method.
1044 fn extract_metadata(&mut self, _destination_path: &Path) -> Result<Vec<PathBuf>> {
1045 Ok(vec![])
1046 }
1047
1048 /// Inserts an [`RFile`] into the container.
1049 ///
1050 /// If a file with the same path already exists, it will be replaced.
1051 ///
1052 /// # Parameters
1053 ///
1054 /// - `file`: The [`RFile`] to insert
1055 ///
1056 /// # Returns
1057 ///
1058 /// Returns the [`ContainerPath`] of the inserted file, or `None` if insertion failed.
1059 fn insert(&mut self, file: RFile) -> Result<Option<ContainerPath>> {
1060 let path = file.path_in_container();
1061 let path_raw = file.path_in_container_raw();
1062
1063 self.paths_cache_insert_path(path_raw);
1064 self.files_mut().insert(path_raw.to_owned(), file);
1065 Ok(Some(path))
1066 }
1067
1068 /// Inserts a file from disk into the container.
1069 ///
1070 /// This method reads a file from the filesystem and inserts it at the specified path
1071 /// within the container. If a file already exists at that path, it will be replaced.
1072 ///
1073 /// # TSV Import
1074 ///
1075 /// If a [`Schema`] is provided and the source file has a `.tsv` extension, this method
1076 /// will attempt to import it as a binary DB/Loc file. If the conversion fails, it falls
1077 /// back to importing it as a plain text file.
1078 ///
1079 /// # Parameters
1080 ///
1081 /// - `source_path`: Path to the file on disk to import
1082 /// - `container_path_folder`: Target path within the container (folder or full path)
1083 /// - `schema`: Optional schema for TSV-to-binary conversion
1084 ///
1085 /// # Returns
1086 ///
1087 /// Returns the [`ContainerPath`] of the inserted file, or `None` if insertion failed.
1088 ///
1089 /// # Errors
1090 ///
1091 /// Returns an error if:
1092 /// - The source file cannot be read
1093 /// - File type detection fails
1094 ///
1095 /// # Path Behavior
1096 ///
1097 /// - If `container_path_folder` ends with `/` or is empty, the source filename is appended
1098 /// - Otherwise, `container_path_folder` is used as the full target path
1099 fn insert_file(&mut self, source_path: &Path, container_path_folder: &str, schema: &Option<Schema>) -> Result<Option<ContainerPath>> {
1100 let mut container_path_folder = container_path_folder.replace('\\', "/");
1101 if container_path_folder.starts_with('/') {
1102 container_path_folder.remove(0);
1103 }
1104
1105 if container_path_folder.ends_with('/') || container_path_folder.is_empty() {
1106 let trimmed_path = source_path.file_name()
1107 .ok_or_else(|| RLibError::PathMissingFileName(source_path.to_string_lossy().to_string()))?
1108 .to_string_lossy().to_string();
1109 container_path_folder = container_path_folder.to_owned() + &trimmed_path;
1110 }
1111
1112 // If tsv import is enabled, try to import the file to binary before adding it to the Container.
1113 let mut tsv_imported = false;
1114 let mut rfile = match source_path.extension() {
1115 Some(extension) if extension.to_string_lossy() == "tsv" => {
1116 tsv_imported = true;
1117 let rfile = RFile::tsv_import_from_path(source_path, schema);
1118 if let Err(error) = rfile {
1119 warn!("File with path {} failed to import as TSV. Importing it as binary. If you're using the CLI - did you forget to provide schema with --tsv-as-binary flag? Error was: {}", &source_path.to_string_lossy(), error);
1120
1121 tsv_imported = false;
1122 RFile::new_from_file_path(source_path)
1123 } else {
1124 rfile
1125 }
1126 }
1127 _ => RFile::new_from_file_path(source_path),
1128 }?;
1129
1130 if !tsv_imported {
1131 rfile.set_path_in_container_raw(&container_path_folder);
1132 }
1133
1134 // Make sure to guess the file type before inserting it.
1135 rfile.load()?;
1136 rfile.guess_file_type()?;
1137
1138 self.insert(rfile)
1139 }
1140
1141 /// Inserts an entire folder from disk into the container recursively.
1142 ///
1143 /// This method recursively scans a directory and imports all files, preserving
1144 /// the folder structure. Files with identical paths in the container are replaced.
1145 ///
1146 /// # TSV Import
1147 ///
1148 /// If a [`Schema`] is provided, `.tsv` files will be converted to binary DB/Loc format.
1149 /// If conversion fails, they're imported as plain text files.
1150 ///
1151 /// # Parameters
1152 ///
1153 /// - `source_path`: Path to the folder on disk to import
1154 /// - `container_path_folder`: Target folder path within the container
1155 /// - `ignored_paths`: Optional list of relative paths to exclude from import
1156 /// - `schema`: Optional schema for TSV-to-binary conversion
1157 /// - `include_base_folder`: If `true`, includes the source folder name in container paths
1158 ///
1159 /// # Returns
1160 ///
1161 /// Returns a list of all [`ContainerPath`]s that were inserted.
1162 ///
1163 /// # Errors
1164 ///
1165 /// Returns an error if:
1166 /// - The source directory cannot be read
1167 /// - Any file read or type detection fails
1168 ///
1169 /// # Folder Inclusion Behavior
1170 ///
1171 /// - `include_base_folder = false`: Contents of `source_path` go directly into `container_path_folder`
1172 /// - `include_base_folder = true`: A subfolder with the source folder's name is created first
1173 ///
1174 /// # Examples
1175 ///
1176 /// ```ignore
1177 /// // Import folder contents directly into "data/"
1178 /// container.insert_folder(
1179 /// Path::new("./my_mod"),
1180 /// "data/",
1181 /// &None,
1182 /// &Some(schema),
1183 /// false // Don't include "my_mod" folder name
1184 /// )?;
1185 ///
1186 /// // Import with ignored paths
1187 /// container.insert_folder(
1188 /// Path::new("./my_mod"),
1189 /// "data/",
1190 /// &Some(vec![".git/", "node_modules/"]),
1191 /// &None,
1192 /// true // Include "my_mod" folder name
1193 /// )?;
1194 /// ```
1195 fn insert_folder(&mut self, source_path: &Path, container_path_folder: &str, ignored_paths: &Option<Vec<&str>>, schema: &Option<Schema>, include_base_folder: bool) -> Result<Vec<ContainerPath>> {
1196 let mut container_path_folder = container_path_folder.replace('\\', "/");
1197 if !container_path_folder.is_empty() && !container_path_folder.ends_with('/') {
1198 container_path_folder.push('/');
1199 }
1200
1201 if container_path_folder.starts_with('/') {
1202 container_path_folder.remove(0);
1203 }
1204
1205 let mut source_path_without_base_folder = source_path.to_path_buf();
1206 source_path_without_base_folder.pop();
1207
1208 let file_paths = files_from_subdir(source_path, true)?;
1209 let mut inserted_paths = Vec::with_capacity(file_paths.len());
1210 for file_path in file_paths {
1211 let trimmed_path = if include_base_folder {
1212 file_path.strip_prefix(&source_path_without_base_folder)?
1213 } else {
1214 file_path.strip_prefix(source_path)?
1215 }.to_string_lossy().replace('\\', "/");
1216
1217 let file_container_path = container_path_folder.to_owned() + &trimmed_path;
1218
1219 if let Some(ignored_paths) = ignored_paths {
1220 if ignored_paths.iter().any(|x| trimmed_path.starts_with(x)) {
1221 continue;
1222 }
1223 }
1224
1225 // If tsv import is enabled, try to import the file to binary before adding it to the Container.
1226 let mut tsv_imported = false;
1227 let mut rfile = match file_path.extension() {
1228 Some(extension) if extension.to_string_lossy() == "tsv" => {
1229 tsv_imported = true;
1230 let rfile = RFile::tsv_import_from_path(&file_path, schema);
1231 if let Err(error) = rfile {
1232 warn!("File with path {} failed to import as TSV. Importing it as binary. If you're using the CLI - did you forget to provide schema with --tsv-as-binary flag? Error was: {}", &file_path.to_string_lossy(), error);
1233
1234 tsv_imported = false;
1235 RFile::new_from_file_path(&file_path)
1236 } else {
1237 rfile
1238 }
1239 }
1240 _ => RFile::new_from_file_path(&file_path),
1241 }?;
1242
1243 if !tsv_imported {
1244 rfile.set_path_in_container_raw(&file_container_path);
1245 }
1246
1247 // Make sure to guess the file type before inserting it.
1248 rfile.load()?;
1249 rfile.guess_file_type()?;
1250
1251 if let Some(path) = self.insert(rfile)? {
1252 inserted_paths.push(path);
1253 }
1254 }
1255
1256 Ok(inserted_paths)
1257 }
1258
1259 /// Removes files matching the provided [`ContainerPath`] from the container.
1260 ///
1261 /// This method deletes files or entire folder hierarchies from the container.
1262 ///
1263 /// # Parameters
1264 ///
1265 /// - `path`: The container path to remove (file or folder)
1266 ///
1267 /// # Returns
1268 ///
1269 /// Returns a list of removed [`ContainerPath`]s, always using the [`File`](ContainerPath::File) variant.
1270 ///
1271 /// # Special Cases
1272 ///
1273 /// - `ContainerPath::Folder("")`: Represents the container root, deletes **all** files
1274 /// - `ContainerPath::File(...)`: Deletes a single file
1275 /// - `ContainerPath::Folder(...)`: Deletes all files under that folder (recursive)
1276 ///
1277 /// # Examples
1278 ///
1279 /// ```ignore
1280 /// // Remove a single file
1281 /// container.remove(&ContainerPath::File("data/units.bin".to_string()));
1282 ///
1283 /// // Remove entire folder
1284 /// container.remove(&ContainerPath::Folder("db/".to_string()));
1285 ///
1286 /// // Clear entire container
1287 /// container.remove(&ContainerPath::Folder("".to_string()));
1288 /// ```
1289 fn remove(&mut self, path: &ContainerPath) -> Vec<ContainerPath> {
1290 match path {
1291 ContainerPath::File(path) => {
1292 let mut path = path.to_owned();
1293 if path.starts_with('/') {
1294 path.remove(0);
1295 }
1296
1297 self.paths_cache_remove_path(&path);
1298 self.files_mut().remove(&path);
1299 vec![ContainerPath::File(path.to_owned())]
1300 },
1301 ContainerPath::Folder(path) => {
1302 let mut path = path.to_owned();
1303 if path.starts_with('/') {
1304 path.remove(0);
1305 }
1306
1307 // If the path is empty, we mean the root of the container, including everything on it.
1308 if path.is_empty() {
1309 self.files_mut().clear();
1310 vec![ContainerPath::Folder(String::new()); 1]
1311 }
1312
1313 // Otherwise, it's a normal folder.
1314 else {
1315 let mut path_full = path.to_owned();
1316 path_full.push('/');
1317
1318 let paths_to_remove = self.files().par_iter()
1319 .filter_map(|(key, _)| {
1320
1321 // Make sure to only pick folders, not files matching folder names or partial folder matches!
1322 if key.starts_with(&path_full) {
1323 Some(key.to_owned())
1324 } else {
1325 None
1326 }
1327 }).collect::<Vec<String>>();
1328
1329 paths_to_remove.iter().for_each(|path| {
1330 self.paths_cache_remove_path(path);
1331 self.files_mut().remove(path);
1332 });
1333
1334 // Fix for when we try to delete empty folders.
1335 if paths_to_remove.is_empty() {
1336 vec![ContainerPath::Folder(path); 1]
1337 } else {
1338 paths_to_remove.par_iter().map(|path| ContainerPath::File(path.to_string())).collect()
1339 }
1340 }
1341 }
1342 }
1343 }
1344
1345 /// Returns the full path on disk where this container is stored.
1346 ///
1347 /// # Returns
1348 ///
1349 /// The absolute file path, or an empty string if the container is not backed by a disk file.
1350 fn disk_file_path(&self) -> &str;
1351
1352 /// Returns the filename of the container on disk.
1353 ///
1354 /// Extracts just the filename portion from [`disk_file_path()`](Self::disk_file_path).
1355 ///
1356 /// # Returns
1357 ///
1358 /// The filename as a string, or an empty string if no disk path is set.
1359 fn disk_file_name(&self) -> String {
1360 PathBuf::from(self.disk_file_path()).file_name().unwrap_or_default().to_string_lossy().to_string()
1361 }
1362
1363 /// Returns the byte offset of this container's data within its disk file.
1364 ///
1365 /// This is used for nested containers (e.g., a container embedded within another file).
1366 ///
1367 /// # Returns
1368 ///
1369 /// The offset in bytes, or `0` if the container starts at the beginning of the file.
1370 fn disk_file_offset(&self) -> u64;
1371
1372 /// Checks if a file with the specified path exists in the container.
1373 ///
1374 /// # Parameters
1375 ///
1376 /// - `path`: The file path to check (case-sensitive)
1377 ///
1378 /// # Returns
1379 ///
1380 /// `true` if the file exists, `false` otherwise.
1381 fn has_file(&self, path: &str) -> bool {
1382 self.files().get(path).is_some()
1383 }
1384
1385 /// Checks if a non-empty folder exists at the specified path.
1386 ///
1387 /// A folder is considered to exist if there is at least one file whose path starts
1388 /// with the provided folder path.
1389 ///
1390 /// # Parameters
1391 ///
1392 /// - `path`: The folder path to check
1393 ///
1394 /// # Returns
1395 ///
1396 /// `true` if the folder exists and contains files, `false` otherwise.
1397 ///
1398 /// # Note
1399 ///
1400 /// Empty string always returns `false`. Paths are normalized to end with `/` for matching.
1401 fn has_folder(&self, path: &str) -> bool {
1402 if path.is_empty() {
1403 false
1404 } else {
1405
1406 // Make sure we don't trigger false positives due to similarly started files/folders.
1407 let path = if path.ends_with('/') {
1408 path.to_string()
1409 } else {
1410 let mut path = path.to_string();
1411 path.push('/');
1412 path
1413 };
1414
1415 self.files().keys().any(|x| x.starts_with(&path) && x.len() > path.len())
1416 }
1417 }
1418
1419 /// Returns a reference to an [`RFile`] in the container by path.
1420 ///
1421 /// # Parameters
1422 ///
1423 /// - `path`: The file path to look up
1424 /// - `case_insensitive`: If `true`, performs case-insensitive matching
1425 ///
1426 /// # Returns
1427 ///
1428 /// `Some(&RFile)` if the file exists, `None` otherwise.
1429 fn file(&self, path: &str, case_insensitive: bool) -> Option<&RFile> {
1430 if case_insensitive {
1431 let lower = path.to_lowercase();
1432 self.paths_cache().get(&lower).and_then(|paths| self.files().get(&paths[0]))
1433 } else {
1434 self.files().get(path)
1435 }
1436 }
1437
1438 /// Returns a mutable reference to an [`RFile`] in the container by path.
1439 ///
1440 /// # Parameters
1441 ///
1442 /// - `path`: The file path to look up
1443 /// - `case_insensitive`: If `true`, performs case-insensitive matching
1444 ///
1445 /// # Returns
1446 ///
1447 /// `Some(&mut RFile)` if the file exists, `None` otherwise.
1448 fn file_mut(&mut self, path: &str, case_insensitive: bool) -> Option<&mut RFile> {
1449 if case_insensitive {
1450 let lower = path.to_lowercase();
1451 self.paths_cache().get(&lower).cloned().and_then(|paths| self.files_mut().get_mut(&paths[0]))
1452 } else {
1453 self.files_mut().get_mut(path)
1454 }
1455 }
1456
1457 /// Returns a reference to the internal file map.
1458 ///
1459 /// The map uses file paths as keys and [`RFile`]s as values.
1460 fn files(&self) -> &HashMap<String, RFile>;
1461
1462 /// Returns a mutable reference to the internal file map.
1463 ///
1464 /// The map uses file paths as keys and [`RFile`]s as values.
1465 fn files_mut(&mut self) -> &mut HashMap<String, RFile>;
1466
1467 /// Returns references to all files of the specified types.
1468 ///
1469 /// # Parameters
1470 ///
1471 /// - `file_types`: Slice of [`FileType`]s to filter by
1472 ///
1473 /// # Returns
1474 ///
1475 /// A vector of references to matching files.
1476 fn files_by_type(&self, file_types: &[FileType]) -> Vec<&RFile> {
1477 self.files()
1478 .iter()
1479 .filter(|(_, file)| file_types.contains(&file.file_type))
1480 .map(|(_, file)| file)
1481 .collect()
1482 }
1483
1484 /// Returns mutable references to all files of the specified types.
1485 ///
1486 /// # Parameters
1487 ///
1488 /// - `file_types`: Slice of [`FileType`]s to filter by
1489 ///
1490 /// # Returns
1491 ///
1492 /// A vector of mutable references to matching files.
1493 fn files_by_type_mut(&mut self, file_types: &[FileType]) -> Vec<&mut RFile> {
1494 self.files_mut().par_iter_mut().filter(|(_, file)| file_types.contains(&file.file_type)).map(|(_, file)| file).collect()
1495 }
1496
1497 /// Returns references to files matching the provided [`ContainerPath`].
1498 ///
1499 /// # Parameters
1500 ///
1501 /// - `path`: The container path to match (file or folder)
1502 /// - `case_insensitive`: Enable case-insensitive matching
1503 ///
1504 /// # Returns
1505 ///
1506 /// A vector of references to matching files.
1507 ///
1508 /// # Special Cases
1509 ///
1510 /// `ContainerPath::Folder("")` represents the container root and returns **all** files.
1511 fn files_by_path(&self, path: &ContainerPath, case_insensitive: bool) -> Vec<&RFile> {
1512 match path {
1513 ContainerPath::File(path) => self.file(path, case_insensitive).map(|file| vec![file]).unwrap_or(vec![]),
1514 ContainerPath::Folder(path) => {
1515
1516 // If the path is empty, get everything.
1517 if path.is_empty() {
1518 self.files().values().collect()
1519 }
1520
1521 else {
1522 let mut path = if case_insensitive { path.to_lowercase() } else { path.to_owned() };
1523 if !path.ends_with('/') {
1524 path.push('/');
1525 }
1526
1527 // Otherwise, only get the files under our folder.
1528 let real_paths = self.paths_cache()
1529 .par_iter()
1530 .filter_map(|(lower_path, real_paths)| if lower_path.starts_with(&path) { Some(real_paths) } else { None })
1531 .map(|paths| paths.iter().map(|path| ContainerPath::File(path.to_owned())).collect::<Vec<_>>())
1532 .flatten()
1533 .collect::<Vec<_>>();
1534
1535 self.files_by_paths(&real_paths, false)
1536 }
1537 },
1538 }
1539 }
1540
1541 /// Returns mutable references to files matching the provided [`ContainerPath`].
1542 ///
1543 /// # Parameters
1544 ///
1545 /// - `path`: The container path to match (file or folder)
1546 /// - `case_insensitive`: Enable case-insensitive matching
1547 ///
1548 /// # Returns
1549 ///
1550 /// A vector of mutable references to matching files.
1551 ///
1552 /// # Special Cases
1553 ///
1554 /// `ContainerPath::Folder("")` represents the container root and returns **all** files.
1555 fn files_by_path_mut(&mut self, path: &ContainerPath, case_insensitive: bool) -> Vec<&mut RFile> {
1556 match path {
1557 ContainerPath::File(path) => self.file_mut(path, case_insensitive).map(|file| vec![file]).unwrap_or(vec![]),
1558 ContainerPath::Folder(path) => {
1559
1560 // If the path is empty, get everything.
1561 if path.is_empty() {
1562 self.files_mut().values_mut().collect()
1563 }
1564
1565 // Otherwise, only get the files under our folder.
1566 else {
1567 self.files_mut().par_iter_mut()
1568 .filter_map(|(key, file)|
1569 if case_insensitive {
1570 if starts_with_case_insensitive(key, path) { Some(file) } else { None }
1571 } else if key.starts_with(path) {
1572 Some(file)
1573 } else {
1574 None
1575 }
1576 ).collect::<Vec<&mut RFile>>()
1577 }
1578 },
1579 }
1580 }
1581
1582 /// Returns references to files matching any of the provided [`ContainerPath`]s.
1583 ///
1584 /// # Parameters
1585 ///
1586 /// - `paths`: Slice of container paths to match
1587 /// - `case_insensitive`: Enable case-insensitive matching
1588 ///
1589 /// # Returns
1590 ///
1591 /// A vector of references to all matching files (may contain duplicates if paths overlap).
1592 fn files_by_paths(&self, paths: &[ContainerPath], case_insensitive: bool) -> Vec<&RFile> {
1593 paths.iter()
1594 .flat_map(|path| self.files_by_path(path, case_insensitive))
1595 .collect()
1596 }
1597
1598 /// Returns mutable references to files matching any of the provided [`ContainerPath`]s.
1599 ///
1600 /// This method should be used instead of [`files_by_path_mut()`](Self::files_by_path_mut)
1601 /// when you need mutable references to files across multiple different paths, as it
1602 /// properly handles the borrowing requirements.
1603 ///
1604 /// # Parameters
1605 ///
1606 /// - `paths`: Slice of container paths to match
1607 /// - `case_insensitive`: Enable case-insensitive matching
1608 ///
1609 /// # Returns
1610 ///
1611 /// A vector of mutable references to all matching files (no duplicates).
1612 fn files_by_paths_mut(&mut self, paths: &[ContainerPath], case_insensitive: bool) -> Vec<&mut RFile> {
1613 self.files_mut()
1614 .iter_mut()
1615 .filter(|(file_path, _)| {
1616 paths.iter().any(|path| {
1617 match path {
1618 ContainerPath::File(path) => {
1619 if case_insensitive {
1620 caseless::canonical_caseless_match_str(file_path, path)
1621 } else {
1622 file_path == &path
1623 }
1624 }
1625 ContainerPath::Folder(path) => {
1626 if case_insensitive {
1627 starts_with_case_insensitive(file_path, path)
1628 } else {
1629 file_path.starts_with(path)
1630 }
1631 }
1632 }
1633 })
1634 })
1635 .map(|(_, file)| file)
1636 .collect()
1637 }
1638
1639 /// Returns references to files matching both type and path criteria.
1640 ///
1641 /// This is a filtered combination of [`files_by_type()`](Self::files_by_type) and
1642 /// [`files_by_paths()`](Self::files_by_paths).
1643 ///
1644 /// # Parameters
1645 ///
1646 /// - `file_types`: Slice of [`FileType`]s to filter by
1647 /// - `paths`: Slice of [`ContainerPath`]s to match
1648 /// - `case_insensitive`: Enable case-insensitive path matching
1649 ///
1650 /// # Returns
1651 ///
1652 /// A vector of references to files matching both the type and path criteria.
1653 fn files_by_type_and_paths(&self, file_types: &[FileType], paths: &[ContainerPath], case_insensitive: bool) -> Vec<&RFile> {
1654 paths.iter()
1655 .flat_map(|path| self.files_by_path(path, case_insensitive)
1656 .into_iter()
1657 .filter(|file| file_types.contains(&file.file_type()))
1658 .collect::<Vec<_>>()
1659 ).collect()
1660 }
1661
1662 /// Returns mutable references to files matching both type and path criteria.
1663 ///
1664 /// This is a filtered combination of [`files_by_type_mut()`](Self::files_by_type_mut) and
1665 /// [`files_by_paths_mut()`](Self::files_by_paths_mut).
1666 ///
1667 /// # Parameters
1668 ///
1669 /// - `file_types`: Slice of [`FileType`]s to filter by
1670 /// - `paths`: Slice of [`ContainerPath`]s to match
1671 /// - `case_insensitive`: Enable case-insensitive path matching
1672 ///
1673 /// # Returns
1674 ///
1675 /// A vector of mutable references to files matching both the type and path criteria.
1676 fn files_by_type_and_paths_mut(&mut self, file_types: &[FileType], paths: &[ContainerPath], case_insensitive: bool) -> Vec<&mut RFile> {
1677 self.files_by_paths_mut(paths, case_insensitive).into_iter().filter(|file| file_types.contains(&file.file_type())).collect()
1678 }
1679
1680 /// Regenerates the internal paths cache from the current file list.
1681 ///
1682 /// The paths cache maps lowercase paths to their actual casing variants, enabling
1683 /// efficient case-insensitive lookups. This method should be called after bulk
1684 /// modifications to the file list.
1685 fn paths_cache_generate(&mut self) {
1686 self.paths_cache_mut().clear();
1687
1688 let mut cache: HashMap<String, Vec<String>> = HashMap::new();
1689 self.files().keys().for_each(|path| {
1690 let lower = path.to_lowercase();
1691 match cache.get_mut(&lower) {
1692 Some(paths) => paths.push(path.to_owned()),
1693 None => { cache.insert(lower, vec![path.to_owned()]); },
1694 }
1695 });
1696
1697 *self.paths_cache_mut() = cache;
1698 }
1699
1700 /// Adds a single path to the paths cache.
1701 ///
1702 /// This is more efficient than regenerating the entire cache when adding individual files.
1703 ///
1704 /// # Parameters
1705 ///
1706 /// - `path`: The file path to add (with original casing)
1707 fn paths_cache_insert_path(&mut self, path: &str) {
1708 let path_lower = path.to_lowercase();
1709 match self.paths_cache_mut().get_mut(&path_lower) {
1710 Some(paths) => if paths.iter().all(|x| x != path) {
1711 paths.push(path.to_owned());
1712 }
1713 None => { self.paths_cache_mut().insert(path_lower, vec![path.to_owned()]); }
1714 }
1715 }
1716
1717 /// Removes a single path from the paths cache.
1718 ///
1719 /// This is more efficient than regenerating the entire cache when removing individual files.
1720 ///
1721 /// # Parameters
1722 ///
1723 /// - `path`: The file path to remove (with original casing)
1724 ///
1725 /// # Note
1726 ///
1727 /// Reserved paths (notes, settings) are automatically skipped.
1728 fn paths_cache_remove_path(&mut self, path: &str) {
1729 let path_lower = path.to_lowercase();
1730
1731 // Skip reserved paths when using this.
1732 if path_lower == RESERVED_NAME_NOTES || path_lower == RESERVED_NAME_SETTINGS {
1733 return;
1734 }
1735
1736 match self.paths_cache_mut().get_mut(&path_lower) {
1737 Some(paths) => {
1738 match paths.iter().position(|x| x == path) {
1739 Some(pos) => {
1740 paths.remove(pos);
1741 if paths.is_empty() {
1742 self.paths_cache_mut().remove(&path_lower);
1743 }
1744 },
1745 None => { warn!("remove_path received a valid path, but we don't have casing equivalence for it. This is a bug. {path_lower}, {path}"); },
1746 }
1747 }
1748 None => { warn!("remove_path received an invalid path. This is a bug. {path_lower}, {path}"); },
1749 }
1750 }
1751
1752 /// Returns the paths cache mapping lowercase paths to their original-cased variants.
1753 ///
1754 /// This cache enables efficient case-insensitive file lookups. The map structure is:
1755 /// `lowercase_path -> vec![OriginalCased1, OriginalCased2, ...]`
1756 ///
1757 /// # Important
1758 ///
1759 /// If you manipulate the file list directly (via [`files_mut()`](Self::files_mut)),
1760 /// you **must** update this cache using [`paths_cache_insert_path()`](Self::paths_cache_insert_path),
1761 /// [`paths_cache_remove_path()`](Self::paths_cache_remove_path), or
1762 /// [`paths_cache_generate()`](Self::paths_cache_generate).
1763 fn paths_cache(&self) -> &HashMap<String, Vec<String>>;
1764
1765 /// Returns a mutable reference to the paths cache.
1766 ///
1767 /// See [`paths_cache()`](Self::paths_cache) for details on cache structure and maintenance.
1768 fn paths_cache_mut(&mut self) -> &mut HashMap<String, Vec<String>>;
1769
1770 /// Returns a set of all folder paths contained within the container.
1771 ///
1772 /// This method analyzes file paths to extract unique folder hierarchies.
1773 ///
1774 /// # Returns
1775 ///
1776 /// A set of folder paths (without trailing slashes). Root-level files contribute no entries.
1777 fn paths_folders_raw(&self) -> HashSet<String> {
1778 self.files()
1779 .par_iter()
1780 .filter_map(|(path, _)| {
1781 let file_path_split = path.split('/').collect::<Vec<&str>>();
1782 let folder_path_len = file_path_split.len() - 1;
1783 if folder_path_len == 0 {
1784 None
1785 } else {
1786
1787 let mut paths = Vec::with_capacity(folder_path_len);
1788
1789 for (index, folder) in file_path_split.iter().enumerate() {
1790 if index < path.len() - 1 && !folder.is_empty() {
1791 paths.push(file_path_split[0..=index].join("/"))
1792 }
1793 }
1794
1795 Some(paths)
1796 }
1797 })
1798 .flatten()
1799 .collect::<HashSet<String>>()
1800 }
1801
1802 /// This method returns the list of [ContainerPath] corresponding to RFiles within the provided Container.
1803 fn paths(&self) -> Vec<ContainerPath> {
1804 self.files()
1805 .par_iter()
1806 .map(|(path, _)| ContainerPath::File(path.to_owned()))
1807 .collect()
1808 }
1809
1810 /// This method returns the list of paths (as [&str]) corresponding to RFiles within the provided Container.
1811 fn paths_raw(&self) -> Vec<&str> {
1812 self.files()
1813 .par_iter()
1814 .map(|(path, _)| &**path)
1815 .collect()
1816 }
1817
1818 /// This function returns the list of paths (as [String]) corresponding to RFiles that match the provided [ContainerPath].
1819 fn paths_raw_from_container_path(&self, path: &ContainerPath) -> Vec<String> {
1820 match path {
1821 ContainerPath::File(path) => vec![path.to_owned(); 1],
1822 ContainerPath::Folder(path) => {
1823
1824 // If the path is empty, get everything.
1825 if path.is_empty() {
1826 self.paths_raw().iter().map(|x| x.to_string()).collect()
1827 }
1828
1829 // Otherwise, only get the paths under our folder.
1830 else {
1831 self.files().par_iter()
1832 .filter_map(|(key, file)|
1833 if key.starts_with(path) {
1834 Some(file.path_in_container_raw().to_owned())
1835 } else {
1836 None
1837 }
1838 ).collect::<Vec<String>>()
1839 }
1840 },
1841 }
1842 }
1843
1844 /// This method returns the `Last modified date` stored on the provided Container, in seconds.
1845 ///
1846 /// A default implementation that returns `0` is provided for Container types that don't support internal timestamps.
1847 ///
1848 /// Implementors should return `0` if the Container doesn't have a file on disk yet.
1849 fn internal_timestamp(&self) -> u64 {
1850 0
1851 }
1852
1853 /// This method returns the `Last modified date` the filesystem reports for the container file, in seconds.
1854 ///
1855 /// Implementors should return `0` if the Container doesn't have a file on disk yet.
1856 fn local_timestamp(&self) -> u64;
1857
1858 /// This function preloads to memory any lazy-loaded RFile within this container.
1859 fn preload(&mut self) -> Result<()> {
1860 self.files_mut()
1861 .into_par_iter()
1862 .try_for_each(|(_, rfile)| rfile.encode(&None, false, true, false).map(|_| ()))
1863 }
1864
1865 /// This function allows you to *move* multiple RFiles or folders of RFiles from one folder to another.
1866 ///
1867 /// It returns a list with all the new [ContainerPath].
1868 fn move_paths(&mut self, in_out_paths: &[(ContainerPath, ContainerPath)]) -> Result<Vec<(ContainerPath, ContainerPath)>> {
1869 let mut successes = vec![];
1870 for (source_path, destination_path) in in_out_paths {
1871 successes.append(&mut self.move_path(source_path, destination_path)?);
1872 }
1873
1874 Ok(successes)
1875 }
1876
1877 /// This function allows you to *move* any RFile or folder of RFiles from one folder to another.
1878 ///
1879 /// It returns a list with all the new [ContainerPath].
1880 fn move_path(&mut self, source_path: &ContainerPath, destination_path: &ContainerPath) -> Result<Vec<(ContainerPath, ContainerPath)>> {
1881 match source_path {
1882 ContainerPath::File(source_path) => match destination_path {
1883 ContainerPath::File(destination_path) => {
1884 if destination_path.is_empty() {
1885 return Err(RLibError::EmptyDestiny);
1886 }
1887
1888 self.paths_cache_remove_path(source_path);
1889 let mut moved = self
1890 .files_mut()
1891 .remove(source_path)
1892 .ok_or_else(|| RLibError::FileNotFound(source_path.to_string()))?;
1893
1894 moved.set_path_in_container_raw(destination_path);
1895
1896 self.insert(moved).map(|x| match x {
1897 Some(x) => vec![(ContainerPath::File(source_path.to_string()), x); 1],
1898 None => Vec::with_capacity(0)
1899 })
1900 },
1901 ContainerPath::Folder(_) => unreachable!("move_path_1"),
1902 },
1903 ContainerPath::Folder(source_path) => match destination_path {
1904 ContainerPath::File(_) => unreachable!("move_path_2"),
1905 ContainerPath::Folder(destination_path) => {
1906 if destination_path.is_empty() {
1907 return Err(RLibError::EmptyDestiny);
1908 }
1909
1910 // Fix to avoid false positives.
1911 let mut source_path_end = source_path.to_owned();
1912 if !source_path_end.ends_with('/') {
1913 source_path_end.push('/');
1914 }
1915
1916 let moved_paths = self.files()
1917 .par_iter()
1918 .filter_map(|(path, _)| if path.starts_with(&source_path_end) { Some(path.to_owned()) } else { None })
1919 .collect::<Vec<_>>();
1920
1921 let moved = moved_paths.iter()
1922 .filter_map(|x| {
1923 self.paths_cache_remove_path(x);
1924 self.files_mut().remove(x)
1925 })
1926 .collect::<Vec<_>>();
1927
1928 let mut new_paths = Vec::with_capacity(moved.len());
1929 for mut moved in moved {
1930 let old_path = moved.path_in_container();
1931 let new_path = moved.path_in_container_raw().replacen(source_path, destination_path, 1);
1932 moved.set_path_in_container_raw(&new_path);
1933
1934 if let Some(new_path) = self.insert(moved)? {
1935 new_paths.push((old_path, new_path));
1936 }
1937 }
1938
1939 Ok(new_paths)
1940 },
1941 },
1942 }
1943 }
1944
1945 /// This function removes all not-in-memory-already Files from the Container.
1946 ///
1947 /// Used for removing possibly corrupted RFiles from the Container in order to sanitize it.
1948 ///
1949 /// BE CAREFUL WITH USING THIS. IT MAY (PROBABLY WILL) CAUSE DATA LOSSES.
1950 fn clean_undecoded(&mut self) {
1951 self.files_mut().retain(|_, file| file.decoded().is_ok() || file.cached().is_ok());
1952 }
1953}
1954
1955//----------------------------------------------------------------//
1956// Implementations
1957//----------------------------------------------------------------//
1958
1959impl RFile {
1960
1961 /// This function creates a RFile from a lazy-loaded file inside a Container.
1962 ///
1963 /// About the parameters:
1964 /// - `container`: The container this RFile is on.
1965 /// - `size`: Size in bytes of the RFile.
1966 /// - `is_compressed`: If the RFile is compressed.
1967 /// - `is_encrypted`: If the RFile is encrypted.
1968 /// - `data_pos`: Byte offset of the data from the beginning of the Container.
1969 /// - `file_timestamp`: Timestamp of this specific file (not of the container, but the file). If it doesn't have one, pass 0.
1970 /// - `path_in_container`: Path of the RFile in the container.
1971 ///
1972 /// NOTE: Remember to call `guess_file_type` after this to properly set the FileType.
1973 pub fn new_from_container<C: Container>(
1974 container: &C,
1975 size: u64,
1976 is_compressed: bool,
1977 is_encrypted: Option<PFHVersion>,
1978 data_pos: u64,
1979 file_timestamp: u64,
1980 path_in_container: &str,
1981 ) -> Result<Self> {
1982 let on_disk = OnDisk {
1983 path: container.disk_file_path().to_owned(),
1984 timestamp: container.local_timestamp(),
1985 start: container.disk_file_offset() + data_pos,
1986 size,
1987 is_compressed,
1988 is_encrypted,
1989 };
1990
1991 let rfile = Self {
1992 path: path_in_container.to_owned(),
1993 timestamp: if file_timestamp == 0 { None } else { Some(file_timestamp) },
1994 file_type: FileType::Unknown,
1995 container_name: Some(container.disk_file_name()),
1996 data: RFileInnerData::OnDisk(on_disk)
1997 };
1998
1999 Ok(rfile)
2000 }
2001
2002 /// This function creates a RFile from a path on disk.
2003 ///
2004 /// This may fail if the file doesn't exist or errors out when trying to be read for metadata.
2005 ///
2006 /// NOTE: Remember to call `guess_file_type` after this to properly set the FileType.
2007 pub fn new_from_file(path: &str) -> Result<Self> {
2008 let path_checked = PathBuf::from(path);
2009 if !path_checked.is_file() {
2010 return Err(RLibError::FileNotFound(path.to_owned()));
2011 }
2012
2013 let mut file = File::open(path)?;
2014 let on_disk = OnDisk {
2015 path: path.to_owned(),
2016 timestamp: last_modified_time_from_file(&file)?,
2017 start: 0,
2018 size: file.len()?,
2019 is_compressed: false,
2020 is_encrypted: None,
2021 };
2022
2023
2024 let rfile = Self {
2025 path: path.to_owned(),
2026 timestamp: Some(on_disk.timestamp),
2027 file_type: FileType::Unknown,
2028 container_name: None,
2029 data: RFileInnerData::OnDisk(on_disk)
2030 };
2031
2032 Ok(rfile)
2033 }
2034
2035 /// This function creates a RFile from a path on disk.
2036 ///
2037 /// This may fail if the file doesn't exist or errors out when trying to be read for metadata.
2038 ///
2039 /// NOTE: Remember to call `guess_file_type` after this to properly set the FileType.
2040 pub fn new_from_file_path(path: &Path) -> Result<Self> {
2041 let path = path.to_string_lossy().to_string();
2042 Self::new_from_file(&path)
2043 }
2044
2045 /// This function creates a RFile from raw data on memory.
2046 ///
2047 /// NOTE: Remember to call `guess_file_type` after this to properly set the FileType.
2048 pub fn new_from_vec(data: &[u8], file_type: FileType, timestamp: u64, path: &str) -> Self {
2049 Self {
2050 path: path.to_owned(),
2051 timestamp: if timestamp == 0 { None } else { Some(timestamp) },
2052 file_type,
2053 container_name: None,
2054 data: RFileInnerData::Cached(data.to_vec())
2055 }
2056 }
2057
2058 /// This function creates a RFile from an RFileDecoded on memory.
2059 ///
2060 /// NOTE: Remember to call `guess_file_type` after this to properly set the FileType.
2061 pub fn new_from_decoded(data: &RFileDecoded, timestamp: u64, path: &str) -> Self {
2062 Self {
2063 path: path.to_owned(),
2064 timestamp: if timestamp == 0 { None } else { Some(timestamp) },
2065 file_type: FileType::from(data),
2066 container_name: None,
2067 data: RFileInnerData::Decoded(Box::new(data.clone()))
2068 }
2069 }
2070
2071 /// This function returns a reference to the cached data of an RFile, if said RFile has been cached. If not, it returns an error.
2072 ///
2073 /// Useful for accessing preloaded data.
2074 pub fn cached(&self) -> Result<&[u8]> {
2075 match self.data {
2076 RFileInnerData::Cached(ref data) => Ok(data),
2077 _ => Err(RLibError::FileNotCached(self.path_in_container_raw().to_string()))
2078 }
2079 }
2080
2081 /// This function returns a mutable reference to the cached data of an RFile, if said RFile has been cached. If not, it returns an error.
2082 ///
2083 /// Useful for accessing preloaded data.
2084 pub fn cached_mut(&mut self) -> Result<&mut Vec<u8>> {
2085 match self.data {
2086 RFileInnerData::Cached(ref mut data) => Ok(data),
2087 _ => Err(RLibError::FileNotCached(self.path_in_container_raw().to_string()))
2088 }
2089 }
2090
2091 /// This function returns a reference to the decoded data of an RFile, if said RFile has been decoded. If not, it returns an error.
2092 ///
2093 /// Useful for accessing preloaded data.
2094 pub fn decoded(&self) -> Result<&RFileDecoded> {
2095 match self.data {
2096 RFileInnerData::Decoded(ref data) => Ok(data),
2097 _ => Err(RLibError::FileNotDecoded(self.path_in_container_raw().to_string()))
2098 }
2099 }
2100
2101 /// This function returns a mutable reference to the decoded data of an RFile, if said RFile has been decoded. If not, it returns an error.
2102 ///
2103 /// Useful for accessing preloaded data.
2104 pub fn decoded_mut(&mut self) -> Result<&mut RFileDecoded> {
2105 match self.data {
2106 RFileInnerData::Decoded(ref mut data) => Ok(data),
2107 _ => Err(RLibError::FileNotDecoded(self.path_in_container_raw().to_string()))
2108 }
2109 }
2110
2111 /// This function replace any data a RFile has with the provided raw data.
2112 pub fn set_cached(&mut self, data: &[u8]) {
2113 self.data = RFileInnerData::Cached(data.to_vec());
2114 }
2115
2116 /// This function allows to replace the inner decoded data of a RFile with another. It'll fail if the decoded data is not valid for the file's type.
2117 pub fn set_decoded(&mut self, decoded: RFileDecoded) -> Result<()> {
2118 match (self.file_type(), &decoded) {
2119 (FileType::Anim, &RFileDecoded::Anim(_)) |
2120 (FileType::AnimFragmentBattle, &RFileDecoded::AnimFragmentBattle(_)) |
2121 (FileType::AnimPack, &RFileDecoded::AnimPack(_)) |
2122 (FileType::AnimsTable, &RFileDecoded::AnimsTable(_)) |
2123 (FileType::Atlas, &RFileDecoded::Atlas(_)) |
2124 (FileType::Audio, &RFileDecoded::Audio(_)) |
2125 (FileType::BMD, &RFileDecoded::BMD(_)) |
2126 (FileType::BMDVegetation, &RFileDecoded::BMDVegetation(_)) |
2127 (FileType::Dat, &RFileDecoded::Dat(_)) |
2128 (FileType::DB, &RFileDecoded::DB(_)) |
2129 (FileType::ESF, &RFileDecoded::ESF(_)) |
2130 (FileType::Font, &RFileDecoded::Font(_)) |
2131 (FileType::GroupFormations, &RFileDecoded::GroupFormations(_)) |
2132 (FileType::HlslCompiled, &RFileDecoded::HlslCompiled(_)) |
2133 (FileType::Image, &RFileDecoded::Image(_)) |
2134 (FileType::Loc, &RFileDecoded::Loc(_)) |
2135 (FileType::MatchedCombat, &RFileDecoded::MatchedCombat(_)) |
2136 (FileType::Pack, &RFileDecoded::Pack(_)) |
2137 (FileType::PortraitSettings, &RFileDecoded::PortraitSettings(_)) |
2138 (FileType::RigidModel, &RFileDecoded::RigidModel(_)) |
2139 (FileType::SoundBank, &RFileDecoded::SoundBank(_)) |
2140 (FileType::Text, &RFileDecoded::Text(_)) |
2141 (FileType::UIC, &RFileDecoded::UIC(_)) |
2142 (FileType::UnitVariant, &RFileDecoded::UnitVariant(_)) |
2143 (FileType::Video, &RFileDecoded::Video(_)) |
2144 (FileType::VMD, &RFileDecoded::VMD(_)) |
2145 (FileType::WSModel, &RFileDecoded::WSModel(_)) |
2146 (FileType::Unknown, &RFileDecoded::Unknown(_)) => self.data = RFileInnerData::Decoded(Box::new(decoded)),
2147 _ => return Err(RLibError::DecodedDataDoesNotMatchFileType(self.file_type(), From::from(&decoded)))
2148 }
2149
2150 Ok(())
2151 }
2152
2153 /// This function decodes an RFile from binary data, optionally caching and returning the decoded RFile.
2154 ///
2155 /// About the arguments:
2156 ///
2157 /// - `extra_data`: any data needed to decode specific file types. Check each file type for info about what do each file type need.
2158 /// - `keep_in_cache`: if true, the data will be cached on memory.
2159 /// - `return_data`: if true, the decoded data will be returned.
2160 ///
2161 /// NOTE: Passing `keep_in_cache` and `return_data` at false causes this function to decode the RFile and
2162 /// immediately drop the resulting data.
2163 pub fn decode(&mut self, extra_data: &Option<DecodeableExtraData>, keep_in_cache: bool, return_data: bool) -> Result<Option<RFileDecoded>> {
2164 let mut already_decoded = false;
2165 let decoded = match &self.data {
2166
2167 // If the data is already decoded, just return a copy of it.
2168 RFileInnerData::Decoded(data) => {
2169 already_decoded = true;
2170
2171 // Microoptimization: don't clone data if we're not going to use it.
2172 if !return_data {
2173 return Ok(None);
2174 }
2175
2176 *data.clone()
2177 },
2178
2179 // If the data is on memory but not yet decoded, decode it.
2180 RFileInnerData::Cached(data) => {
2181
2182 // Copy the provided extra data (if any), then replace the file-specific stuff.
2183 let mut extra_data = match extra_data {
2184 Some(extra_data) => extra_data.clone(),
2185 None => DecodeableExtraData::default(),
2186 };
2187 extra_data.file_name = self.file_name();
2188 extra_data.data_size = data.len() as u64;
2189
2190 // Some types require extra data specific for them to be added to the extra data before decoding.
2191 let mut data = Cursor::new(data);
2192 match self.file_type {
2193 FileType::Anim => RFileDecoded::Anim(Anim::decode(&mut data, &Some(extra_data))?),
2194 FileType::AnimFragmentBattle => RFileDecoded::AnimFragmentBattle(AnimFragmentBattle::decode(&mut data, &Some(extra_data))?),
2195 FileType::AnimPack => RFileDecoded::AnimPack(AnimPack::decode(&mut data, &Some(extra_data))?),
2196 FileType::AnimsTable => RFileDecoded::AnimsTable(AnimsTable::decode(&mut data, &Some(extra_data))?),
2197 FileType::Atlas => RFileDecoded::Atlas(Atlas::decode(&mut data, &Some(extra_data))?),
2198 FileType::Audio => RFileDecoded::Audio(Audio::decode(&mut data, &Some(extra_data))?),
2199 FileType::BMD => RFileDecoded::BMD(Box::new(Bmd::decode(&mut data, &Some(extra_data))?)),
2200 FileType::BMDVegetation => RFileDecoded::BMDVegetation(BmdVegetation::decode(&mut data, &Some(extra_data))?),
2201 FileType::Dat => RFileDecoded::Dat(Dat::decode(&mut data, &Some(extra_data))?),
2202 FileType::DB => {
2203
2204 if extra_data.table_name.is_none() {
2205 extra_data.table_name = self.db_table_name_from_path();
2206 }
2207 RFileDecoded::DB(DB::decode(&mut data, &Some(extra_data))?)
2208 },
2209 FileType::ESF => RFileDecoded::ESF(ESF::decode(&mut data, &Some(extra_data))?),
2210 FileType::Font => RFileDecoded::Font(Font::decode(&mut data, &Some(extra_data))?),
2211 FileType::GroupFormations => RFileDecoded::GroupFormations(GroupFormations::decode(&mut data, &Some(extra_data))?),
2212 FileType::HlslCompiled => RFileDecoded::HlslCompiled(HlslCompiled::decode(&mut data, &Some(extra_data))?),
2213 FileType::Image => {
2214
2215 if self.path.ends_with(".dds") {
2216 extra_data.is_dds = true;
2217 }
2218
2219 RFileDecoded::Image(Image::decode(&mut data, &Some(extra_data))?)
2220 },
2221 FileType::Loc => RFileDecoded::Loc(Loc::decode(&mut data, &Some(extra_data))?),
2222 FileType::MatchedCombat => RFileDecoded::MatchedCombat(MatchedCombat::decode(&mut data, &Some(extra_data))?),
2223 FileType::Pack => RFileDecoded::Pack(Pack::decode(&mut data, &Some(extra_data))?),
2224 FileType::PortraitSettings => RFileDecoded::PortraitSettings(PortraitSettings::decode(&mut data, &Some(extra_data))?),
2225 FileType::RigidModel => RFileDecoded::RigidModel(RigidModel::decode(&mut data, &Some(extra_data))?),
2226 FileType::SoundBank => RFileDecoded::SoundBank(SoundBank::decode(&mut data, &Some(extra_data))?),
2227 FileType::Text => RFileDecoded::Text(Text::decode(&mut data, &Some(extra_data))?),
2228 FileType::UIC => RFileDecoded::UIC(UIC::decode(&mut data, &Some(extra_data))?),
2229 FileType::UnitVariant => RFileDecoded::UnitVariant(UnitVariant::decode(&mut data, &Some(extra_data))?),
2230 FileType::Unknown => RFileDecoded::Unknown(Unknown::decode(&mut data, &Some(extra_data))?),
2231 FileType::Video => RFileDecoded::Video(Video::decode(&mut data, &Some(extra_data))?),
2232 FileType::VMD => RFileDecoded::VMD(Text::decode(&mut data, &Some(extra_data))?),
2233 FileType::WSModel => RFileDecoded::WSModel(Text::decode(&mut data, &Some(extra_data))?),
2234 }
2235 },
2236
2237 // If the data is not yet in memory, it depends:
2238 // - If it's something we can lazy-load and we want to, decode it directly from disk.
2239 // - If it's not, load it to memory and decode it from there.
2240 RFileInnerData::OnDisk(data) => {
2241
2242 // Copy the provided extra data (if any), then replace the file-specific stuff.
2243 let raw_data = data.read()?;
2244 let mut extra_data = match extra_data {
2245 Some(extra_data) => extra_data.clone(),
2246 None => DecodeableExtraData::default(),
2247 };
2248
2249 extra_data.file_name = self.file_name();
2250 extra_data.data_size = raw_data.len() as u64;
2251
2252 // These are the easy types: just load the data to memory, and decode.
2253 let mut data = Cursor::new(raw_data);
2254 match self.file_type {
2255 FileType::Anim => RFileDecoded::Anim(Anim::decode(&mut data, &Some(extra_data))?),
2256 FileType::AnimFragmentBattle => RFileDecoded::AnimFragmentBattle(AnimFragmentBattle::decode(&mut data, &Some(extra_data))?),
2257 FileType::AnimsTable => RFileDecoded::AnimsTable(AnimsTable::decode(&mut data, &Some(extra_data))?),
2258 FileType::AnimPack => RFileDecoded::AnimPack(AnimPack::decode(&mut data, &Some(extra_data))?),
2259 FileType::Atlas => RFileDecoded::Atlas(Atlas::decode(&mut data, &Some(extra_data))?),
2260 FileType::Audio => RFileDecoded::Audio(Audio::decode(&mut data, &Some(extra_data))?),
2261 FileType::BMD => RFileDecoded::BMD(Box::new(Bmd::decode(&mut data, &Some(extra_data))?)),
2262 FileType::BMDVegetation => RFileDecoded::BMDVegetation(BmdVegetation::decode(&mut data, &Some(extra_data))?),
2263 FileType::Dat => RFileDecoded::Dat(Dat::decode(&mut data, &Some(extra_data))?),
2264 FileType::DB => {
2265
2266 if extra_data.table_name.is_none() {
2267 extra_data.table_name = self.db_table_name_from_path();
2268 }
2269 RFileDecoded::DB(DB::decode(&mut data, &Some(extra_data))?)
2270 },
2271 FileType::ESF => RFileDecoded::ESF(ESF::decode(&mut data, &Some(extra_data))?),
2272 FileType::Font => RFileDecoded::Font(Font::decode(&mut data, &Some(extra_data))?),
2273 FileType::GroupFormations => RFileDecoded::GroupFormations(GroupFormations::decode(&mut data, &Some(extra_data))?),
2274 FileType::HlslCompiled => RFileDecoded::HlslCompiled(HlslCompiled::decode(&mut data, &Some(extra_data))?),
2275 FileType::Image => {
2276
2277 if self.path.ends_with(".dds") {
2278 extra_data.is_dds = true;
2279 }
2280
2281 RFileDecoded::Image(Image::decode(&mut data, &Some(extra_data))?)
2282 },
2283 FileType::Loc => RFileDecoded::Loc(Loc::decode(&mut data, &Some(extra_data))?),
2284 FileType::MatchedCombat => RFileDecoded::MatchedCombat(MatchedCombat::decode(&mut data, &Some(extra_data))?),
2285 FileType::Pack => RFileDecoded::Pack(Pack::decode(&mut data, &Some(extra_data))?),
2286 FileType::PortraitSettings => RFileDecoded::PortraitSettings(PortraitSettings::decode(&mut data, &Some(extra_data))?),
2287 FileType::RigidModel => RFileDecoded::RigidModel(RigidModel::decode(&mut data, &Some(extra_data))?),
2288 FileType::SoundBank => RFileDecoded::SoundBank(SoundBank::decode(&mut data, &Some(extra_data))?),
2289 FileType::Text => RFileDecoded::Text(Text::decode(&mut data, &Some(extra_data))?),
2290 FileType::UIC => RFileDecoded::UIC(UIC::decode(&mut data, &Some(extra_data))?),
2291 FileType::UnitVariant => RFileDecoded::UnitVariant(UnitVariant::decode(&mut data, &Some(extra_data))?),
2292 FileType::Unknown => RFileDecoded::Unknown(Unknown::decode(&mut data, &Some(extra_data))?),
2293 FileType::Video => RFileDecoded::Video(Video::decode(&mut data, &Some(extra_data))?),
2294 FileType::VMD => RFileDecoded::VMD(Text::decode(&mut data, &Some(extra_data))?),
2295 FileType::WSModel => RFileDecoded::WSModel(Text::decode(&mut data, &Some(extra_data))?),
2296 }
2297 },
2298 };
2299
2300 // If we're returning data, clone it. If not, skip the clone.
2301 if !already_decoded && keep_in_cache && return_data {
2302 self.data = RFileInnerData::Decoded(Box::new(decoded.clone()));
2303 } else if !already_decoded && keep_in_cache && !return_data{
2304 self.data = RFileInnerData::Decoded(Box::new(decoded));
2305 return Ok(None)
2306 }
2307
2308 if return_data {
2309 Ok(Some(decoded))
2310 } else {
2311 Ok(None)
2312 }
2313 }
2314
2315 /// This function encodes an RFile to binary, optionally caching and returning the data.
2316 ///
2317 /// About the arguments:
2318 /// - `extra_data`: any data needed to encode specific file types. Check each file type for info about what do each file type need.
2319 /// - `move_decoded_to_cache`: if true, the decoded data will be dropped in favor of undecoded cached data.
2320 /// - `move_undecoded_to_cache`: if true, the data will be cached on memory.
2321 /// - `return_data`: if true, the data will be returned.
2322 pub fn encode(&mut self, extra_data: &Option<EncodeableExtraData>, move_decoded_to_cache: bool, move_undecoded_to_cache: bool, return_data: bool) -> Result<Option<Vec<u8>>> {
2323 let mut previously_decoded = false;
2324 let mut already_encoded = false;
2325 let mut previously_undecoded = false;
2326
2327 let encoded = match &mut self.data {
2328 RFileInnerData::Decoded(data) => {
2329 previously_decoded = true;
2330 let mut buffer = vec![];
2331 match &mut **data {
2332 RFileDecoded::Anim(data) => data.encode(&mut buffer, extra_data)?,
2333 RFileDecoded::AnimFragmentBattle(data) => data.encode(&mut buffer, extra_data)?,
2334 RFileDecoded::AnimPack(data) => data.encode(&mut buffer, extra_data)?,
2335 RFileDecoded::AnimsTable(data) => data.encode(&mut buffer, extra_data)?,
2336 RFileDecoded::Atlas(data) => data.encode(&mut buffer, extra_data)?,
2337 RFileDecoded::Audio(data) => data.encode(&mut buffer, extra_data)?,
2338 RFileDecoded::BMD(data) => data.encode(&mut buffer, extra_data)?,
2339 RFileDecoded::BMDVegetation(data) => data.encode(&mut buffer, extra_data)?,
2340 RFileDecoded::Dat(data) => data.encode(&mut buffer, extra_data)?,
2341 RFileDecoded::DB(data) => data.encode(&mut buffer, extra_data)?,
2342 RFileDecoded::ESF(data) => data.encode(&mut buffer, extra_data)?,
2343 RFileDecoded::Font(data) => data.encode(&mut buffer, extra_data)?,
2344 RFileDecoded::GroupFormations(data) => data.encode(&mut buffer, extra_data)?,
2345 RFileDecoded::HlslCompiled(data) => data.encode(&mut buffer, extra_data)?,
2346 RFileDecoded::Image(data) => data.encode(&mut buffer, extra_data)?,
2347 RFileDecoded::Loc(data) => data.encode(&mut buffer, extra_data)?,
2348 RFileDecoded::MatchedCombat(data) => data.encode(&mut buffer, extra_data)?,
2349 RFileDecoded::Pack(data) => data.encode(&mut buffer, extra_data)?,
2350 RFileDecoded::PortraitSettings(data) => data.encode(&mut buffer, extra_data)?,
2351 RFileDecoded::RigidModel(data) => data.encode(&mut buffer, extra_data)?,
2352 RFileDecoded::SoundBank(data) => data.encode(&mut buffer, extra_data)?,
2353 RFileDecoded::Text(data) => data.encode(&mut buffer, extra_data)?,
2354 RFileDecoded::UIC(data) => data.encode(&mut buffer, extra_data)?,
2355 RFileDecoded::UnitVariant(data) => data.encode(&mut buffer, extra_data)?,
2356 RFileDecoded::Unknown(data) => data.encode(&mut buffer, extra_data)?,
2357 RFileDecoded::Video(data) => data.encode(&mut buffer, extra_data)?,
2358 RFileDecoded::VMD(data) => data.encode(&mut buffer, extra_data)?,
2359 RFileDecoded::WSModel(data) => data.encode(&mut buffer, extra_data)?,
2360 }
2361
2362 buffer
2363 },
2364 RFileInnerData::Cached(data) => {
2365 already_encoded = true;
2366 data.to_vec()
2367 },
2368 RFileInnerData::OnDisk(data) => {
2369 previously_undecoded = true;
2370 data.read()?
2371 },
2372 };
2373
2374 // If the RFile was already decoded.
2375 if previously_decoded {
2376 if move_decoded_to_cache {
2377 if return_data {
2378 self.data = RFileInnerData::Cached(encoded.to_vec());
2379 Ok(Some(encoded))
2380 } else {
2381 self.data = RFileInnerData::Cached(encoded);
2382 Ok(None)
2383 }
2384 } else if return_data {
2385 Ok(Some(encoded))
2386 } else {
2387 Ok(None)
2388 }
2389 }
2390
2391 // If the RFile was not even loaded.
2392 else if previously_undecoded {
2393 if move_undecoded_to_cache {
2394 if return_data {
2395 self.data = RFileInnerData::Cached(encoded.to_vec());
2396 Ok(Some(encoded))
2397 } else {
2398 self.data = RFileInnerData::Cached(encoded);
2399 Ok(None)
2400 }
2401 } else if return_data {
2402 Ok(Some(encoded))
2403 } else {
2404 Ok(None)
2405 }
2406 }
2407
2408 // If the RFile was already encoded and loaded.
2409 else if already_encoded && return_data {
2410 Ok(Some(encoded))
2411 } else {
2412 Ok(None)
2413 }
2414 }
2415
2416 /// This function loads the data of an RFile to memory if it's not yet loaded.
2417 ///
2418 /// If it has already been loaded either to cache, or for decoding, this does nothing.
2419 pub fn load(&mut self) -> Result<()> {
2420 let loaded = match &self.data {
2421 RFileInnerData::Decoded(_) |
2422 RFileInnerData::Cached(_) => return Ok(()),
2423 RFileInnerData::OnDisk(data) => data.read()?,
2424 };
2425
2426 // Piece of code to find text files we do not support yet. Needs enabling the content_inspector crate.
2427 #[cfg(feature = "enable_content_inspector")]
2428 if self.file_type() == FileType::Unknown && !loaded.is_empty() && content_inspector::inspect(&loaded).is_text() {
2429 dbg!(self.path_in_container_raw());
2430 }
2431
2432 self.data = RFileInnerData::Cached(loaded);
2433 Ok(())
2434 }
2435
2436 /// This function returns a copy of the `Last modified date` of this RFile, if any.
2437 pub fn timestamp(&self) -> Option<u64> {
2438 self.timestamp
2439 }
2440
2441 /// This function returns a copy of the FileType of this RFile.
2442 pub fn file_type(&self) -> FileType {
2443 self.file_type
2444 }
2445
2446 /// This function returns the file name if this RFile, if it has one.
2447 pub fn file_name(&self) -> Option<&str> {
2448 self.path_in_container_raw().split('/').next_back()
2449 }
2450
2451 /// This function returns the file name of the container this RFile originates from, if any.
2452 pub fn container_name(&self) -> &Option<String> {
2453 &self.container_name
2454 }
2455
2456 /// This function returns the [ContainerPath] corresponding to this file.
2457 pub fn path_in_container(&self) -> ContainerPath {
2458 ContainerPath::File(self.path.to_owned())
2459 }
2460
2461 /// This function returns the [ContainerPath] corresponding to this file as an [&str].
2462 pub fn path_in_container_raw(&self) -> &str {
2463 &self.path
2464 }
2465
2466 /// This function returns the [ContainerPath] corresponding to this file as a [Vec] of [&str].
2467 pub fn path_in_container_split(&self) -> Vec<&str> {
2468 self.path.split('/').collect()
2469 }
2470
2471 /// This function the *table_name* of this file (the folder that contains this file) if this file is a DB table.
2472 ///
2473 /// It returns None of the file provided is not a DB Table.
2474 pub fn db_table_name_from_path(&self) -> Option<&str> {
2475 let split_path = self.path.split('/').collect::<Vec<_>>();
2476 let start_lower = split_path[0].to_lowercase();
2477 if split_path.len() == 3 && (start_lower == "db" || start_lower == "ceo_db") {
2478 Some(split_path[1])
2479 } else {
2480 None
2481 }
2482 }
2483
2484 /// This function sets the [ContainerPath] of the provided RFile to the provided path..
2485 pub fn set_path_in_container_raw(&mut self, path: &str) {
2486 self.path = path.to_owned();
2487 }
2488
2489 /// This function returns if the RFile can be compressed or not.
2490 pub fn is_compressible(&self, game_info: &GameInfo) -> bool {
2491 !game_info.compression_formats_supported().is_empty() &&
2492
2493 // These files are needed in plain text for this lib to read them.
2494 self.file_name() != Some(RESERVED_NAME_DEPENDENCIES_MANAGER_V2) &&
2495 self.file_name() != Some(RESERVED_NAME_DEPENDENCIES_MANAGER) &&
2496 self.file_name() != Some(RESERVED_NAME_SETTINGS) &&
2497 self.file_name() != Some(RESERVED_NAME_NOTES) &&
2498
2499 // These files either do not benefit from compression (video), may cause issues due to compression (audio not working)
2500 // or may cause overhead due to decompression (models).
2501 !matches!(self.file_type, FileType::Audio | FileType::Dat | FileType::RigidModel | FileType::SoundBank | FileType::Video) &&
2502
2503 // We can only compress files if the game supports them. And only in WH3 (and newer games?) is the table compression bug fixed.
2504 (
2505 !matches!(self.file_type, FileType::DB | FileType::Loc) || (
2506 game_info.key() != KEY_PHARAOH_DYNASTIES &&
2507 game_info.key() != KEY_PHARAOH &&
2508 game_info.key() != KEY_TROY &&
2509 game_info.key() != KEY_THREE_KINGDOMS &&
2510 game_info.key() != KEY_WARHAMMER_2
2511 )
2512 )
2513 }
2514
2515 /// This function guesses the [`FileType`] of the provided RFile and stores it on it for later queries.
2516 ///
2517 /// The way it works is: first it tries to guess it by extension (fast), then by full path (not as fast), then by data (slow and it may fail on lazy-loaded files).
2518 ///
2519 /// This may fail for some files, so if you doubt set the type manually.
2520 pub fn guess_file_type(&mut self) -> Result<()> {
2521
2522 // First, try with extensions.
2523 let path = self.path.to_lowercase();
2524
2525 if path.ends_with(pack::EXTENSION) {
2526 self.file_type = FileType::Pack;
2527 }
2528
2529 else if path.ends_with(loc::EXTENSION) {
2530 self.file_type = FileType::Loc;
2531 }
2532
2533 else if path.ends_with(rigidmodel::EXTENSION) {
2534 self.file_type = FileType::RigidModel
2535 }
2536
2537 else if path.ends_with(animpack::EXTENSION) {
2538 self.file_type = FileType::AnimPack
2539 }
2540
2541 else if path.ends_with(anim::EXTENSION) {
2542 self.file_type = FileType::Anim
2543 }
2544
2545 else if path.ends_with(video::EXTENSION) {
2546 self.file_type = FileType::Video;
2547 }
2548
2549 else if path.ends_with(dat::EXTENSION) {
2550 self.file_type = FileType::Dat;
2551 }
2552
2553 else if path.ends_with(font::EXTENSION) {
2554 self.file_type = FileType::Font;
2555 }
2556
2557 else if audio::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
2558 self.file_type = FileType::Audio;
2559 }
2560
2561 // TODO: detect bin files for maps and tile maps.
2562 else if bmd::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
2563 self.file_type = FileType::BMD;
2564 }
2565
2566 else if bmd_vegetation::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
2567 self.file_type = FileType::BMDVegetation;
2568 }
2569
2570 else if path.ends_with(sound_bank::EXTENSION) {
2571 self.file_type = FileType::SoundBank;
2572 }
2573
2574 else if image::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
2575 self.file_type = FileType::Image;
2576 }
2577
2578 else if cfg!(feature = "support_uic") && path.starts_with(uic::BASE_PATH) && uic::EXTENSIONS.iter().any(|x| path.ends_with(x) || !path.contains('.')) {
2579 self.file_type = FileType::UIC;
2580 }
2581
2582 else if path.ends_with(text::EXTENSION_VMD.0) {
2583 self.file_type = FileType::VMD;
2584 }
2585
2586 else if path.ends_with(text::EXTENSION_WSMODEL.0) {
2587 self.file_type = FileType::WSModel;
2588 }
2589
2590 else if text::EXTENSIONS.iter().any(|(x, _)| path.ends_with(x)) {
2591 self.file_type = FileType::Text;
2592 }
2593
2594 else if path.ends_with(unit_variant::EXTENSION) {
2595 self.file_type = FileType::UnitVariant
2596 }
2597
2598 else if path == group_formations::PATH {
2599 self.file_type = FileType::GroupFormations;
2600 }
2601
2602 else if esf::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
2603 self.file_type = FileType::ESF;
2604 }
2605
2606 // If that failed, try types that need to be in a specific path.
2607 else if matched_combat::BASE_PATHS.iter().any(|x| path.starts_with(*x)) && path.ends_with(matched_combat::EXTENSION) {
2608 self.file_type = FileType::MatchedCombat;
2609 }
2610
2611 else if path.starts_with(anims_table::BASE_PATH) && path.ends_with(anims_table::EXTENSION) {
2612 self.file_type = FileType::AnimsTable;
2613 }
2614
2615 else if path.ends_with(anim_fragment_battle::EXTENSION_OLD) || (path.starts_with(anim_fragment_battle::BASE_PATH) && path.contains(anim_fragment_battle::MID_PATH) && path.ends_with(anim_fragment_battle::EXTENSION_NEW)) {
2616 self.file_type = FileType::AnimFragmentBattle;
2617 }
2618
2619 // If that failed, check if it's in a folder which is known to only have specific files.
2620 // Microoptimization: check the path before using the regex. Regex is very, VERY slow.
2621 else if (path.starts_with("db/") && REGEX_DB.is_match(&path)) || (path.starts_with("ceo_db/") && REGEX_CEO_DB.is_match(&path)) {
2622 self.file_type = FileType::DB;
2623 }
2624
2625 else if path.ends_with(portrait_settings::EXTENSION) && REGEX_PORTRAIT_SETTINGS.is_match(&path) {
2626 self.file_type = FileType::PortraitSettings;
2627 }
2628
2629 else if path.ends_with(atlas::EXTENSION) {
2630 self.file_type = FileType::Atlas;
2631 }
2632
2633 else if path.ends_with(hlsl_compiled::EXTENSION) {
2634 self.file_type = FileType::HlslCompiled;
2635 }
2636
2637 // If we reach this... we're clueless. Leave it unknown.
2638 else {
2639 self.file_type = FileType::Unknown;
2640 }
2641
2642 Ok(())
2643 }
2644
2645 /// This function allows to import a TSV file on the provided Path into a binary database file.
2646 ///
2647 /// It requires the path on disk of the TSV file and the Schema to use. Schema is only needed for DB tables.
2648 pub fn tsv_import_from_path(path: &Path, schema: &Option<Schema>) -> Result<Self> {
2649
2650 // We want the reader to have no quotes, tab as delimiter and custom headers, because otherwise
2651 // Excel, Libreoffice and all the programs that edit this kind of files break them on save.
2652 let mut reader = ReaderBuilder::new()
2653 .delimiter(b'\t')
2654 .quoting(false)
2655 .has_headers(true)
2656 .flexible(true)
2657 .from_path(path)?;
2658
2659 // If we successfully load the TSV file into a reader, check the first line to get the column list and order.
2660 let field_order = reader.headers()?
2661 .iter()
2662 .enumerate()
2663 .map(|(x, y)| (x as u32, y.to_owned()))
2664 .collect::<HashMap<u32, String>>();
2665
2666 // Get the record iterator so we can check the metadata from the second row.
2667 let mut records = reader.records();
2668 let (table_type, table_version, file_path) = match records.next() {
2669 Some(Ok(record)) => {
2670 let metadata = match record.get(0) {
2671 Some(metadata) => metadata.split(';').map(|x| x.to_owned()).collect::<Vec<String>>(),
2672 None => return Err(RLibError::ImportTSVWrongTypeTable),
2673 };
2674
2675 let table_type = match metadata.first() {
2676 Some(table_type) => {
2677 let mut table_type = table_type.to_owned();
2678 if table_type.starts_with('#') {
2679 table_type.remove(0);
2680 }
2681 table_type
2682 },
2683 None => return Err(RLibError::ImportTSVWrongTypeTable),
2684 };
2685
2686 let table_version = match metadata.get(1) {
2687 Some(table_version) => table_version.parse::<i32>().map_err(|_| RLibError::ImportTSVInvalidVersion)?,
2688 None => return Err(RLibError::ImportTSVInvalidVersion),
2689 };
2690
2691 let file_path = match metadata.get(2) {
2692 Some(file_path) => file_path.replace('\\', "/"),
2693 None => return Err(RLibError::ImportTSVInvalidOrMissingPath),
2694 };
2695
2696 (table_type, table_version, file_path)
2697 }
2698 Some(Err(_)) |
2699 None => return Err(RLibError::ImportTSVIncorrectRow(1, 0)),
2700 };
2701
2702 // Once we get the metadata, we know what kind of file we have. Create it and pass the records.
2703 let decoded = match &*table_type {
2704 loc::TSV_NAME_LOC | loc::TSV_NAME_LOC_OLD => {
2705 let decoded = Loc::tsv_import(records, &field_order)?;
2706 RFileDecoded::Loc(decoded)
2707 }
2708
2709 // Any other name is assumed to be a db table.
2710 _ => {
2711 match schema {
2712 Some(schema) => {
2713 let decoded = DB::tsv_import(records, &field_order, schema, &table_type, table_version)?;
2714 RFileDecoded::DB(decoded)
2715 },
2716 None => return Err(RLibError::SchemaNotProvided),
2717 }
2718 }
2719 };
2720
2721 let rfile = RFile::new_from_decoded(&decoded, 0, &file_path);
2722 Ok(rfile)
2723 }
2724
2725 /// This function allows to export a RFile into a TSV file on disk.
2726 ///
2727 /// Only supported for DB and Loc files.
2728 pub fn tsv_export_to_path(&mut self, path: &Path, schema: &Schema, keys_first: bool) -> Result<()> {
2729
2730 // Sanitize the path before creating the file
2731 let sanitized_path = sanitize_path(path);
2732
2733 // Make sure the folder actually exists.
2734 let mut folder_path = sanitized_path.to_path_buf();
2735 folder_path.pop();
2736 DirBuilder::new().recursive(true).create(&folder_path)?;
2737
2738 // We want the writer to have no quotes, tab as delimiter and custom headers, because otherwise
2739 // Excel, Libreoffice and all the programs that edit this kind of files break them on save.
2740 let mut writer = WriterBuilder::new()
2741 .delimiter(b'\t')
2742 .quote_style(QuoteStyle::Never)
2743 .has_headers(false)
2744 .flexible(true)
2745 .from_path(&sanitized_path)?;
2746
2747 let mut extra_data = DecodeableExtraData::default();
2748 extra_data.set_schema(Some(schema));
2749
2750 let extra_data = Some(extra_data);
2751
2752 // If it fails in decoding, delete the tsv file.
2753 let file = self.decode(&extra_data, false, true);
2754 if let Err(error) = file {
2755 let _ = std::fs::remove_file(&sanitized_path);
2756 return Err(error);
2757 }
2758
2759 let file = match file?.unwrap() {
2760 RFileDecoded::DB(table) => table.tsv_export(&mut writer, self.path_in_container_raw(), keys_first),
2761 RFileDecoded::Loc(table) => table.tsv_export(&mut writer, self.path_in_container_raw()),
2762 _ => unimplemented!()
2763 };
2764
2765 // If the tsv export failed, delete the tsv file.
2766 if file.is_err() {
2767 let _ = std::fs::remove_file(&sanitized_path);
2768 }
2769
2770 file
2771 }
2772
2773 /// This function tries to merge multiple files into one.
2774 ///
2775 /// All files must be of the same type and said type must support merging.
2776 pub fn merge(sources: &[&Self], path: &str) -> Result<Self> {
2777 if sources.len() <= 1 {
2778 return Err(RLibError::RFileMergeOnlyOneFileProvided);
2779 }
2780
2781 let mut file_types = sources.iter().map(|file| file.file_type()).collect::<Vec<_>>();
2782 file_types.sort();
2783 file_types.dedup();
2784
2785 if file_types.len() > 1 {
2786 return Err(RLibError::RFileMergeDifferentTypes);
2787 }
2788
2789 match file_types[0] {
2790 FileType::DB => {
2791 let files = sources.iter().filter_map(|file| if let Ok(RFileDecoded::DB(table)) = file.decoded() { Some(table) } else { None }).collect::<Vec<_>>();
2792 let data = RFileDecoded::DB(DB::merge(&files)?);
2793 Ok(Self::new_from_decoded(&data, current_time()?, path))
2794 },
2795 FileType::Loc => {
2796 let files = sources.iter().filter_map(|file| if let Ok(RFileDecoded::Loc(table)) = file.decoded() { Some(table) } else { None }).collect::<Vec<_>>();
2797 let data = RFileDecoded::Loc(Loc::merge(&files)?);
2798 Ok(Self::new_from_decoded(&data, current_time()?, path))
2799 },
2800 _ => Err(RLibError::RFileMergeNotSupportedForType(file_types[0].to_string())),
2801 }
2802 }
2803
2804 /// This function tries to update a file to a new version of the same file's format.
2805 ///
2806 /// Files used by this function are expected to be pre-decoded.
2807 pub fn update(&mut self, definition: &Option<Definition>) -> Result<()> {
2808 match self.decoded_mut() {
2809 Ok(RFileDecoded::DB(file)) => match definition {
2810 Some(definition) => file.update(definition),
2811 None => return Err(RLibError::RawTableMissingDefinition),
2812 }
2813 _ => return Err(RLibError::FileNotDecoded(self.path_in_container_raw().to_string())),
2814 }
2815
2816 Ok(())
2817 }
2818
2819 /// This function returns the data hash of the file.
2820 pub fn data_hash(&mut self, extra_data: &Option<EncodeableExtraData>) -> Result<u64> {
2821 Ok(match self.data {
2822 RFileInnerData::Decoded(_) => checksum(CrcAlgorithm::Crc32Iscsi, &self.encode(extra_data, false, false, true)?.unwrap()),
2823 RFileInnerData::Cached(ref data) => checksum(CrcAlgorithm::Crc32Iscsi, data),
2824 RFileInnerData::OnDisk(ref on_disk) => checksum(CrcAlgorithm::Crc32Iscsi, &on_disk.read()?),
2825 })
2826 }
2827
2828 /// Sanitizes a destination path and creates a file with the sanitized path.
2829 /// Logs a message if the filename was changed due to invalid Windows characters.
2830 pub fn sanitize_and_create_file(&mut self, destination_path: &Path, extra_data: &Option<EncodeableExtraData>) -> Result<PathBuf> {
2831 let sanitized_destination_path = sanitize_path(destination_path);
2832
2833 if sanitized_destination_path != destination_path {
2834 warn!("Filename sanitized from '{}' to '{}' due to invalid Windows characters",
2835 destination_path.to_owned().file_name().unwrap_or_default().to_string_lossy(),
2836 sanitized_destination_path.file_name().unwrap_or_default().to_string_lossy());
2837 }
2838
2839 // Encode first, then create the destination file. This way, if the source happens to be
2840 // the same path we're writing to (e.g. MyMod Import/Export), we read the source bytes
2841 // before File::create truncates it. Avoids needing to pre-load OnDisk files into memory.
2842 let data = self.encode(extra_data, false, false, true)?.unwrap();
2843 let mut file = BufWriter::new(File::create(&sanitized_destination_path)?);
2844 file.write_all(&data)?;
2845 Ok(sanitized_destination_path)
2846 }
2847
2848 /// Function to encode a file using data from a external path, effectively replacing its data.
2849 /// without replacing the file's metadata.
2850 ///
2851 /// NOTE: If TSV is detected and fails to import, this returns an error.
2852 pub fn encode_from_external_data(&mut self, schema: &Option<Schema>, external_path: &Path) -> Result<()> {
2853
2854 let mut file = BufReader::new(File::open(external_path)?);
2855 let mut data = vec![];
2856 file.read_to_end(&mut data)?;
2857
2858 // If we're dealing with a TSV, make sure to import it before setting up the data.
2859 match external_path.extension() {
2860 Some(extension) => {
2861 if extension.to_string_lossy() == "tsv" {
2862 match RFile::tsv_import_from_path(external_path, schema) {
2863 Ok(rfile) => self.set_decoded(rfile.decoded()?.clone())?,
2864 Err(_) => self.set_cached(&data),
2865 }
2866 } else {
2867 self.set_cached(&data);
2868 }
2869 }
2870 None => self.set_cached(&data),
2871 }
2872
2873 // If they're tables, make sure they're left decoded in memory.
2874 if self.file_type() == FileType::DB || self.file_type() == FileType::Loc {
2875 if let Some(ref schema) = schema {
2876 let mut extra_data = DecodeableExtraData::default();
2877 extra_data.set_schema(Some(schema));
2878 let extra_data = Some(extra_data);
2879 let _ = self.decode(&extra_data, true, false);
2880 }
2881 }
2882
2883 Ok(())
2884 }
2885}
2886
2887impl OnDisk {
2888
2889 /// This function tries to read and return the raw data of an RFile.
2890 ///
2891 /// This returns the data uncompressed and unencrypted.
2892 fn read(&self) -> Result<Vec<u8>> {
2893
2894 // Date check, to ensure the source file or container hasn't been modified since we got the indexes to read it.
2895 let mut file = BufReader::new(File::open(&self.path)?);
2896 let timestamp = last_modified_time_from_file(file.get_ref())?;
2897 if timestamp != self.timestamp {
2898 return Err(RLibError::FileSourceChanged(self.path.clone()));
2899 }
2900
2901 // Read the data from disk.
2902 let mut data = vec![0; self.size as usize];
2903 file.seek(SeekFrom::Start(self.start))?;
2904 file.read_exact(&mut data)?;
2905
2906 // If the data is encrypted, decrypt it.
2907 if self.is_encrypted.is_some() {
2908 data = Cursor::new(data).decrypt()?;
2909 }
2910
2911 // If the data is compressed. decompress it.
2912 if self.is_compressed {
2913 data = data.as_slice().decompress()?;
2914 }
2915
2916 Ok(data)
2917 }
2918}
2919
2920impl ContainerPath {
2921
2922 /// This function returns true if the provided [ContainerPath] corresponds to a file.
2923 pub fn is_file(&self) -> bool {
2924 matches!(self, ContainerPath::File(_))
2925 }
2926
2927 /// This function returns true if the provided [ContainerPath] corresponds to a folder.
2928 pub fn is_folder(&self) -> bool {
2929 matches!(self, ContainerPath::Folder(_))
2930 }
2931
2932 /// This function returns true if the provided [ContainerPath] corresponds to a root Pack.
2933 pub fn is_pack(&self) -> bool {
2934 match self {
2935 ContainerPath::Folder(path) => path.is_empty(),
2936 _ => false,
2937 }
2938 }
2939
2940 /// This function returns a reference to the path stored within the provided [ContainerPath].
2941 pub fn path_raw(&self) -> &str {
2942 match self {
2943 Self::File(ref path) => path,
2944 Self::Folder(ref path) => path,
2945 }
2946 }
2947
2948 /// This function returns the last item of the provided [ContainerPath], if any.
2949 pub fn name(&self) -> Option<&str> {
2950 self.path_raw().split('/').next_back()
2951 }
2952
2953 /// This function the *table_name* of this file (the folder that contains this file) if this file is a DB table.
2954 ///
2955 /// It returns None of the file provided is not a DB Table.
2956 pub fn db_table_name_from_path(&self) -> Option<&str> {
2957 let split_path = self.path_raw().split('/').collect::<Vec<_>>();
2958 let start_lower = split_path[0].to_lowercase();
2959 if split_path.len() == 3 && (start_lower == "db" || start_lower == "ceo_db") {
2960 Some(split_path[1])
2961 } else {
2962 None
2963 }
2964 }
2965
2966 /// This function returns the path of the parent folder of the provided [ContainerPath].
2967 ///
2968 /// If the provided [ContainerPath] corresponds to a Container root, the path returned will be the current one.
2969 pub fn parent_path(&self) -> String {
2970 match self {
2971 ContainerPath::File(path) |
2972 ContainerPath::Folder(path) => {
2973 if path.is_empty() || (path.chars().count() == 1 && path.starts_with('/')) {
2974 path.to_owned()
2975 } else {
2976 let mut path_split = path.split('/').collect::<Vec<_>>();
2977 path_split.pop();
2978 path_split.join("/")
2979 }
2980 },
2981 }
2982 }
2983
2984 /// This function removes collided items from the provided list of [ContainerPath].
2985 ///
2986 /// This means, if you have a file and a folder containing the file, it removes the file.
2987 pub fn dedup(paths: &[Self]) -> Vec<Self> {
2988
2989 // As this operation can get very expensive very fast, we first check if we have a path containing the root of the container.
2990 let root = ContainerPath::Folder("".to_string());
2991 if paths.contains(&root) {
2992 return vec![root; 1];
2993 }
2994
2995 // If we don't have the root of the container, second optimization: check if we have at least one folder.
2996 // If not, we just need to dedup the file list.
2997 if !paths.par_iter().any(|item| matches!(item, ContainerPath::Folder(_))) {
2998 let mut paths = paths.to_vec();
2999 paths.sort();
3000 paths.dedup();
3001 return paths;
3002 }
3003
3004 // If we reached this point, we have a mix of files and folders, or only folders.
3005 // In any case, we need to filter them, then dedup the resultant paths.
3006 let items_to_remove = paths.par_iter().filter(|item_type_to_add| {
3007 match item_type_to_add {
3008
3009 // If it's a file, we have to check if there is a folder containing it.
3010 ContainerPath::File(ref path_to_add) => {
3011 !paths.par_iter()
3012 .any(|item_type| {
3013
3014 // If the other one is a folder that contains it, dont add it.
3015 item_type.is_folder() && path_to_add.starts_with(item_type.path_raw())
3016 })
3017 }
3018
3019 // If it's a folder, we have to check if there is already another folder containing it.
3020 ContainerPath::Folder(ref path_to_add) => {
3021 !paths.par_iter()
3022 .any(|item_type| {
3023
3024 // If the other one is a folder that contains it, dont add it.
3025 let path = item_type.path_raw();
3026 item_type.is_folder() && path_to_add.starts_with(path) && path_to_add.len() > path.len() && path_to_add.is_char_boundary(path.len()) && path_to_add.as_bytes()[path.len()] == b'/'
3027 })
3028 }
3029 }
3030 }).cloned().collect::<Vec<Self>>();
3031
3032 let mut paths = paths.to_vec();
3033 paths.retain(|x| items_to_remove.contains(x));
3034 paths.sort();
3035 paths.dedup();
3036 paths
3037 }
3038}
3039
3040impl Ord for ContainerPath {
3041 fn cmp(&self, other: &Self) -> Ordering {
3042 match self {
3043 ContainerPath::File(a) => match other {
3044 ContainerPath::File(b) => a.cmp(b),
3045 ContainerPath::Folder(_) => Ordering::Less,
3046 }
3047 ContainerPath::Folder(a) => match other {
3048 ContainerPath::File(_) => Ordering::Greater,
3049 ContainerPath::Folder(b) => a.cmp(b),
3050 }
3051 }
3052 }
3053}
3054
3055impl PartialOrd for ContainerPath {
3056 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
3057 Some(self.cmp(other))
3058 }
3059}
3060
3061impl Display for FileType {
3062 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3063 match self {
3064 FileType::Anim => write!(f, "Anim"),
3065 FileType::AnimFragmentBattle => write!(f, "AnimFragmentBattle"),
3066 FileType::AnimPack => write!(f, "AnimPack"),
3067 FileType::AnimsTable => write!(f, "AnimsTable"),
3068 FileType::Atlas => write!(f, "Atlas"),
3069 FileType::Audio => write!(f, "Audio"),
3070 FileType::BMD => write!(f, "Battle Map Definition"),
3071 FileType::BMDVegetation => write!(f, "Battle Map Definition (Vegetation)"),
3072 FileType::Dat => write!(f, "Dat Audio File"),
3073 FileType::DB => write!(f, "DB Table"),
3074 FileType::ESF => write!(f, "ESF"),
3075 FileType::Font => write!(f, "Font"),
3076 FileType::HlslCompiled => write!(f, "Hlsl Compiled"),
3077 FileType::GroupFormations => write!(f, "Group Formations"),
3078 FileType::Image => write!(f, "Image"),
3079 FileType::Loc => write!(f, "Loc Table"),
3080 FileType::MatchedCombat => write!(f, "Matched Combat"),
3081 FileType::Pack => write!(f, "PackFile"),
3082 FileType::PortraitSettings => write!(f, "Portrait Settings"),
3083 FileType::RigidModel => write!(f, "RigidModel"),
3084 FileType::SoundBank => write!(f, "SoundBank"),
3085 FileType::Text => write!(f, "Text"),
3086 FileType::UIC => write!(f, "UI Component"),
3087 FileType::UnitVariant => write!(f, "Unit Variant"),
3088 FileType::Unknown => write!(f, "Unknown"),
3089 FileType::Video => write!(f, "Video"),
3090 FileType::VMD => write!(f, "VMD"),
3091 FileType::WSModel => write!(f, "WSModel"),
3092 }
3093 }
3094}
3095
3096impl From<&str> for FileType {
3097 fn from(value: &str) -> Self {
3098 match value {
3099 "Anim" => FileType::Anim,
3100 "AnimFragmentBattle" => FileType::AnimFragmentBattle,
3101 "AnimPack" => FileType::AnimPack,
3102 "AnimsTable" => FileType::AnimsTable,
3103 "Atlas" => FileType::Atlas,
3104 "Audio" => FileType::Audio,
3105 "BMD" => FileType::BMD,
3106 "BMDVegetation" => FileType::BMDVegetation,
3107 "Dat" => FileType::Dat,
3108 "DB" => FileType::DB,
3109 "ESF" => FileType::ESF,
3110 "Font" => FileType::Font,
3111 "HlslCompiled" => FileType::HlslCompiled,
3112 "GroupFormations" => FileType::GroupFormations,
3113 "Image" => FileType::Image,
3114 "Loc" => FileType::Loc,
3115 "MatchedCombat" => FileType::MatchedCombat,
3116 "Pack" => FileType::Pack,
3117 "PortraitSettings" => FileType::PortraitSettings,
3118 "RigidModel" => FileType::RigidModel,
3119 "SoundBank" => FileType::SoundBank,
3120 "Text" => FileType::Text,
3121 "UIC" => FileType::UIC,
3122 "UnitVariant" => FileType::UnitVariant,
3123 "Unknown" => FileType::Unknown,
3124 "Video" => FileType::Video,
3125 "VMD" => FileType::VMD,
3126 "WSModel" => FileType::WSModel,
3127 _ => unimplemented!(),
3128 }
3129 }
3130}
3131
3132impl From<FileType> for String {
3133 fn from(value: FileType) -> String {
3134 match value {
3135 FileType::Anim => "Anim",
3136 FileType::AnimFragmentBattle => "AnimFragmentBattle",
3137 FileType::AnimPack => "AnimPack",
3138 FileType::AnimsTable => "AnimsTable",
3139 FileType::Atlas => "Atlas",
3140 FileType::Audio => "Audio",
3141 FileType::BMD => "BMD",
3142 FileType::BMDVegetation => "BMD Vegetation",
3143 FileType::Dat => "Dat",
3144 FileType::DB => "DB",
3145 FileType::ESF => "ESF",
3146 FileType::Font => "Font",
3147 FileType::HlslCompiled => "HlslCompiled",
3148 FileType::GroupFormations => "GroupFormations",
3149 FileType::Image => "Image",
3150 FileType::Loc => "Loc",
3151 FileType::MatchedCombat => "MatchedCombat",
3152 FileType::Pack => "Pack",
3153 FileType::PortraitSettings => "PortraitSettings",
3154 FileType::RigidModel => "RigidModel",
3155 FileType::SoundBank => "SoundBank",
3156 FileType::Text => "Text",
3157 FileType::UIC => "UIC",
3158 FileType::UnitVariant => "UnitVariant",
3159 FileType::Unknown => "Unknown",
3160 FileType::Video => "Video",
3161 FileType::VMD => "VMD",
3162 FileType::WSModel => "WSModel",
3163 }.to_owned()
3164 }
3165}
3166
3167impl From<&RFileDecoded> for FileType {
3168 fn from(file: &RFileDecoded) -> Self {
3169 match file {
3170 RFileDecoded::Anim(_) => Self::Anim,
3171 RFileDecoded::AnimFragmentBattle(_) => Self::AnimFragmentBattle,
3172 RFileDecoded::AnimPack(_) => Self::AnimPack,
3173 RFileDecoded::AnimsTable(_) => Self::AnimsTable,
3174 RFileDecoded::Atlas(_) => Self::Atlas,
3175 RFileDecoded::Audio(_) => Self::Audio,
3176 RFileDecoded::BMD(_) => Self::BMD,
3177 RFileDecoded::BMDVegetation(_) => Self::BMDVegetation,
3178 RFileDecoded::Dat(_) => Self::Dat,
3179 RFileDecoded::DB(_) => Self::DB,
3180 RFileDecoded::ESF(_) => Self::ESF,
3181 RFileDecoded::Font(_) => Self::Font,
3182 RFileDecoded::HlslCompiled(_) => Self::HlslCompiled,
3183 RFileDecoded::GroupFormations(_) => Self::GroupFormations,
3184 RFileDecoded::Image(_) => Self::Image,
3185 RFileDecoded::Loc(_) => Self::Loc,
3186 RFileDecoded::MatchedCombat(_) => Self::MatchedCombat,
3187 RFileDecoded::Pack(_) => Self::Pack,
3188 RFileDecoded::PortraitSettings(_) => Self::PortraitSettings,
3189 RFileDecoded::RigidModel(_) => Self::RigidModel,
3190 RFileDecoded::SoundBank(_) => Self::SoundBank,
3191 RFileDecoded::Text(_) => Self::Text,
3192 RFileDecoded::UIC(_) => Self::UIC,
3193 RFileDecoded::UnitVariant(_) => Self::UnitVariant,
3194 RFileDecoded::Unknown(_) => Self::Unknown,
3195 RFileDecoded::Video(_) => Self::Video,
3196 RFileDecoded::VMD(_) => Self::VMD,
3197 RFileDecoded::WSModel(_) => Self::WSModel,
3198 }
3199 }
3200}
3201
3202impl<'a> EncodeableExtraData<'a> {
3203
3204 /// This functions generates an EncodeableExtraData for a specific game.
3205 pub fn new_from_game_info(game_info: &'a GameInfo) -> Self {
3206 let mut extra_data = Self::default();
3207 extra_data.set_game_info(Some(game_info));
3208 extra_data.set_table_has_guid(*game_info.db_tables_have_guid());
3209 extra_data
3210 }
3211
3212 /// This functions generates an EncodeableExtraData for a specific game and settings.
3213 pub fn new_from_game_info_and_settings(game_info: &'a GameInfo, cf: CompressionFormat, disable_regen_table_guid: bool) -> EncodeableExtraData<'a> {
3214 let mut extra_data = EncodeableExtraData::new_from_game_info(game_info);
3215 extra_data.set_regenerate_table_guid(!disable_regen_table_guid);
3216 extra_data.set_compression_format(cf);
3217 extra_data
3218 }
3219}