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