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::{fmt, fmt::Display};
18
19use rpfm_lib::files::{EncodeableExtraData, pack::Pack, RFile};
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) -> Vec<DiagnosticType> {
108        let mut diagnostics = Vec::new();
109
110        let extra_data = Some(EncodeableExtraData::new_from_game_info(game));
111
112        for (key, pack) in packs.iter_mut() {
113            let mut diagnostic = PackDiagnostic { pack: key.clone(), ..Default::default() };
114
115            let name = pack.disk_file_name();
116            if name.contains(' ') {
117                let result = PackDiagnosticReport::new(PackDiagnosticReportType::InvalidPackName(name.to_string()));
118                diagnostic.results_mut().push(result);
119            }
120
121            let (existing, new) = pack.missing_locs_paths();
122            if pack.paths().contains_key(&existing) {
123                let result = PackDiagnosticReport::new(PackDiagnosticReportType::MissingLocDataFileDetected(existing));
124                diagnostic.results_mut().push(result);
125            }
126
127            if pack.paths().contains_key(&new) {
128                let result = PackDiagnosticReport::new(PackDiagnosticReportType::MissingLocDataFileDetected(new));
129                diagnostic.results_mut().push(result);
130            }
131
132            let results = pack.paths()
133                .values()
134                .filter_map(|x| if x.len() >= 2 {
135                    Some(x.iter().map(|x| PackDiagnosticReport::new(PackDiagnosticReportType::FileDuplicated(x.to_string()))).collect::<Vec<_>>())
136                } else {
137                    None
138                })
139                .flatten()
140                .collect::<Vec<_>>();
141
142            if !results.is_empty() {
143                diagnostic.results_mut().extend(results);
144            }
145
146            let invalid_file_names = pack.paths().par_iter()
147                .map(|(path, real_paths)| (path, path.split("/"), real_paths))
148                .filter(|(_, split, _)| {
149                    let filename = split.clone().last().unwrap_or_default();
150                    let has_invalid_chars = filename.chars().any(|c| INVALID_CHARACTERS_WINDOWS.contains(&c));
151                    let has_whitespace_issues = filename.starts_with(' ') || filename.ends_with(' ');
152                    let is_only_dots = !filename.is_empty() && filename.chars().all(|c| c == '.');
153
154                    has_invalid_chars || has_whitespace_issues || is_only_dots
155                })
156                .filter_map(|(_, _, real_paths)| real_paths.first())
157                .collect::<Vec<_>>();
158
159            for path in invalid_file_names {
160                let result = PackDiagnosticReport::new(PackDiagnosticReportType::InvalidFileName(name.to_string(), path.to_string()));
161                diagnostic.results_mut().push(result);
162            }
163
164            // ITM / overwrite pass. The expensive bit is `data_hash` (encode /
165            // disk read), so we collect disjoint `&mut RFile` pairs and parallelise.
166            let candidate_paths: HashSet<String> = pack.files().keys()
167                .filter(|k| dependencies.file(k, true, true, false).is_ok())
168                .cloned()
169                .collect();
170
171            if !candidate_paths.is_empty() {
172
173                // One-shot batch: `Dependencies::file_mut` in a loop would
174                // reborrow `&mut self` each call and invalidate prior refs.
175                let mut dep_files = dependencies.files_mut_by_paths(&candidate_paths, true, true);
176
177                // `HashMap::remove` moves the `&mut RFile` out so each pair
178                // owns two disjoint refs, safe to send across rayon workers.
179                let pairs: Vec<(&mut RFile, &mut RFile)> = pack.files_mut().iter_mut()
180                    .filter_map(|(k, pack_rfile)| {
181                        dep_files.remove(k).map(|dep_rfile| (pack_rfile, dep_rfile))
182                    })
183                    .collect();
184
185                // Parallel hash + compare. Each worker owns a disjoint pair so
186                // the two `data_hash` calls can run concurrently with the rest.
187                let reports: Vec<PackDiagnosticReport> = pairs.into_par_iter()
188                    .filter_map(|(rfile, dep_file)| {
189                        let local_hash = rfile.data_hash(&extra_data).ok()?;
190                        let dependency_hash = dep_file.data_hash(&extra_data).ok()?;
191                        let path = rfile.path_in_container_raw().to_string();
192                        let report_type = if local_hash == dependency_hash {
193                            PackDiagnosticReportType::FileITM(path)
194                        } else {
195                            PackDiagnosticReportType::FileOverwrite(path)
196                        };
197                        Some(PackDiagnosticReport::new(report_type))
198                    })
199                    .collect();
200
201                diagnostic.results_mut().extend(reports);
202            }
203
204            if !diagnostic.results().is_empty() {
205                diagnostics.push(DiagnosticType::Pack(diagnostic));
206            }
207        }
208
209        diagnostics
210    }
211
212}