Skip to main content

rpfm_extensions/diagnostics/
pack.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//! Module with the structs and functions specific for `Pack` diagnostics.
12
13use getset::{Getters, MutGetters};
14use serde_derive::{Serialize, Deserialize};
15
16use std::{fmt, fmt::Display};
17
18use rpfm_lib::files::{Container, EncodeableExtraData, pack::Pack, RFile};
19use rpfm_lib::games::GameInfo;
20use rpfm_lib::utils::INVALID_CHARACTERS_WINDOWS;
21
22use crate::diagnostics::*;
23
24
25
26//-------------------------------------------------------------------------------//
27//                              Enums & Structs
28//-------------------------------------------------------------------------------//
29
30/// This struct contains the results of a Pack diagnostic.
31#[derive(Debug, Clone, Default, Getters, MutGetters, Serialize, Deserialize)]
32#[getset(get = "pub", get_mut = "pub")]
33pub struct PackDiagnostic {
34    pack: String,
35    results: Vec<PackDiagnosticReport>
36}
37
38/// This struct defines an individual pack diagnostic result.
39#[derive(Debug, Clone, Getters, MutGetters, Serialize, Deserialize)]
40#[getset(get = "pub", get_mut = "pub")]
41pub struct PackDiagnosticReport {
42    report_type: PackDiagnosticReportType,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum PackDiagnosticReportType {
47    InvalidPackName(String),
48    InvalidFileName(String, String),
49    MissingLocDataFileDetected(String),
50    FileITM(String),
51    FileOverwrite(String),
52    FileDuplicated(String),
53}
54
55//-------------------------------------------------------------------------------//
56//                             Implementations
57//-------------------------------------------------------------------------------//
58
59impl PackDiagnosticReport {
60    pub fn new(report_type: PackDiagnosticReportType) -> Self {
61        Self {
62            report_type
63        }
64    }
65}
66
67impl DiagnosticReport for PackDiagnosticReport {
68    fn message(&self) -> String {
69        match &self.report_type {
70            PackDiagnosticReportType::InvalidPackName(pack_name) => format!("Invalid Pack name: {pack_name}"),
71            PackDiagnosticReportType::InvalidFileName(pack_name, file_name) => format!("Invalid file name ({file_name}) in pack: {pack_name}. This file will be renamed when extracting/exporting, which may cause issues when importing back, especially with MyMods."),
72            PackDiagnosticReportType::MissingLocDataFileDetected(pack_name) => format!("Missing Loc Data file in Pack: {pack_name}"),
73            PackDiagnosticReportType::FileITM(path) => format!("File identical to parent/vanilla file: {path}"),
74            PackDiagnosticReportType::FileOverwrite(path) => format!("File overwriting a parent/vanilla file: {path}"),
75            PackDiagnosticReportType::FileDuplicated(path) => format!("File duplicated: {path}"),
76        }
77    }
78
79    fn level(&self) -> DiagnosticLevel {
80        match self.report_type {
81            PackDiagnosticReportType::InvalidPackName(_) => DiagnosticLevel::Error,
82            PackDiagnosticReportType::InvalidFileName(_,_) => DiagnosticLevel::Error,
83            PackDiagnosticReportType::MissingLocDataFileDetected(_) => DiagnosticLevel::Warning,
84            PackDiagnosticReportType::FileITM(_) => DiagnosticLevel::Warning,
85            PackDiagnosticReportType::FileOverwrite(_) => DiagnosticLevel::Info,
86            PackDiagnosticReportType::FileDuplicated(_) => DiagnosticLevel::Warning,
87        }
88    }
89}
90
91impl Display for PackDiagnosticReportType {
92    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93        Display::fmt(match self {
94            Self::InvalidPackName(_) => "InvalidPackFileName",
95            Self::InvalidFileName(_,_) => "InvalidFileName",
96            Self::MissingLocDataFileDetected(_) => "MissingLocDataFileDetected",
97            Self::FileITM(_) => "FileITM",
98            Self::FileOverwrite(_) => "FileOverwrite",
99            Self::FileDuplicated(_) => "FileDuplicated",
100        }, f)
101    }
102}
103
104impl PackDiagnostic {
105
106    /// This function takes care of checking for PackFile-Related for errors.
107    pub fn check(packs: &mut BTreeMap<String, Pack>, dependencies: &mut Dependencies, game: &GameInfo, ca_packs: &HashSet<String>) -> Vec<DiagnosticType> {
108        let mut diagnostics = Vec::new();
109        let extra_data = Some(EncodeableExtraData::new_from_game_info(game));
110        for (key, pack) in packs.iter_mut() {
111            let mut diagnostic = PackDiagnostic { pack: key.clone(), ..Default::default() };
112            let is_ca_pack = ca_packs.contains(key);
113
114            let name = pack.disk_file_name();
115            if name.contains(' ') {
116                let result = PackDiagnosticReport::new(PackDiagnosticReportType::InvalidPackName(name.to_string()));
117                diagnostic.results_mut().push(result);
118            }
119
120            let (existing, new) = pack.missing_locs_paths();
121            if pack.paths().contains_key(&existing) {
122                let result = PackDiagnosticReport::new(PackDiagnosticReportType::MissingLocDataFileDetected(existing));
123                diagnostic.results_mut().push(result);
124            }
125
126            if pack.paths().contains_key(&new) {
127                let result = PackDiagnosticReport::new(PackDiagnosticReportType::MissingLocDataFileDetected(new));
128                diagnostic.results_mut().push(result);
129            }
130
131            let results = pack.paths()
132                .values()
133                .filter_map(|x| if x.len() >= 2 {
134                    Some(x.iter().map(|x| PackDiagnosticReport::new(PackDiagnosticReportType::FileDuplicated(x.to_string()))).collect::<Vec<_>>())
135                } else {
136                    None
137                })
138                .flatten()
139                .collect::<Vec<_>>();
140
141            if !results.is_empty() {
142                diagnostic.results_mut().extend(results);
143            }
144
145            let invalid_file_names = pack.paths().par_iter()
146                .map(|(path, real_paths)| (path, path.split("/"), real_paths))
147                .filter(|(_, split, _)| {
148                    let filename = split.clone().last().unwrap_or_default();
149                    let has_invalid_chars = filename.chars().any(|c| INVALID_CHARACTERS_WINDOWS.contains(&c));
150                    let has_whitespace_issues = filename.starts_with(' ') || filename.ends_with(' ');
151                    let is_only_dots = !filename.is_empty() && filename.chars().all(|c| c == '.');
152
153                    has_invalid_chars || has_whitespace_issues || is_only_dots
154                })
155                .filter_map(|(_, _, real_paths)| real_paths.first())
156                .collect::<Vec<_>>();
157
158            for path in invalid_file_names {
159                let result = PackDiagnosticReport::new(PackDiagnosticReportType::InvalidFileName(name.to_string(), path.to_string()));
160                diagnostic.results_mut().push(result);
161            }
162
163            // ITM / overwrite pass. The expensive bit is `data_hash` (encode /
164            // disk read), so we collect disjoint `&mut RFile` pairs and parallelise.
165            let candidate_paths: HashSet<String> = pack.files().keys()
166                .filter(|k| dependencies.file(k, true, true, false).is_ok())
167                .cloned()
168                .collect();
169
170            if !candidate_paths.is_empty() {
171
172                // One-shot batch: `Dependencies::file_mut` in a loop would
173                // reborrow `&mut self` each call and invalidate prior refs.
174                let mut dep_files = dependencies.files_mut_by_paths(&candidate_paths, true, true);
175
176                // `HashMap::remove` moves the `&mut RFile` out so each pair
177                // owns two disjoint refs, safe to send across rayon workers.
178                let pairs: Vec<(&mut RFile, &mut RFile)> = pack.files_mut().iter_mut()
179                    .filter_map(|(k, pack_rfile)| {
180                        dep_files.remove(k).map(|dep_rfile| (pack_rfile, dep_rfile))
181                    })
182                    .collect();
183
184                // Parallel hash + compare. Each worker owns a disjoint pair so
185                // the two `data_hash` calls can run concurrently with the rest.
186                let reports: Vec<PackDiagnosticReport> = if is_ca_pack {
187                    return vec![];
188                } else {
189                    pairs.into_par_iter()
190                        .filter_map(|(rfile, dep_file)| {
191                            let local_hash = rfile.data_hash(&extra_data).ok()?;
192                            let dependency_hash = dep_file.data_hash(&extra_data).ok()?;
193                            let path = rfile.path_in_container_raw().to_string();
194                            let report_type = if local_hash == dependency_hash {
195                                PackDiagnosticReportType::FileITM(path)
196                            } else {
197                                PackDiagnosticReportType::FileOverwrite(path)
198                            };
199                            Some(PackDiagnosticReport::new(report_type))
200                        })
201                        .collect()
202                };
203
204                diagnostic.results_mut().extend(reports);
205            }
206
207            if !diagnostic.results().is_empty() {
208                diagnostics.push(DiagnosticType::Pack(diagnostic));
209            }
210        }
211
212        diagnostics
213    }
214
215}