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