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