rpfm_extensions/diagnostics/
pack.rs1use 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#[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#[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
56impl 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 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 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 let mut dep_files = dependencies.files_mut_by_paths(&candidate_paths, true, true);
177
178 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 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}