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 InvalidKey,
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::InvalidKey => "Invalid 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::InvalidKey => 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::InvalidKey => "InvalidKey",
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 ca_packs: &HashSet<String>,
251 ) -> Vec<DiagnosticType> {
252 let mut diagnostics = vec![];
253
254 if files.is_empty() {
255 return diagnostics;
256 }
257
258 let file = files.first().and_then(|(_, x)| x.decoded().ok());
260 let dependency_data = if let Some(RFileDecoded::DB(table)) = file {
261 dependencies.db_reference_data(schema, packs, table.table_name(), table.definition(), loc_data)
262 } else {
263 return diagnostics
264 };
265
266 let mut table_infos = vec![];
269 for (pack_key, file) in files {
270 let (ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields) = Diagnostics::ignore_data_for_file(file, files_to_ignore).unwrap_or_default();
271 if let Ok(RFileDecoded::DB(table)) = file.decoded() {
272 let fields_processed = table.definition().fields_processed();
273 let patches = Some(table.definition().patches());
274 let table_data = table.data();
275
276 table_infos.push(TableInfo {
277 path: file.path_in_container_raw(),
278 pack_key,
279 key_amount: fields_processed.iter().filter(|field| field.is_key(patches)).count(),
280 fields_processed,
281 patches,
282 table_data,
283 default_row: table.new_row(),
284 ignored_fields,
285 ignored_diagnostics,
286 ignored_diagnostics_for_fields
287 });
288 }
289 }
290
291 let mut global_keys: DbKeyIndex = HashMap::with_capacity(table_infos.iter().map(|x| x.table_data.len()).sum());
292 let dec_files = files.iter()
293 .filter_map(|(_, x)| match x.decoded().ok() {
294 Some(RFileDecoded::DB(ref table)) => Some((table, *x)),
295 _ => None,
296 })
297 .collect::<Vec<_>>();
298
299 for (index, (table, file)) in dec_files.iter().enumerate() {
300 let is_twad_key_deletes = table.table_name().starts_with("twad_key_deletes");
301 let check_ak_only = check_ak_only_refs || table.table_name().starts_with("start_pos_");
302 if let Some(table_info) = table_infos.get(index) {
303 let mut diagnostic = TableDiagnostic::new(table_info.path, table_info.pack_key);
304
305 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) {
307 let result = TableDiagnosticReport::new(TableDiagnosticReportType::OutdatedTable, &[], &[]);
308 diagnostic.results_mut().push(result);
309 }
310
311 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()) {
313 let result = TableDiagnosticReport::new(TableDiagnosticReportType::BannedTable, &[], &[]);
314 diagnostic.results_mut().push(result);
315 }
316
317 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() {
318 let result = TableDiagnosticReport::new(TableDiagnosticReportType::AlteredTable, &[], &[]);
319 diagnostic.results_mut().push(result);
320 }
321
322 if let Some(name) = file.file_name() {
324 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') ||
325 name.ends_with('1') ||
326 name.ends_with('2') ||
327 name.ends_with('3') ||
328 name.ends_with('4') ||
329 name.ends_with('5') ||
330 name.ends_with('6') ||
331 name.ends_with('7') ||
332 name.ends_with('8') || name.ends_with('9')) {
333
334 let result = TableDiagnosticReport::new(TableDiagnosticReportType::TableNameEndsInNumber, &[], &[]);
335 diagnostic.results_mut().push(result);
336 }
337
338 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(' ') {
339 let result = TableDiagnosticReport::new(TableDiagnosticReportType::TableNameHasSpace, &[], &[]);
340 diagnostic.results_mut().push(result);
341 }
342
343 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("TableIsDataCoring"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && !ca_packs.contains(table_info.pack_key) {
344 match game_info.vanilla_db_table_name_logic() {
345 VanillaDBTableNameLogic::FolderName => {
346 if table.table_name_without_tables() == file.path_in_container_split()[2] {
347 let result = TableDiagnosticReport::new(TableDiagnosticReportType::TableIsDataCoring, &[], &[]);
348 diagnostic.results_mut().push(result);
349 }
350 }
351
352 VanillaDBTableNameLogic::DefaultName(ref default_name) => {
353 if name == default_name {
354 let result = TableDiagnosticReport::new(TableDiagnosticReportType::TableIsDataCoring, &[], &[]);
355 diagnostic.results_mut().push(result);
356 }
357 }
358 }
359 }
360 }
361
362 let mut ignore_path_columns = vec![];
364 for (column, field) in table_info.fields_processed.iter().enumerate() {
365 if let Some(rel_paths) = field.filename_relative_path(table_info.patches) {
366 if rel_paths.iter().any(|path| path.contains('*')) {
367 ignore_path_columns.push(column);
368 }
369 }
370 }
371
372 let mut no_ref_table_nor_column_found_marked = HashSet::new();
373 let mut no_ref_table_found_marked = HashSet::new();
374
375 for (row, cells) in table_info.table_data.iter().enumerate() {
376 let mut row_keys_are_empty = true;
377 let mut row_keys: BTreeMap<i32, &DecodedData> = BTreeMap::new();
378 for (column, field) in table_info.fields_processed.iter().enumerate() {
379
380 let cell_data = cells[column].data_to_string();
386
387 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) &&
389 !cell_data.is_empty() &&
390 cell_data != "." &&
391 cell_data != "x" &&
392 cell_data != "false" &&
393 cell_data != "building_placeholder" &&
394 cell_data != "placeholder" &&
395 cell_data != "PLACEHOLDER" &&
396 cell_data != "placeholder.png" &&
397 cell_data != "placehoder.png" &&
398 table_info.fields_processed[column].is_filename(table_info.patches) &&
399 !ignore_path_columns.contains(&column) {
400
401 let mut path_found = false;
402 let relative_paths = table_info.fields_processed[column].filename_relative_path(table_info.patches);
403 let paths = if let Some(relative_paths) = relative_paths {
404 relative_paths.iter()
405 .flat_map(|x| {
406 let mut paths = vec![];
407 let cell_data = cell_data.replace('\\', "/");
408 for cell_data in cell_data.split(',') {
409
410 let mut start_offset = 0;
412 if cell_data.starts_with("/") {
413 start_offset += 1;
414 }
415 if cell_data.starts_with("data/") {
416 start_offset += 5;
417 }
418
419 paths.push(x.replace('%', &cell_data[start_offset..]));
420 }
421
422 paths
423 })
424 .collect::<Vec<_>>()
425 } else {
426 let mut paths = vec![];
427 let cell_data = cell_data.replace('\\', "/");
428 for cell_data in cell_data.split(',') {
429
430 let mut start_offset = 0;
432 if cell_data.starts_with("/") {
433 start_offset += 1;
434 }
435 if cell_data.starts_with("data/") {
436 start_offset += 5;
437 }
438
439 paths.push(cell_data[start_offset..].to_string());
440 }
441
442 paths
443 };
444
445 for path in &paths {
446 if !path_found && local_path_list.get(&path.to_lowercase()).is_some() {
447 path_found = true;
448 }
449
450 if !path_found && dependencies.file_exists(path, true, true, true) {
451 path_found = true;
452 }
453
454 if path_found {
455 break;
456 }
457 }
458
459 if !path_found {
460 let result = TableDiagnosticReport::new(TableDiagnosticReportType::FieldWithPathNotFound(paths), &[(row as i32, column as i32)], &table_info.fields_processed);
461 diagnostic.results_mut().push(result);
462 }
463 }
464
465 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) &&
467 (field.is_reference(table_info.patches).is_some() ||
468 (
469 is_twad_key_deletes &&
470 field.name() == "table_name"
471 )
472 ) {
473
474 match dependency_data.get(&(column as i32)) {
475 Some(ref_data) => {
476 if *ref_data.referenced_column_is_localised() {
477 }
479 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)) {
489 if !dependencies.is_asskit_data_loaded() {
490 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("NoReferenceTableNorColumnFoundNoPak"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) {
491 let field_name = table_info.fields_processed[column].name().to_string();
492 let result = TableDiagnosticReport::new(TableDiagnosticReportType::NoReferenceTableNorColumnFoundNoPak(field_name), &[(-1, column as i32)], &table_info.fields_processed);
493 diagnostic.results_mut().push(result);
494 }
495 }
496 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) {
497 let field_name = table_info.fields_processed[column].name().to_string();
498 let result = TableDiagnosticReport::new(TableDiagnosticReportType::NoReferenceTableNorColumnFoundPak(field_name), &[(-1, column as i32)], &table_info.fields_processed);
499 diagnostic.results_mut().push(result);
500 }
501
502 no_ref_table_nor_column_found_marked.insert(column);
503 }
504
505 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) {
507
508 let is_number = *field.field_type() == FieldType::I32 || *field.field_type() == FieldType::I64 || *field.field_type() == FieldType::OptionalI32 || *field.field_type() == FieldType::OptionalI64;
510 let is_valid_reference = if is_number { cell_data != "0" } else { true };
511 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 {
512 let result = TableDiagnosticReport::new(TableDiagnosticReportType::InvalidReference(cell_data.to_string(), field.name().to_string()), &[(row as i32, column as i32)], &table_info.fields_processed);
513 diagnostic.results_mut().push(result);
514 }
515 }
516 }
517 None => {
518
519 if no_ref_table_found_marked.is_empty() || !no_ref_table_found_marked.contains(&column) {
521 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("NoReferenceTableFound"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) {
522 let field_name = table_info.fields_processed[column].name().to_string();
523 let result = TableDiagnosticReport::new(TableDiagnosticReportType::NoReferenceTableFound(field_name), &[(-1, column as i32)], &table_info.fields_processed);
524 diagnostic.results_mut().push(result);
525 }
526 no_ref_table_found_marked.insert(column);
527 }
528 }
529 }
530 }
531
532 if row_keys_are_empty && field.is_key(table_info.patches) && (!cell_data.is_empty() && cell_data != "false") {
533 row_keys_are_empty = false;
534 }
535
536 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") {
537 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyKeyField(field.name().to_string()), &[(row as i32, column as i32)], &table_info.fields_processed);
538 diagnostic.results_mut().push(result);
539 }
540
541 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field.name()), Some("InvalidKey"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && field.is_key(table_info.patches) && !cell_data.is_empty() && (cell_data.ends_with(' ') || cell_data.contains('\n') || cell_data.contains('\r') || cell_data.contains('\t')) {
542 let result = TableDiagnosticReport::new(TableDiagnosticReportType::InvalidKey, &[(row as i32, column as i32)], &table_info.fields_processed);
543 diagnostic.results_mut().push(result);
544 }
545
546 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) {
547 let result = TableDiagnosticReport::new(TableDiagnosticReportType::ValueCannotBeEmpty(field.name().to_string()), &[(row as i32, column as i32)], &table_info.fields_processed);
548 diagnostic.results_mut().push(result);
549 }
550
551 if field.is_key(table_info.patches) {
552 row_keys.insert(column as i32, &cells[column]);
553 }
554 }
555
556 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 {
557 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyRow, &[(row as i32, -1)], &table_info.fields_processed);
558 diagnostic.results_mut().push(result);
559 }
560
561 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 {
562 let cells_affected = row_keys.keys().map(|column| (row as i32, *column)).collect::<Vec<(i32, i32)>>();
563 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyKeyFields, &cells_affected, &table_info.fields_processed);
564 diagnostic.results_mut().push(result);
565 }
566
567 let keys = row_keys.values().copied().collect::<Vec<_>>();
568 let values = (row_keys.keys().map(|column| (row as i32, *column)).collect::<Vec<(i32, i32)>>(), index);
569 if !keys.is_empty() {
570 match global_keys.get_mut(&keys) {
571 Some(val) => val.push(values),
572 None => { global_keys.insert(keys, vec![values]); },
573 }
574 }
575 }
576
577 if !diagnostic.results().is_empty() {
578 diagnostics.push(DiagnosticType::DB(diagnostic));
579 }
580 }
581 }
582
583 global_keys.iter()
587 .filter(|(_, val)| val.len() > 1)
588 .for_each(|(key, val)| {
589 for (pos, index) in val {
590 if let Some(table_info) = table_infos.get(*index) {
591 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics,
592 None,
593 Some("DuplicatedCombinedKeys"),
594 &table_info.ignored_fields,
595 &table_info.ignored_diagnostics,
596 &table_info.ignored_diagnostics_for_fields
597 ) {
598
599 match diagnostics.iter_mut().find(|x| x.path() == table_info.path) {
600 Some(diag) => if let DiagnosticType::DB(ref mut diag) = diag {
601 diag.results_mut().push(
602 TableDiagnosticReport::new(
603 TableDiagnosticReportType::DuplicatedCombinedKeys(
604 key.iter().map(|x| x.data_to_string()).join("| |")
605 ),
606 pos,
607 &table_info.fields_processed
608 )
609 )
610 }
611 None => {
612 let mut diag = TableDiagnostic::new(table_info.path, table_info.pack_key);
613 diag.results_mut().push(
614 TableDiagnosticReport::new(
615 TableDiagnosticReportType::DuplicatedCombinedKeys(
616 key.iter().map(|x| x.data_to_string()).join("| |")
617 ),
618 pos,
619 &table_info.fields_processed
620 )
621 );
622
623 diagnostics.push(DiagnosticType::DB(diag));
625 }
626 }
627 }
628 }
629 }
630 });
631
632 diagnostics
633 }
634
635 pub fn check_loc(
637 files: &[(&str, &RFile)],
638 global_ignored_diagnostics: &[String],
639 files_to_ignore: &Option<Vec<DiagnosticIgnoreEntry>>
640 ) -> Vec<DiagnosticType> {
641 let mut diagnostics = vec![];
642
643 let mut table_infos = vec![];
646 for (pack_key, file) in files {
647 let (ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields) = Diagnostics::ignore_data_for_file(file, files_to_ignore).unwrap_or_default();
648 if let Ok(RFileDecoded::Loc(table)) = file.decoded() {
649 let fields_processed = table.definition().fields_processed();
650 let patches = Some(table.definition().patches());
651 let table_data = table.data();
652
653 table_infos.push(TableInfo {
654 path: file.path_in_container_raw(),
655 pack_key,
656 key_amount: fields_processed.iter().filter(|field| field.is_key(patches)).count(),
657 fields_processed,
658 patches,
659 table_data,
660 default_row: table.new_row(),
661 ignored_fields,
662 ignored_diagnostics,
663 ignored_diagnostics_for_fields
664 });
665 }
666 }
667
668 let dec_files = files.iter()
669 .filter_map(|(_, x)| match x.decoded().ok() {
670 Some(RFileDecoded::Loc(ref table)) => Some((table, *x)),
671 _ => None,
672 })
673 .collect::<Vec<_>>();
674
675 let mut global_keys: LocKeyIndex = HashMap::with_capacity(table_infos.iter().map(|x| x.table_data.len()).sum());
676
677 for (index, (table, _file)) in dec_files.iter().enumerate() {
678 if let Some(table_info) = table_infos.get(index) {
679 let mut diagnostic = TableDiagnostic::new(table_info.path, table_info.pack_key);
680 let fields = table.definition().fields_processed();
681 let field_key_name = fields[0].name();
682 let field_text_name = fields[1].name();
683
684 for (row, cells) in table_info.table_data.iter().enumerate() {
685 let key = cells[0].data_to_string();
686 let data = cells[1].data_to_string();
687 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, Some(field_key_name), Some("InvalidKey"), &table_info.ignored_fields, &table_info.ignored_diagnostics, &table_info.ignored_diagnostics_for_fields) && !key.is_empty() && (key.ends_with(' ') || key.contains('\n') || key.contains('\r') || key.contains('\t')) {
688 let result = TableDiagnosticReport::new(TableDiagnosticReportType::InvalidKey, &[(row as i32, 0)], &fields);
689 diagnostic.results_mut().push(result);
690 }
691
692 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() {
694 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyRow, &[(row as i32, -1)], &fields);
695 diagnostic.results_mut().push(result);
696 }
697
698 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() {
699 let result = TableDiagnosticReport::new(TableDiagnosticReportType::EmptyKeyField("Key".to_string()), &[(row as i32, 0)], &fields);
700 diagnostic.results_mut().push(result);
701 }
702
703 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) &&
705 !data.is_empty() &&
706 (
707
708 (data.contains("\r") || (data.match_indices("\\r").count() != data.match_indices("\\\\r").count())) ||
710 (data.contains("\n") || (data.match_indices("\\n").count() != data.match_indices("\\\\n").count())) ||
711 (data.contains("\t") || (data.match_indices("\\t").count() != data.match_indices("\\\\t").count()))
712 ) {
713 let result = TableDiagnosticReport::new(TableDiagnosticReportType::InvalidEscape, &[(row as i32, 1)], &fields);
714 diagnostic.results_mut().push(result);
715 }
716
717 match global_keys.get_mut(&cells[0]) {
718 Some(val) => val.push(((row as i32, 0i32), index)),
719 None => { global_keys.insert(&cells[0], vec![((row as i32, 0i32), index)]); },
720 }
721 }
722
723
724 if !diagnostic.results().is_empty() {
725 diagnostics.push(DiagnosticType::Loc(diagnostic));
726 }
727 }
728 }
729
730 global_keys.iter()
734 .filter(|(_, val)| val.len() > 1)
735 .for_each(|(key, val)| {
736 for (pos, index) in val {
737 if let Some(table_info) = table_infos.get(*index) {
738 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics,
739 None,
740 Some("DuplicatedCombinedKeys"),
741 &table_info.ignored_fields,
742 &table_info.ignored_diagnostics,
743 &table_info.ignored_diagnostics_for_fields
744 ) {
745
746 match diagnostics.iter_mut().find(|x| x.path() == table_info.path) {
747 Some(diag) => if let DiagnosticType::Loc(ref mut diag) = diag {
748 diag.results_mut().push(
749 TableDiagnosticReport::new(
750 TableDiagnosticReportType::DuplicatedCombinedKeys(
751 key.data_to_string().to_string()
752 ),
753 &[*pos],
754 &table_info.fields_processed
755 )
756 )
757 }
758 None => {
759 let mut diag = TableDiagnostic::new(table_info.path, table_info.pack_key);
760 diag.results_mut().push(
761 TableDiagnosticReport::new(
762 TableDiagnosticReportType::DuplicatedCombinedKeys(
763 key.data_to_string().to_string()
764 ),
765 &[*pos],
766 &table_info.fields_processed
767 )
768 );
769
770 diagnostics.push(DiagnosticType::Loc(diag));
772 }
773 }
774 }
775
776 }
777 }
778
779 let mut values = Vec::with_capacity(val.len());
780 for (pos, index) in val {
781 if let Some(table_info) = table_infos.get(*index) {
782 values.push((&table_info.table_data[pos.0 as usize][1], pos.0, index));
783 }
784 }
785
786 values.sort_unstable_by_key(|x| x.0.data_to_string());
787 let dups = values.iter().duplicates_by(|x| x.0);
788 let poss = values.iter().positions(|x| dups.clone().any(|y| y.0 == x.0));
789
790 for pos in poss {
791 if let Some((data, row, index)) = values.get(pos) {
792 if let Some(table_info) = table_infos.get(**index) {
793 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics,
794 None,
795 Some("DuplicatedRow"),
796 &table_info.ignored_fields,
797 &table_info.ignored_diagnostics,
798 &table_info.ignored_diagnostics_for_fields
799 ) {
800
801 match diagnostics.iter_mut().find(|x| x.path() == table_info.path) {
802 Some(diag) => if let DiagnosticType::Loc(ref mut diag) = diag {
803 diag.results_mut().push(
804 TableDiagnosticReport::new(
805 TableDiagnosticReportType::DuplicatedRow(
806 String::from(table_info.table_data[*row as usize][0].data_to_string()) + "| |" + &data.data_to_string()
807 ),
808 &[(*row, 0), (*row, 1)],
809 &table_info.fields_processed
810 )
811 )
812 }
813 None => {
814 let mut diag = TableDiagnostic::new(table_info.path, table_info.pack_key);
815 diag.results_mut().push(
816 TableDiagnosticReport::new(
817 TableDiagnosticReportType::DuplicatedRow(
818 String::from(table_info.table_data[*row as usize][0].data_to_string()) + "| |" + &data.data_to_string()
819 ),
820 &[(*row, 0), (*row, 1)],
821 &table_info.fields_processed
822 )
823 );
824
825 diagnostics.push(DiagnosticType::Loc(diag));
827 }
828 }
829 }
830 }
831 }
832 }
833 });
834
835 diagnostics
836 }
837}