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