Skip to main content

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