Skip to main content

rpfm_extensions/search/
table.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11/*!
12Module with all the code related to the `TableMatches`.
13
14This module contains the code needed to get table matches from a `GlobalSearch`.
15!*/
16
17use getset::{Getters, MutGetters, Setters};
18use serde_derive::{Deserialize, Serialize};
19
20use rpfm_lib::files::{db::DB, loc::Loc, table::DecodedData};
21use rpfm_lib::schema::Field;
22
23use super::{find_in_string, MatchingMode, Replaceable, SearchSource, Searchable, replace_match_string};
24
25//-------------------------------------------------------------------------------//
26//                              Enums & Structs
27//-------------------------------------------------------------------------------//
28
29/// This struct represents all the matches of the global search within a table.
30#[derive(Debug, Clone, Eq, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
31#[getset(get = "pub", get_mut = "pub", set = "pub")]
32pub struct TableMatches {
33
34    /// The path of the table.
35    path: String,
36
37    /// The search source that produced these matches.
38    #[serde(default)]
39    source: SearchSource,
40
41    /// The container name (pack file name) this file belongs to.
42    #[serde(default)]
43    container_name: String,
44
45    /// The list of matches within a table.
46    matches: Vec<TableMatch>,
47}
48
49/// This struct represents a match on a row of a Table PackedFile (DB & Loc).
50#[derive(Debug, Clone, Eq, PartialEq, Getters, MutGetters, Serialize, Deserialize)]
51#[getset(get = "pub", get_mut = "pub")]
52pub struct TableMatch {
53
54    // The name of the column where the match is.
55    column_name: String,
56
57    // The logical index of the column where the match is. This should be -1 when the column is hidden.
58    column_number: u32,
59
60    // The row number of this match. This should be -1 when the row is hidden by a filter.
61    row_number: i64,
62
63    /// Byte where the match starts.
64    start: usize,
65
66    /// Byte where the match ends.
67    end: usize,
68
69    // The contents of the matched cell.
70    text: String,
71}
72
73//-------------------------------------------------------------------------------//
74//                             Implementations
75//-------------------------------------------------------------------------------//
76
77impl Searchable for DB {
78    type SearchMatches = TableMatches;
79
80    fn search(&self, file_path: &str, pattern_to_search: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> TableMatches {
81        let mut matches = TableMatches::new(file_path);
82
83        let fields_processed = self.definition().fields_processed();
84
85        for (row_number, row) in self.data().iter().enumerate() {
86            for (column_number, cell) in row.iter().enumerate() {
87                matches.match_decoded_data(&cell.data_to_string(), pattern_to_search, case_sensitive, matching_mode, &fields_processed, column_number as u32, row_number as i64);
88            }
89        }
90
91        matches
92    }
93}
94
95impl Searchable for Loc {
96    type SearchMatches = TableMatches;
97
98    fn search(&self, file_path: &str, pattern_to_search: &str, case_sensitive: bool, matching_mode: &MatchingMode) -> TableMatches {
99        let mut matches = TableMatches::new(file_path);
100
101        let fields_processed = self.definition().fields_processed();
102
103        for (row_number, row) in self.data().iter().enumerate() {
104            for (column_number, cell) in row.iter().enumerate() {
105                matches.match_decoded_data(&cell.data_to_string(), pattern_to_search, case_sensitive, matching_mode, &fields_processed, column_number as u32, row_number as i64);
106            }
107        }
108
109        matches
110    }
111}
112
113impl Replaceable for DB {
114
115    fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &TableMatches) -> bool {
116        let mut edited = false;
117
118        for search_match in search_matches.matches() {
119            if let Some(row) = self.data_mut().get_mut(search_match.row_number as usize) {
120                if let Some(data) = row.get_mut(search_match.column_number as usize) {
121                    edited |= search_match.replace(pattern, replace_pattern, case_sensitive, matching_mode, data);
122                }
123            }
124        }
125
126        edited
127    }
128}
129
130impl Replaceable for Loc {
131
132    fn replace(&mut self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, search_matches: &TableMatches) -> bool {
133        let mut edited = false;
134
135        for search_match in search_matches.matches() {
136            if let Some(row) = self.data_mut().get_mut(search_match.row_number as usize) {
137                if let Some(data) = row.get_mut(search_match.column_number as usize) {
138                    edited |= search_match.replace(pattern, replace_pattern, case_sensitive, matching_mode, data);
139                }
140            }
141        }
142
143        edited
144    }
145}
146
147/// Implementation of `TableMatches`.
148impl TableMatches {
149
150    /// This function creates a new `TableMatches` for the provided path.
151    pub fn new(path: &str) -> Self {
152        Self {
153            path: path.to_owned(),
154            matches: vec![],
155            source: SearchSource::default(),
156            container_name: String::new(),
157        }
158    }
159
160    /// This function check if the provided `&str` matches our search.
161    fn match_decoded_data(
162        &mut self,
163        text: &str,
164        pattern: &str,
165        case_sensitive: bool,
166        matching_mode: &MatchingMode,
167        fields_processed: &[Field],
168        column_number: u32,
169        row_number: i64,
170    ) {
171        match matching_mode {
172            MatchingMode::Regex(regex) => {
173                for entry_match in regex.find_iter(text) {
174                    let column_name = fields_processed[column_number as usize].name();
175                    self.matches.push(TableMatch::new(column_name, column_number, row_number, entry_match.start(), entry_match.end(), text));
176                }
177            }
178
179            MatchingMode::Pattern(regex) => {
180                for (start, end, _) in &find_in_string(text, pattern, case_sensitive, regex) {
181                    let column_name = fields_processed[column_number as usize].name();
182                    self.matches.push(TableMatch::new(column_name, column_number, row_number, *start, *end, text));
183                }
184            }
185        }
186    }
187}
188
189/// Implementation of `TableMatch`.
190impl TableMatch {
191
192    /// This function creates a new `TableMatch` with the provided data.
193    pub fn new(column_name: &str, column_number: u32, row_number: i64, start: usize, end: usize, text: &str) -> Self {
194        Self {
195            column_name: column_name.to_owned(),
196            column_number,
197            row_number,
198            start,
199            end,
200            text: text.to_owned(),
201        }
202    }
203
204    /// This function replaces all the matches in the provided text.
205    fn replace(&self, pattern: &str, replace_pattern: &str, case_sensitive: bool, matching_mode: &MatchingMode, data: &mut DecodedData) -> bool {
206        let (previous_data, mut current_data) = (data.data_to_string().to_string(), data.data_to_string().to_string());
207        let edited = replace_match_string(pattern, replace_pattern, case_sensitive, matching_mode, self.start, self.end, &previous_data, &mut current_data);
208        data.set_data(&current_data).is_ok() && edited
209    }
210}