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