1use getset::{Getters, MutGetters};
14use itertools::Itertools;
15use serde_derive::{Serialize, Deserialize};
16
17use std::{fmt, fmt::Display};
18
19use rpfm_lib::files::pack::DiagnosticIgnoreEntry;
20use rpfm_lib::files::table::DecodedData;
21use rpfm_lib::schema::{DefinitionPatch, Field};
22
23use crate::diagnostics::*;
24
25type DbKeyIndex<'a> = HashMap<Vec<&'a DecodedData>, Vec<(Vec<(i32, i32)>, usize)>>;
30
31type LocKeyIndex<'a> = HashMap<&'a DecodedData, Vec<((i32, i32), usize)>>;
35
36#[derive(Debug, Clone, Default, Getters, MutGetters, Serialize, Deserialize)]
42#[getset(get = "pub", get_mut = "pub")]
43pub struct TableDiagnostic {
44 path: String,
45 pack: String,
46 results: Vec<TableDiagnosticReport>
47}
48
49#[derive(Debug, Clone, Getters, MutGetters, Serialize, Deserialize)]
51#[getset(get = "pub", get_mut = "pub")]
52pub struct TableDiagnosticReport {
53
54 cells_affected: Vec<(i32, i32)>,
58
59 column_names: Vec<String>,
61 report_type: TableDiagnosticReportType,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub enum TableDiagnosticReportType {
66 OutdatedTable,
67 InvalidReference(String, String),
68 EmptyRow,
69 EmptyKeyField(String),
70 EmptyKeyFields,
71 DuplicatedCombinedKeys(String),
72 NoReferenceTableFound(String),
73 NoReferenceTableNorColumnFoundPak(String),
74 NoReferenceTableNorColumnFoundNoPak(String),
75 InvalidEscape,
76 DuplicatedRow(String),
77 InvalidLocKey,
78 TableNameEndsInNumber,
79 TableNameHasSpace,
80 TableIsDataCoring,
81 FieldWithPathNotFound(Vec<String>),
82 BannedTable,
83 ValueCannotBeEmpty(String),
84 AlteredTable,
85}
86
87struct TableInfo<'a> {
91 path: &'a str,
92 pack_key: &'a str,
93 fields_processed: Vec<Field>,
94 patches: Option<&'a DefinitionPatch>,
95 key_amount: usize,
96 table_data: Cow<'a, [Vec<DecodedData>]>,
97 default_row: Vec<DecodedData>,
98 ignored_fields: Vec<String>,
99 ignored_diagnostics: HashSet<String>,
100 ignored_diagnostics_for_fields: HashMap<String, Vec<String>>,
101}
102
103impl TableDiagnosticReport {
108 pub fn new(report_type: TableDiagnosticReportType, cells_affected: &[(i32, i32)], fields: &[Field]) -> Self {
109 let mut fields_affected = cells_affected.iter().map(|(_, column)| *column).collect::<Vec<_>>();
110 fields_affected.sort();
111 fields_affected.dedup();
112
113 if fields_affected.contains(&-1) {
114 fields_affected = vec![-1];
115 }
116
117 Self {
118 cells_affected: cells_affected.to_vec(),
119 column_names: fields_affected.iter().flat_map(|index| {
120 if index == &-1 {
121 fields.iter().map(|field| field.name().to_owned()).collect()
122 } else {
123 vec![fields[*index as usize].name().to_owned()]
124 }
125 }).collect(),
126 report_type
127 }
128 }
129}
130
131impl DiagnosticReport for TableDiagnosticReport {
132 fn message(&self) -> String {
133 match &self.report_type {
134 TableDiagnosticReportType::OutdatedTable => "Possibly outdated table".to_owned(),
135 TableDiagnosticReportType::InvalidReference(cell_data, field_name) => format!("Invalid reference \"{cell_data}\" in column \"{field_name}\"."),
136 TableDiagnosticReportType::EmptyRow => "Empty row.".to_owned(),
137 TableDiagnosticReportType::EmptyKeyField(field_name) => format!("Empty key for column \"{field_name}\"."),
138 TableDiagnosticReportType::EmptyKeyFields => "Empty key fields.".to_owned(),
139 TableDiagnosticReportType::DuplicatedCombinedKeys(combined_keys) => format!("Duplicated combined keys: {}.", &combined_keys),
140 TableDiagnosticReportType::NoReferenceTableFound(field_name) => format!("No reference table found for column \"{field_name}\"."),
141 TableDiagnosticReportType::NoReferenceTableNorColumnFoundPak(field_name) => format!("No reference column found in referenced table for column \"{field_name}\". Maybe a problem with the schema?"),
142 TableDiagnosticReportType::NoReferenceTableNorColumnFoundNoPak(field_name) => format!("No reference column found in referenced table for column \"{field_name}\". Did you forget to generate the Dependencies Cache, or did you generate it before installing the Assembly kit?"),
143 TableDiagnosticReportType::InvalidEscape => "Invalid line jump/tabulation detected in loc entry. Use \\\\n or \\\\t instead.".to_owned(),
144 TableDiagnosticReportType::DuplicatedRow(combined_keys) => format!("Duplicated row: {combined_keys}."),
145 TableDiagnosticReportType::InvalidLocKey => "Invalid localisation key.".to_owned(),
146 TableDiagnosticReportType::TableNameEndsInNumber => "Table name ends in number.".to_owned(),
147 TableDiagnosticReportType::TableNameHasSpace => "Table name contains spaces.".to_owned(),
148 TableDiagnosticReportType::TableIsDataCoring => "Table is datacoring.".to_owned(),
149 TableDiagnosticReportType::FieldWithPathNotFound(paths) => format!("Path not found: {}.", paths.iter().join(" || ")),
150 TableDiagnosticReportType::BannedTable => "Banned table.".to_owned(),
151 TableDiagnosticReportType::ValueCannotBeEmpty(field_name) => format!("Empty value for column \"{field_name}\"."),
152 TableDiagnosticReportType::AlteredTable => "Altered Table".to_owned(),
153 }
154 }
155
156 fn level(&self) -> DiagnosticLevel {
157 match self.report_type {
158 TableDiagnosticReportType::OutdatedTable => DiagnosticLevel::Error,
159 TableDiagnosticReportType::InvalidReference(_,_) => DiagnosticLevel::Error,
160 TableDiagnosticReportType::EmptyRow => DiagnosticLevel::Warning,
161 TableDiagnosticReportType::EmptyKeyField(_) => DiagnosticLevel::Error,
162 TableDiagnosticReportType::EmptyKeyFields => DiagnosticLevel::Warning,
163 TableDiagnosticReportType::DuplicatedCombinedKeys(_) => DiagnosticLevel::Warning,
164 TableDiagnosticReportType::NoReferenceTableFound(_) => DiagnosticLevel::Info,
165 TableDiagnosticReportType::NoReferenceTableNorColumnFoundPak(_) => DiagnosticLevel::Info,
166 TableDiagnosticReportType::NoReferenceTableNorColumnFoundNoPak(_) => DiagnosticLevel::Warning,
167 TableDiagnosticReportType::InvalidEscape => DiagnosticLevel::Warning,
168 TableDiagnosticReportType::DuplicatedRow(_) => DiagnosticLevel::Warning,
169 TableDiagnosticReportType::InvalidLocKey => DiagnosticLevel::Error,
170 TableDiagnosticReportType::TableNameEndsInNumber => DiagnosticLevel::Error,
171 TableDiagnosticReportType::TableNameHasSpace => DiagnosticLevel::Error,
172 TableDiagnosticReportType::TableIsDataCoring => DiagnosticLevel::Warning,
173 TableDiagnosticReportType::FieldWithPathNotFound(_) => DiagnosticLevel::Warning,
174 TableDiagnosticReportType::BannedTable => DiagnosticLevel::Error,
175 TableDiagnosticReportType::ValueCannotBeEmpty(_) => DiagnosticLevel::Error,
176 TableDiagnosticReportType::AlteredTable => DiagnosticLevel::Error,
177 }
178 }
179}
180
181impl Display for TableDiagnosticReportType {
182 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
183 Display::fmt(match self {
184 Self::OutdatedTable => "OutdatedTable",
185 Self::InvalidReference(_,_) => "InvalidReference",
186 Self::EmptyRow => "EmptyRow",
187 Self::EmptyKeyField(_) => "EmptyKeyField",
188 Self::EmptyKeyFields => "EmptyKeyFields",
189 Self::DuplicatedCombinedKeys(_) => "DuplicatedCombinedKeys",
190 Self::NoReferenceTableFound(_) => "NoReferenceTableFound",
191 Self::NoReferenceTableNorColumnFoundPak(_) => "NoReferenceTableNorColumnFoundPak",
192 Self::NoReferenceTableNorColumnFoundNoPak(_) => "NoReferenceTableNorColumnFoundNoPak",
193 Self::InvalidEscape => "InvalidEscape",
194 Self::DuplicatedRow(_) => "DuplicatedRow",
195 Self::InvalidLocKey => "InvalidLocKey",
196 Self::TableNameEndsInNumber => "TableNameEndsInNumber",
197 Self::TableNameHasSpace => "TableNameHasSpace",
198 Self::TableIsDataCoring => "TableIsDataCoring",
199 Self::FieldWithPathNotFound(_) => "FieldWithPathNotFound",
200 Self::BannedTable => "BannedTable",
201 Self::ValueCannotBeEmpty(_) => "ValueCannotBeEmpty",
202 Self::AlteredTable => "AlteredTable",
203 }, f)
204 }
205}
206
207
208impl TableDiagnostic {
209 pub fn new(path: &str, pack: &str) -> Self {
210 Self {
211 path: path.to_owned(),
212 pack: pack.to_owned(),
213 results: vec![],
214 }
215 }
216
217 fn is_table_outdated(table_name: &str, table_version: i32, dependencies: &Dependencies) -> bool {
219 if let Ok(vanilla_dbs) = dependencies.db_data(table_name, true, false) {
220 if let Some(max_version) = vanilla_dbs.iter()
221 .filter_map(|x| {
222 if let Ok(RFileDecoded::DB(table)) = x.decoded() {
223 Some(table.definition().version())
224 } else {
225 None
226 }
227 }).max_by(|x, y| x.cmp(y)) {
228 if *max_version != table_version {
229 return true
230 }
231 }
232 }
233
234 false
235 }
236
237 #[allow(clippy::too_many_arguments)]
239 pub fn check_db(
240 files: &[(&str, &RFile)],
241 dependencies: &Dependencies,
242 global_ignored_diagnostics: &[String],
243 game_info: &GameInfo,
244 local_path_list: &HashMap<String, Vec<String>>,
245 check_ak_only_refs: bool,
246 files_to_ignore: &Option<Vec<DiagnosticIgnoreEntry>>,
247 packs: &BTreeMap<String, Pack>,
248 schema: &Schema,
249 loc_data: &Option<HashMap<Cow<str>, Cow<str>>>
250 ) -> Vec<DiagnosticType> {
251 let mut diagnostics = vec![];
252
253 if files.is_empty() {
254 return diagnostics;
255 }
256
257 let file = files.first().and_then(|(_, x)| x.decoded().ok());
259 let dependency_data = if let Some(RFileDecoded::DB(table)) = file {
260 dependencies.db_reference_data(schema, packs, table.table_name(), table.definition(), loc_data)
261 } else {
262 return diagnostics
263 };
264
265 let mut table_infos = vec![];
268 for (pack_key, file) in files {
269 let (ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields) = Diagnostics::ignore_data_for_file(file, files_to_ignore).unwrap_or_default();
270 if let Ok(RFileDecoded::DB(table)) = file.decoded() {
271 let fields_processed = table.definition().fields_processed();
272 let patches = Some(table.definition().patches());
273 let table_data = table.data();
274
275 table_infos.push(TableInfo {
276 path: file.path_in_container_raw(),
277 pack_key,
278 key_amount: fields_processed.iter().filter(|field| field.is_key(patches)).count(),
279 fields_processed,
280 patches,
281 table_data,
282 default_row: table.new_row(),
283 ignored_fields,
284 ignored_diagnostics,
285 ignored_diagnostics_for_fields
286 });
287 }
288 }
289
290 let mut global_keys: DbKeyIndex = HashMap::with_capacity(table_infos.iter().map(|x| x.table_data.len()).sum());
291 let dec_files = files.iter()
292 .filter_map(|(_, x)| match x.decoded().ok() {
293 Some(RFileDecoded::DB(ref table)) => Some((table, *x)),
294 _ => None,
295 })
296 .collect::<Vec<_>>();
297
298 for (index, (table, file)) in dec_files.iter().enumerate() {
299 let is_twad_key_deletes = table.table_name().starts_with("twad_key_deletes");
300 let check_ak_only = check_ak_only_refs || table.table_name().starts_with("start_pos_");
301 if let Some(table_info) = table_infos.get(index) {
302 let mut diagnostic = TableDiagnostic::new(table_info.path, table_info.pack_key);
303
304 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("OutdatedTable"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && Self::is_table_outdated(table.table_name(), *table.definition().version(), dependencies) {
306 let result = TableDiagnosticReport::new(TableDiagnosticReportType::OutdatedTable, &[], &[]);
307 diagnostic.results_mut().push(result);
308 }
309
310 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("BannedTable"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && game_info.is_file_banned(file.path_in_container_raw()) {
312 let result = TableDiagnosticReport::new(TableDiagnosticReportType::BannedTable, &[], &[]);
313 diagnostic.results_mut().push(result);
314 }
315
316 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("AlteredTable"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && table.altered() {
317 let result = TableDiagnosticReport::new(TableDiagnosticReportType::AlteredTable, &[], &[]);
318 diagnostic.results_mut().push(result);
319 }
320
321 if let Some(name) = file.file_name() {
323 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("TableNameEndsInNumber"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && (name.ends_with('0') ||
324 name.ends_with('1') ||
325 name.ends_with('2') ||
326 name.ends_with('3') ||
327 name.ends_with('4') ||
328 name.ends_with('5') ||
329 name.ends_with('6') ||
330 name.ends_with('7') ||
331 name.ends_with('8') || name.ends_with('9')) {
332
333 let result = TableDiagnosticReport::new(TableDiagnosticReportType::TableNameEndsInNumber, &[], &[]);
334 diagnostic.results_mut().push(result);
335 }
336
337 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("TableNameHasSpace"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && name.contains(' ') {
338 let result = TableDiagnosticReport::new(TableDiagnosticReportType::TableNameHasSpace, &[], &[]);
339 diagnostic.results_mut().push(result);
340 }
341
342 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("TableIsDataCoring"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) {
343 match game_info.vanilla_db_table_name_logic() {
344 VanillaDBTableNameLogic::FolderName => {
345 if table.table_name_without_tables() == file.path_in_container_split()[2] {
346 let result = TableDiagnosticReport::new(TableDiagnosticReportType::TableIsDataCoring, &[], &[]);
347 diagnostic.results_mut().push(result);
348 }
349 }
350
351 VanillaDBTableNameLogic::DefaultName(ref default_name) => {
352 if name == default_name {
353 let result = TableDiagnosticReport::new(TableDiagnosticReportType::TableIsDataCoring, &[], &[]);
354 diagnostic.results_mut().push(result);
355 }
356 }
357 }
358 }
359 }
360
361 let mut ignore_path_columns = vec![];
363 for (column, field) in table_info.fields_processed.iter().enumerate() {
364 if let Some(rel_paths) = field.filename_relative_path(table_info.patches) {
365 if rel_paths.iter().any(|path| path.contains('*')) {
366 ignore_path_columns.push(column);
367 }
368 }
369 }
370
371 let mut no_ref_table_nor_column_found_marked = HashSet::new();
372 let mut no_ref_table_found_marked = HashSet::new();
373
374 for (row, cells) in table_info.table_data.iter().enumerate() {
375 let mut row_keys_are_empty = true;
376 let mut row_keys: BTreeMap<i32, &DecodedData> = BTreeMap::new();
377 for (column, field) in table_info.fields_processed.iter().enumerate() {
378
379 let cell_data = cells[column].data_to_string();
385
386 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field.name()), Some("FieldWithPathNotFound"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) &&
388 !cell_data.is_empty() &&
389 cell_data != "." &&
390 cell_data != "x" &&
391 cell_data != "false" &&
392 cell_data != "building_placeholder" &&
393 cell_data != "placeholder" &&
394 cell_data != "PLACEHOLDER" &&
395 cell_data != "placeholder.png" &&
396 cell_data != "placehoder.png" &&
397 table_info.fields_processed[column].is_filename(table_info.patches) &&
398 !ignore_path_columns.contains(&column) {
399
400 let mut path_found = false;
401 let relative_paths = table_info.fields_processed[column].filename_relative_path(table_info.patches);
402 let paths = if let Some(relative_paths) = relative_paths {
403 relative_paths.iter()
404 .flat_map(|x| {
405 let mut paths = vec![];
406 let cell_data = cell_data.replace('\\', "/");
407 for cell_data in cell_data.split(',') {
408
409 let mut start_offset = 0;
411 if cell_data.starts_with("/") {
412 start_offset += 1;
413 }
414 if cell_data.starts_with("data/") {
415 start_offset += 5;
416 }
417
418 paths.push(x.replace('%', &cell_data[start_offset..]));
419 }
420
421 paths
422 })
423 .collect::<Vec<_>>()
424 } else {
425 let mut paths = vec![];
426 let cell_data = cell_data.replace('\\', "/");
427 for cell_data in cell_data.split(',') {
428
429 let mut start_offset = 0;
431 if cell_data.starts_with("/") {
432 start_offset += 1;
433 }
434 if cell_data.starts_with("data/") {
435 start_offset += 5;
436 }
437
438 paths.push(cell_data[start_offset..].to_string());
439 }
440
441 paths
442 };
443
444 for path in &paths {
445 if !path_found && local_path_list.get(&path.to_lowercase()).is_some() {
446 path_found = true;
447 }
448
449 if !path_found && dependencies.file_exists(path, true, true, true) {
450 path_found = true;
451 }
452
453 if path_found {
454 break;
455 }
456 }
457
458 if !path_found {
459 let result = TableDiagnosticReport::new(TableDiagnosticReportType::FieldWithPathNotFound(paths), &[(row as i32, column as i32)], &table_info.fields_processed);
460 diagnostic.results_mut().push(result);
461 }
462 }
463
464 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field.name()), None, &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) &&
466 (field.is_reference(table_info.patches).is_some() ||
467 (
468 is_twad_key_deletes &&
469 field.name() == "table_name"
470 )
471 ) {
472
473 match dependency_data.get(&(column as i32)) {
474 Some(ref_data) => {
475 if *ref_data.referenced_column_is_localised() {
476 }
478 else if ref_data.data().is_empty() && (no_ref_table_nor_column_found_marked.is_empty() || !no_ref_table_nor_column_found_marked.contains(&column)) {
488 if !dependencies.is_asskit_data_loaded() {
489 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("NoReferenceTableNorColumnFoundNoPak"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) {
490 let field_name = table_info.fields_processed[column].name().to_string();
491 let result = TableDiagnosticReport::new(TableDiagnosticReportType::NoReferenceTableNorColumnFoundNoPak(field_name), &[(-1, column as i32)], &table_info.fields_processed);
492 diagnostic.results_mut().push(result);
493 }
494 }
495 else if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("NoReferenceTableNorColumnFoundPak"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) {
496 let field_name = table_info.fields_processed[column].name().to_string();
497 let result = TableDiagnosticReport::new(TableDiagnosticReportType::NoReferenceTableNorColumnFoundPak(field_name), &[(-1, column as i32)], &table_info.fields_processed);
498 diagnostic.results_mut().push(result);
499 }
500
501 no_ref_table_nor_column_found_marked.insert(column);
502 }
503
504 else if !ref_data.data().is_empty() && !cell_data.is_empty() && !ref_data.data().contains_key(&*cell_data) && (!*ref_data.referenced_table_is_ak_only() || check_ak_only) {
506
507 let is_number = *field.field_type() == FieldType::I32 || *field.field_type() == FieldType::I64 || *field.field_type() == FieldType::OptionalI32 || *field.field_type() == FieldType::OptionalI64;
509 let is_valid_reference = if is_number { cell_data != "0" } else { true };
510 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field.name()), Some("InvalidReference"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && is_valid_reference {
511 let result = TableDiagnosticReport::new(TableDiagnosticReportType::InvalidReference(cell_data.to_string(), field.name().to_string()), &[(row as i32, column as i32)], &table_info.fields_processed);
512 diagnostic.results_mut().push(result);
513 }
514 }
515 }
516 None => {
517
518 if no_ref_table_found_marked.is_empty() || !no_ref_table_found_marked.contains(&column) {
520 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("NoReferenceTableFound"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) {
521 let field_name = table_info.fields_processed[column].name().to_string();
522 let result = TableDiagnosticReport::new(TableDiagnosticReportType::NoReferenceTableFound(field_name), &[(-1, column as i32)], &table_info.fields_processed);
523 diagnostic.results_mut().push(result);
524 }
525 no_ref_table_found_marked.insert(column);
526 }
527 }
528 }
529 }
530
531 if row_keys_are_empty && field.is_key(table_info.patches) && (!cell_data.is_empty() && cell_data != "false") {
532 row_keys_are_empty = false;
533 }
534
535 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field.name()), Some("EmptyKeyField"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && field.is_key(table_info.patches) && table_info.key_amount == 1 && *field.field_type() != FieldType::OptionalStringU8 && *field.field_type() != FieldType::Boolean && (cell_data.is_empty() || cell_data == "false") {
536 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyKeyField(field.name().to_string()), &[(row as i32, column as i32)], &table_info.fields_processed);
537 diagnostic.results_mut().push(result);
538 }
539
540 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field.name()), Some("ValueCannotBeEmpty"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && cell_data.is_empty() && field.cannot_be_empty(table_info.patches) {
541 let result = TableDiagnosticReport::new(TableDiagnosticReportType::ValueCannotBeEmpty(field.name().to_string()), &[(row as i32, column as i32)], &table_info.fields_processed);
542 diagnostic.results_mut().push(result);
543 }
544
545 if field.is_key(table_info.patches) {
546 row_keys.insert(column as i32, &cells[column]);
547 }
548 }
549
550 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("EmptyRow"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && cells == &table_info.default_row {
551 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyRow, &[(row as i32, -1)], &table_info.fields_processed);
552 diagnostic.results_mut().push(result);
553 }
554
555 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("EmptyKeyFields"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && row_keys_are_empty && table_info.key_amount > 1 {
556 let cells_affected = row_keys.keys().map(|column| (row as i32, *column)).collect::<Vec<(i32, i32)>>();
557 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyKeyFields, &cells_affected, &table_info.fields_processed);
558 diagnostic.results_mut().push(result);
559 }
560
561 let keys = row_keys.values().copied().collect::<Vec<_>>();
562 let values = (row_keys.keys().map(|column| (row as i32, *column)).collect::<Vec<(i32, i32)>>(), index);
563 if !keys.is_empty() {
564 match global_keys.get_mut(&keys) {
565 Some(val) => val.push(values),
566 None => { global_keys.insert(keys, vec![values]); },
567 }
568 }
569 }
570
571 if !diagnostic.results().is_empty() {
572 diagnostics.push(DiagnosticType::DB(diagnostic));
573 }
574 }
575 }
576
577 global_keys.iter()
581 .filter(|(_, val)| val.len() > 1)
582 .for_each(|(key, val)| {
583 for (pos, index) in val {
584 if let Some(table_info) = table_infos.get(*index) {
585 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics,
586 None,
587 Some("DuplicatedCombinedKeys"),
588 &table_info.ignored_fields,
589 &table_info.ignored_diagnostics,
590 &table_info.ignored_diagnostics_for_fields
591 ) {
592
593 match diagnostics.iter_mut().find(|x| x.path() == table_info.path) {
594 Some(diag) => if let DiagnosticType::DB(ref mut diag) = diag {
595 diag.results_mut().push(
596 TableDiagnosticReport::new(
597 TableDiagnosticReportType::DuplicatedCombinedKeys(
598 key.iter().map(|x| x.data_to_string()).join("| |")
599 ),
600 pos,
601 &table_info.fields_processed
602 )
603 )
604 }
605 None => {
606 let mut diag = TableDiagnostic::new(table_info.path, table_info.pack_key);
607 diag.results_mut().push(
608 TableDiagnosticReport::new(
609 TableDiagnosticReportType::DuplicatedCombinedKeys(
610 key.iter().map(|x| x.data_to_string()).join("| |")
611 ),
612 pos,
613 &table_info.fields_processed
614 )
615 );
616
617 diagnostics.push(DiagnosticType::DB(diag));
619 }
620 }
621 }
622 }
623 }
624 });
625
626 diagnostics
627 }
628
629 pub fn check_loc(
631 files: &[(&str, &RFile)],
632 global_ignored_diagnostics: &[String],
633 files_to_ignore: &Option<Vec<DiagnosticIgnoreEntry>>
634 ) -> Vec<DiagnosticType> {
635 let mut diagnostics = vec![];
636
637 let mut table_infos = vec![];
640 for (pack_key, file) in files {
641 let (ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields) = Diagnostics::ignore_data_for_file(file, files_to_ignore).unwrap_or_default();
642 if let Ok(RFileDecoded::Loc(table)) = file.decoded() {
643 let fields_processed = table.definition().fields_processed();
644 let patches = Some(table.definition().patches());
645 let table_data = table.data();
646
647 table_infos.push(TableInfo {
648 path: file.path_in_container_raw(),
649 pack_key,
650 key_amount: fields_processed.iter().filter(|field| field.is_key(patches)).count(),
651 fields_processed,
652 patches,
653 table_data,
654 default_row: table.new_row(),
655 ignored_fields,
656 ignored_diagnostics,
657 ignored_diagnostics_for_fields
658 });
659 }
660 }
661
662 let dec_files = files.iter()
663 .filter_map(|(_, x)| match x.decoded().ok() {
664 Some(RFileDecoded::Loc(ref table)) => Some((table, *x)),
665 _ => None,
666 })
667 .collect::<Vec<_>>();
668
669 let mut global_keys: LocKeyIndex = HashMap::with_capacity(table_infos.iter().map(|x| x.table_data.len()).sum());
670
671 for (index, (table, _file)) in dec_files.iter().enumerate() {
672 if let Some(table_info) = table_infos.get(index) {
673 let mut diagnostic = TableDiagnostic::new(table_info.path, table_info.pack_key);
674 let fields = table.definition().fields_processed();
675 let field_key_name = fields[0].name();
676 let field_text_name = fields[1].name();
677
678 for (row, cells) in table_info.table_data.iter().enumerate() {
679 let key = cells[0].data_to_string();
680 let data = cells[1].data_to_string();
681 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field_key_name), Some("InvalidLocKey"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && !key.is_empty() && (key.contains('\n') || key.contains('\r') || key.contains('\t')) {
682 let result = TableDiagnosticReport::new(TableDiagnosticReportType::InvalidLocKey, &[(row as i32, 0)], &fields);
683 diagnostic.results_mut().push(result);
684 }
685
686 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field_key_name), Some("EmptyRow"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field_text_name), Some("EmptyRow"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && key.is_empty() && data.is_empty() {
688 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyRow, &[(row as i32, -1)], &fields);
689 diagnostic.results_mut().push(result);
690 }
691
692 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field_key_name), Some("EmptyKeyField"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && key.is_empty() && !data.is_empty() {
693 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyKeyField("Key".to_string()), &[(row as i32, 0)], &fields);
694 diagnostic.results_mut().push(result);
695 }
696
697 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field_text_name), Some("InvalidEscape"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) &&
699 !data.is_empty() &&
700 (
701
702 (data.contains("\r") || (data.match_indices("\\r").count() != data.match_indices("\\\\r").count())) ||
704 (data.contains("\n") || (data.match_indices("\\n").count() != data.match_indices("\\\\n").count())) ||
705 (data.contains("\t") || (data.match_indices("\\t").count() != data.match_indices("\\\\t").count()))
706 ) {
707 let result = TableDiagnosticReport::new(TableDiagnosticReportType::InvalidEscape, &[(row as i32, 1)], &fields);
708 diagnostic.results_mut().push(result);
709 }
710
711 match global_keys.get_mut(&cells[0]) {
712 Some(val) => val.push(((row as i32, 0i32), index)),
713 None => { global_keys.insert(&cells[0], vec![((row as i32, 0i32), index)]); },
714 }
715 }
716
717
718 if !diagnostic.results().is_empty() {
719 diagnostics.push(DiagnosticType::Loc(diagnostic));
720 }
721 }
722 }
723
724 global_keys.iter()
728 .filter(|(_, val)| val.len() > 1)
729 .for_each(|(key, val)| {
730 for (pos, index) in val {
731 if let Some(table_info) = table_infos.get(*index) {
732 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics,
733 None,
734 Some("DuplicatedCombinedKeys"),
735 &table_info.ignored_fields,
736 &table_info.ignored_diagnostics,
737 &table_info.ignored_diagnostics_for_fields
738 ) {
739
740 match diagnostics.iter_mut().find(|x| x.path() == table_info.path) {
741 Some(diag) => if let DiagnosticType::Loc(ref mut diag) = diag {
742 diag.results_mut().push(
743 TableDiagnosticReport::new(
744 TableDiagnosticReportType::DuplicatedCombinedKeys(
745 key.data_to_string().to_string()
746 ),
747 &[*pos],
748 &table_info.fields_processed
749 )
750 )
751 }
752 None => {
753 let mut diag = TableDiagnostic::new(table_info.path, table_info.pack_key);
754 diag.results_mut().push(
755 TableDiagnosticReport::new(
756 TableDiagnosticReportType::DuplicatedCombinedKeys(
757 key.data_to_string().to_string()
758 ),
759 &[*pos],
760 &table_info.fields_processed
761 )
762 );
763
764 diagnostics.push(DiagnosticType::Loc(diag));
766 }
767 }
768 }
769
770 }
771 }
772
773 let mut values = Vec::with_capacity(val.len());
774 for (pos, index) in val {
775 if let Some(table_info) = table_infos.get(*index) {
776 values.push((&table_info.table_data[pos.0 as usize][1], pos.0, index));
777 }
778 }
779
780 values.sort_unstable_by_key(|x| x.0.data_to_string());
781 let dups = values.iter().duplicates_by(|x| x.0);
782 let poss = values.iter().positions(|x| dups.clone().any(|y| y.0 == x.0));
783
784 for pos in poss {
785 if let Some((data, row, index)) = values.get(pos) {
786 if let Some(table_info) = table_infos.get(**index) {
787 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics,
788 None,
789 Some("DuplicatedRow"),
790 &table_info.ignored_fields,
791 &table_info.ignored_diagnostics,
792 &table_info.ignored_diagnostics_for_fields
793 ) {
794
795 match diagnostics.iter_mut().find(|x| x.path() == table_info.path) {
796 Some(diag) => if let DiagnosticType::Loc(ref mut diag) = diag {
797 diag.results_mut().push(
798 TableDiagnosticReport::new(
799 TableDiagnosticReportType::DuplicatedRow(
800 String::from(table_info.table_data[*row as usize][0].data_to_string()) + "| |" + &data.data_to_string()
801 ),
802 &[(*row, 0), (*row, 1)],
803 &table_info.fields_processed
804 )
805 )
806 }
807 None => {
808 let mut diag = TableDiagnostic::new(table_info.path, table_info.pack_key);
809 diag.results_mut().push(
810 TableDiagnosticReport::new(
811 TableDiagnosticReportType::DuplicatedRow(
812 String::from(table_info.table_data[*row as usize][0].data_to_string()) + "| |" + &data.data_to_string()
813 ),
814 &[(*row, 0), (*row, 1)],
815 &table_info.fields_processed
816 )
817 );
818
819 diagnostics.push(DiagnosticType::Loc(diag));
821 }
822 }
823 }
824 }
825 }
826 }
827 });
828
829 diagnostics
830 }
831}