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