1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::io::Write;
4use std::path::Path;
5
6use anyhow::Result;
7use colored::{Color, Colorize};
8use regex::Regex;
9use secrecy::ExposeSecret;
10use walkdir::WalkDir;
11
12use crate::config::cli::PrintConfig;
13use crate::pgp::PGPClient;
14use crate::util::fs_util::{get_dir_gpg_id_content, path_to_str};
15use crate::util::tree::string_to_color_opt;
16
17#[derive(Default)]
18pub struct GrepPrintConfig {
19 pub grep_pass_color: Option<Color>,
20 pub grep_match_color: Option<Color>,
21}
22
23impl<CFG: AsRef<PrintConfig>> From<CFG> for GrepPrintConfig {
24 fn from(config: CFG) -> Self {
25 Self {
26 grep_pass_color: string_to_color_opt(&config.as_ref().grep_pass_color),
27 grep_match_color: string_to_color_opt(&config.as_ref().grep_match_color),
28 }
29 }
30}
31
32pub fn grep_stream<O>(
33 pgp_executable: &str,
34 root: &Path,
35 search_str: &str,
36 print_cfg: &GrepPrintConfig,
37 out_stream: &mut O,
38) -> Result<()>
39where
40 O: Write,
41{
42 let mut cache: Vec<(u64, Vec<String>, PGPClient)> = Vec::new();
43 let search_regex = Regex::new(®ex::escape(search_str))?;
44
45 for entry in WalkDir::new(root) {
46 let entry = entry?;
47 if entry.file_type().is_file() && entry.path().extension().unwrap_or_default() == "gpg" {
48 let relative_path = entry.path().strip_prefix(root)?;
49 let relative_path_str = path_to_str(relative_path)?;
50
51 let mut keys_fpr = get_dir_gpg_id_content(root, entry.path())?;
52 keys_fpr.sort();
53
54 let mut hasher = DefaultHasher::new();
55 keys_fpr.hash(&mut hasher);
56 let key_hash = hasher.finish();
57
58 let client: &mut PGPClient = if let Some((_, _, client)) = cache
59 .iter_mut()
60 .find(|(h, cached_keys, _)| *h == key_hash && *cached_keys == keys_fpr)
61 {
62 client
63 } else {
64 let new_client = PGPClient::new(
65 pgp_executable,
66 &keys_fpr.iter().map(String::as_str).collect::<Vec<_>>(),
67 )?;
68 cache.push((key_hash, keys_fpr.clone(), new_client));
69 &mut cache.last_mut().unwrap().2
70 };
71
72 let decrypted = client.decrypt_stdin(root, path_to_str(entry.path())?)?;
73 let mut has_matches = false;
74
75 for line in decrypted.expose_secret().lines() {
76 if line.contains(search_str) {
77 if !has_matches {
78 if let Some(color) = print_cfg.grep_pass_color {
79 writeln!(
80 out_stream,
81 "{}:",
82 &relative_path_str[..relative_path_str.len() - 4].color(color)
83 )?;
84 } else {
85 writeln!(
86 out_stream,
87 "{}:",
88 &relative_path_str[..relative_path_str.len() - 4]
89 )?;
90 }
91 has_matches = true;
92 }
93
94 let output_line = if let Some(color) = print_cfg.grep_match_color {
95 search_regex
96 .replace_all(line, |caps: ®ex::Captures| {
97 caps[0].color(color).to_string()
98 })
99 .to_string()
100 } else {
101 line.to_string()
102 };
103 writeln!(out_stream, "{}", output_line)?;
104 }
105 }
106 }
107 }
108
109 Ok(())
110}
111
112pub fn grep(
113 pgp_executable: &str,
114 root: &Path,
115 search_str: &str,
116 print_cfg: &GrepPrintConfig,
117) -> Result<Vec<String>> {
118 let mut results = Vec::new();
119 let mut cache: Vec<(u64, Vec<String>, PGPClient)> = Vec::new();
120 let search_regex = Regex::new(®ex::escape(search_str))?;
121
122 for entry in WalkDir::new(root) {
123 let entry = entry?;
124 if entry.file_type().is_file() && entry.path().extension().unwrap_or_default() == "gpg" {
125 let relative_path = entry.path().strip_prefix(root)?;
126 let relative_path_str = path_to_str(relative_path)?;
127
128 let mut keys_fpr = get_dir_gpg_id_content(root, entry.path())?;
129 keys_fpr.sort();
130
131 let mut hasher = DefaultHasher::new();
132 keys_fpr.hash(&mut hasher);
133 let key_hash = hasher.finish();
134
135 let client: &mut PGPClient = if let Some((_, _, client)) = cache
136 .iter_mut()
137 .find(|(h, cached_keys, _)| *h == key_hash && *cached_keys == keys_fpr)
138 {
139 client
140 } else {
141 let new_client = PGPClient::new(
142 pgp_executable,
143 &keys_fpr.iter().map(String::as_str).collect::<Vec<_>>(),
144 )?;
145 cache.push((key_hash, keys_fpr.clone(), new_client));
146 &mut cache.last_mut().unwrap().2
147 };
148
149 let decrypted = client.decrypt_stdin(root, path_to_str(entry.path())?)?;
150 let matching_lines: Vec<String> = decrypted
151 .expose_secret()
152 .lines()
153 .filter(|line| line.contains(search_str))
154 .map(|line| {
155 if let Some(color) = print_cfg.grep_match_color {
156 search_regex
157 .replace_all(line, |caps: ®ex::Captures| {
158 caps[0].color(color).to_string()
159 })
160 .to_string()
161 } else {
162 line.to_string()
163 }
164 })
165 .collect();
166
167 if !matching_lines.is_empty() {
168 if let Some(color) = print_cfg.grep_pass_color {
169 results.push(format!(
170 "{}:",
171 &relative_path_str[..relative_path_str.len() - 4].color(color)
172 ));
173 } else {
174 results.push(format!("{}:", &relative_path_str[..relative_path_str.len() - 4]));
175 }
176 results.extend(matching_lines);
177 }
178 }
179 }
180
181 Ok(results)
182}
183
184#[cfg(test)]
185mod tests {
186 use std::path::{self, PathBuf};
187
188 use pretty_assertions::assert_eq;
189 use serial_test::serial;
190 use tempfile::TempDir;
191
192 use super::*;
193 use crate::pgp::key_management::key_gen_batch;
194 use crate::util::defer::cleanup;
195 use crate::util::test_util::*;
196
197 fn setup_test_environment() -> (String, String, TempDir, PathBuf) {
198 let executable = get_test_executable();
199 let email = get_test_email();
200 let (_tmp_dir, root) = gen_unique_temp_dir();
201
202 let file1_content = "INF\n2112112";
203 let file2_content = "Overlord\nNAN";
204
205 let structure: &[(Option<&str>, &[&str])] =
206 &[(Some("dir1"), &[][..]), (Some("dir2"), &[][..])];
207 create_dir_structure(&root, structure);
208
209 key_gen_batch(&executable, &gpg_key_gen_example_batch()).unwrap();
210 let test_client = PGPClient::new(executable.clone(), &[&email]).unwrap();
211 test_client.key_edit_batch(&gpg_key_edit_example_batch()).unwrap();
212
213 test_client.encrypt(file1_content, root.join("dir1/01.gpg").to_str().unwrap()).unwrap();
214 test_client.encrypt(file2_content, root.join("dir2/10.gpg").to_str().unwrap()).unwrap();
215 write_gpg_id(&root, &test_client.get_keys_fpr());
216 (executable, email, _tmp_dir, root)
217 }
218
219 #[test]
220 #[serial]
221 #[ignore = "need run interactively"]
222 fn grep_content_match() {
223 let (executable, email, _tmp_dir, root) = setup_test_environment();
224
225 cleanup!(
226 {
227 let results = grep(&executable, &root, "211", &GrepPrintConfig::default()).unwrap();
228 assert_eq!(results, vec![&format!("dir1{}01:", path::MAIN_SEPARATOR), "2112112"]);
229 },
230 {
231 clean_up_test_key(&executable, &[&email]).unwrap();
232 }
233 );
234 }
235
236 #[test]
237 #[serial]
238 #[ignore = "need run interactively"]
239 fn grep_filename_match() {
240 let (executable, email, _tmp_dir, root) = setup_test_environment();
241
242 cleanup!(
243 {
244 let results =
245 grep(&executable, &root, "Overlord", &GrepPrintConfig::default()).unwrap();
246 assert_eq!(results, vec![&format!("dir2{}10:", path::MAIN_SEPARATOR), "Overlord"]);
247
248 let results = grep(&executable, &root, "01", &GrepPrintConfig::default()).unwrap();
249 assert_eq!(results, Vec::<String>::new());
250 },
251 {
252 clean_up_test_key(&executable, &[&email]).unwrap();
253 }
254 );
255 }
256
257 #[test]
258 #[serial]
259 #[ignore = "need run interactively"]
260 fn grep_no_matches() {
261 let (executable, email, _tmp_dir, root) = setup_test_environment();
262
263 cleanup!(
264 {
265 let results =
266 grep(&executable, &root, "nonexistent", &GrepPrintConfig::default()).unwrap();
267 assert!(results.is_empty());
268 },
269 {
270 clean_up_test_key(&executable, &[&email]).unwrap();
271 }
272 );
273 }
274}