1use super::config::{Config, FALLBACK_REGEX, KEYWORDS_REGEX, REGEX_TABLE};
2use super::datatypes::*;
3use log::debug;
4use regex::Regex;
5use std::collections::{HashMap, HashSet};
6use std::ffi::OsString;
7use std::fs::{self, OpenOptions, read_dir};
8use std::io::{self, BufReader, prelude::*};
9use std::num::NonZeroUsize;
10use std::process::Command;
11use std::sync::{Arc, RwLock};
12use std::{io::Result, path::Path, path::PathBuf, thread};
13
14const THREAD_NUM: Option<NonZeroUsize> = NonZeroUsize::new(4);
16
17type Dirs = Vec<PathBuf>;
19
20#[derive(Debug)]
23struct File(PathBuf, &'static Regex);
24
25impl File {
26 fn to_string(&self) -> String {
28 self.0.as_os_str().to_os_string().into_string().unwrap()
29 }
30}
31
32type Files = Vec<File>;
33
34fn files_in_dir_or_file_vec(paths_or_files: &[impl AsRef<Path>], conf: &Config) -> Result<Files> {
37 let mut result: Files = vec![];
38 for ele in paths_or_files {
39 if ele.as_ref().is_dir() {
40 result.append(&mut all_files_in_dir(ele, conf)?)
41 } else {
42 file_checker(
43 &mut result,
44 ele.as_ref(),
45 &conf.filetypes,
46 conf.filetypes.len(),
47 true,
48 )
49 }
50 }
51 Ok(result)
52}
53
54fn all_files_in_dir<T>(p: T, conf: &Config) -> Result<Files>
56where
57 T: AsRef<Path>,
58{
59 let mut result = vec![];
60 let (mut files, dirs) = files_and_dirs_in_path(p, &conf)?;
61 result.append(&mut files);
62
63 if dirs.len() != 0 {
64 result.append(
65 &mut dirs
66 .iter()
67 .map(|d| all_files_in_dir(d, conf).unwrap())
68 .flatten()
69 .collect::<Files>(),
70 )
71 }
72
73 Ok(result)
74}
75
76fn files_and_dirs_in_path(p: impl AsRef<Path>, conf: &Config) -> Result<(Files, Dirs)> {
78 let (mut f, mut d): (Files, Dirs) = (vec![], vec![]);
79
80 let filetypes = &conf.filetypes;
82 let filetypes_count = filetypes.len();
83
84 let ignore_dirs = &conf.ignore_dirs;
86 let ignore_dirs_count = ignore_dirs.len();
87
88 for entry in read_dir(p)? {
89 let dir = entry?;
90 let path = dir.path();
91
92 if path.is_dir() {
93 if ignore_dirs_count != 0 {
95 if let Some(d_name) = path.file_name() {
96 if !ignore_dirs.contains(&d_name.to_os_string()) {
97 d.push(path)
98 }
99 }
100 } else {
101 d.push(path)
102 }
103 } else {
104 file_checker(&mut f, &path, &filetypes, filetypes_count, false)
105 }
106 }
107 Ok((f, d))
108}
109
110fn file_checker(
112 files: &mut Files,
113 path: &Path,
114 filetypes: &[OsString],
115 filetypes_count: usize,
116 is_explicit: bool,
117) {
118 let ext = path.extension();
119 let file_name = path.file_name();
120 let ext_str = ext.and_then(|t| t.to_str());
121 let file_name_str = file_name.and_then(|f| f.to_str()).map(|s| s.to_lowercase());
122
123 if filetypes_count != 0 {
125 if let Some(t) = ext {
127 if filetypes.contains(&t.to_os_string()) {
129 let aa = REGEX_TABLE.lock();
131 if let Some(t_str) = ext_str {
132 match aa.as_ref().unwrap().get(t_str) {
133 Some(re) => {
134 let re = unsafe {
136 match (re as *const Regex).clone().as_ref() {
137 Some(a) => a,
138 None => return,
139 }
140 };
141 files.push(File(path.to_path_buf(), re))
142 }
143 _ => {}
144 }
145 }
146 }
147 }
148 } else {
149 let aa = REGEX_TABLE.lock();
150 let guard = aa.as_ref().unwrap();
151
152 if let Some(t_str) = ext_str {
154 if let Some(re) = guard.get(t_str) {
155 let re = unsafe {
156 match (re as *const Regex).clone().as_ref() {
157 Some(a) => a,
158 None => return,
159 }
160 };
161 files.push(File(path.to_path_buf(), re));
162 return;
163 }
164 }
165
166 if let Some(ref name) = file_name_str {
168 if let Some(re) = guard.get(name) {
169 let re = unsafe {
170 match (re as *const Regex).clone().as_ref() {
171 Some(a) => a,
172 None => return,
173 }
174 };
175 files.push(File(path.to_path_buf(), re));
176 return;
177 }
178 }
179
180 if is_explicit {
182 let re = unsafe {
183 match (&*FALLBACK_REGEX as *const Regex).as_ref() {
184 Some(a) => a,
185 None => return,
186 }
187 };
188 files.push(File(path.to_path_buf(), re));
189 }
190 }
191}
192
193fn filter_line(line: &str, line_num: usize, re: &Regex) -> Option<Crumb> {
195 match re.find(line) {
196 Some(mat) => {
197 let position = mat.start();
198 let cap = re.captures(line).unwrap();
199 let content = cap[2].to_string();
200 let comment_symbol_header = cap[1].to_string();
201 if content.starts_with('!') {
202 Some(
203 Crumb::new(line_num, position, content, comment_symbol_header)
204 .add_ignore_flag(),
205 )
206 } else {
207 Some(Crumb::new(
208 line_num,
209 position,
210 content,
211 comment_symbol_header,
212 ))
213 }
214 }
215 None => None,
216 }
217}
218
219fn op_file(file: File, kwreg: &Option<Regex>, conf: Arc<RwLock<Config>>) -> Result<Option<Bread>> {
221 let breads = match bake_bread(&file, kwreg, &conf.read().unwrap()) {
222 Ok(b) => b,
223 Err(e) => {
224 debug!("file {} had error {}", file.to_string(), e.to_string());
225 return Ok(None);
226 }
227 };
228
229 if !conf.read().unwrap().delete {
230 Ok(breads)
231 } else {
232 match breads {
233 Some(bb) => {
234 delete_the_crumbs(bb)?;
235 Ok(None)
236 }
237 None => Ok(None),
238 }
239 }
240}
241
242fn bake_bread(file: &File, kwreg: &Option<Regex>, conf: &Config) -> Result<Option<Bread>> {
244 let mut buf = vec![];
246 let file_p = file.to_string();
247 let mut f: std::fs::File = std::fs::File::open(file.0.clone())?;
248 f.read_to_end(&mut buf)?;
249
250 let mut line_num = 0;
251 let mut ss = String::new(); let mut buf = buf.as_slice();
253 let mut result = vec![];
254 let mut head: Option<Crumb> = None; let mut shadow_file = vec![]; let mut keyword_checker_and_push = |mut cb: Crumb| {
259 if kwreg.is_some() {
260 if cb.filter_keywords(kwreg.as_ref().unwrap()) {
262 result.push(cb)
263 }
264 } else {
265 if !cb.is_ignore() || conf.show_ignored {
266 result.push(cb)
267 }
268 }
269 };
270
271 loop {
272 line_num += 1;
273 match buf.read_line(&mut ss) {
274 Ok(0) => {
275 if head.is_some() {
276 keyword_checker_and_push(head.unwrap());
277 }
278 break;
279 }
280 Err(e) => {
281 eprintln!(
282 "Warning: file {} had read error at line {}: {}",
283 file_p, line_num, e
284 );
285 if head.is_some() {
286 keyword_checker_and_push(head.unwrap());
287 }
288 break;
289 }
290 Ok(_) => match filter_line(&ss, line_num, file.1) {
291 Some(cb) => {
292 match head {
294 Some(ref mut h) => {
295 if h.has_tail() {
296 h.add_tail(cb);
298 ss.clear(); continue;
300 } else {
301 keyword_checker_and_push(head.unwrap());
303 head = None;
304 }
305 }
306 None => (),
307 }
308
309 if cb.has_tail() {
310 head = Some(cb);
312 } else {
313 keyword_checker_and_push(cb)
315 }
316 }
317 None => {
318 if head.is_some() {
319 keyword_checker_and_push(head.unwrap());
320 head = None;
321 }
322 }
323 },
324 }
325
326 if conf.range > 0 {
327 shadow_file.push(ss.clone());
328 }
329
330 ss.clear()
331 }
332
333 if conf.range > 0 {
335 result.iter_mut().for_each(|crumb| {
336 let ahead_ind = (crumb.line_num - 1).saturating_sub(conf.range as usize);
337 let tail_ind = (crumb.line_num - 1)
338 .saturating_add(conf.range as usize)
339 .min(shadow_file.len());
340 crumb.range_content = Some(
341 (ahead_ind + 1..tail_ind + 1)
342 .zip(
343 shadow_file
344 .get(ahead_ind..tail_ind)
345 .map(|x| x.to_vec())
346 .unwrap(),
347 )
348 .collect(),
349 );
350 });
351 }
352
353 if result.len() == 0 {
354 Ok(None)
355 } else {
356 Ok(Some(Bread::new(file_p, result)))
357 }
358}
359
360pub fn delete_the_crumbs(Bread { file_path, crumbs }: Bread) -> Result<String> {
362 let all_delete_line_postion_pairs = crumbs
363 .iter()
364 .map(|crumb| crumb.all_lines_num_postion_pair())
365 .flatten();
366
367 delete_lines_on(&file_path, all_delete_line_postion_pairs)?;
368
369 println!("deleted the crumbs in {}", file_path);
370 Ok(file_path)
371}
372
373pub fn delete_the_crumbs_on_special_index(
375 Bread { file_path, crumbs }: Bread,
376 indexes: HashSet<usize>,
377) -> Result<String> {
378 let mut all_delete_lines = vec![];
379 for ind in &indexes {
380 match crumbs.get(*ind) {
381 Some(c) => all_delete_lines.append(&mut c.all_lines_num_postion_pair()),
382 None => return Err(io::Error::other("cannot find crumb index in bread")),
383 }
384 }
385
386 delete_lines_on(&file_path, all_delete_lines.into_iter())?;
387
388 println!("deleted {} crumbs in {}", indexes.len(), file_path);
389
390 Ok(file_path)
391}
392
393fn write_file_atomically(file_path: &str, lines: &[Vec<u8>]) -> Result<()> {
394 let temp_path = format!("{}.tmp", file_path);
395 let write_res = (|| {
396 let mut temp_file = OpenOptions::new()
397 .create(true)
398 .write(true)
399 .truncate(true)
400 .open(&temp_path)?;
401 for line in lines {
402 temp_file.write_all(line)?;
403 temp_file.write_all(b"\n")?;
404 }
405 Ok(())
406 })();
407
408 if let Err(e) = write_res {
409 let _ = fs::remove_file(&temp_path);
410 return Err(e);
411 }
412
413 if let Err(e) = fs::rename(&temp_path, file_path) {
414 let _ = fs::remove_file(&temp_path);
415 return Err(e);
416 }
417
418 Ok(())
419}
420
421fn delete_lines_on(
423 file_path: &str,
424 line_num_pos_pairs: impl Iterator<Item = (usize, usize)>,
425) -> Result<()> {
426 let f = fs::File::open(&file_path)?;
427 let reader = BufReader::new(f).lines();
428
429 let all_delete_lines = line_num_pos_pairs.collect();
430
431 let finish_deleted = delete_nth_lines(reader, all_delete_lines)?
432 .into_iter()
433 .map(|line| line.into_bytes())
434 .collect::<Vec<_>>();
435
436 write_file_atomically(file_path, &finish_deleted)
437}
438
439fn delete_nth_lines(
441 f: impl Iterator<Item = Result<String>>,
442 nm: HashMap<usize, usize>,
443) -> Result<Vec<String>> {
444 let mut result = vec![];
445
446 for (line_num, ll) in f.enumerate() {
447 if nm.contains_key(&(line_num + 1)) {
448 let mut new_l = ll?;
449 new_l.truncate(*nm.get(&(line_num + 1)).unwrap());
450 if new_l == "" {
451 continue;
453 }
454 result.push(new_l);
455 } else {
456 result.push(ll?);
457 }
458 }
459
460 Ok(result)
461}
462
463pub fn restore_the_crumb(Bread { file_path, crumbs }: Bread) -> Result<String> {
465 let all_restore_lines = crumbs
466 .iter()
467 .map(|c| c.all_lines_num_postion_and_header_content())
468 .flatten();
469
470 restore_lines_on(&file_path, all_restore_lines)?;
471
472 println!("restored the crumbs in {}", file_path);
473 Ok(file_path)
474}
475
476pub fn restore_the_crumb_on_special_index(
478 Bread { file_path, crumbs }: Bread,
479 indexes: HashSet<usize>,
480) -> Result<String> {
481 let mut all_restore_lines = Vec::with_capacity(indexes.len());
482 for ind in &indexes {
483 match crumbs.get(*ind) {
484 Some(c) => all_restore_lines.append(&mut c.all_lines_num_postion_and_header_content()),
485 None => return Err(io::Error::other("cannot find crumb index in bread")),
486 }
487 }
488
489 restore_lines_on(&file_path, all_restore_lines.into_iter())?;
490
491 println!("restored {} crumbs in {}", indexes.len(), file_path);
492 Ok(file_path)
493}
494
495fn restore_lines_on<'a>(
496 file_path: &'a str,
497 all_restore_lines: impl Iterator<Item = (usize, usize, &'a str, &'a str)>,
498) -> Result<()> {
499 let f = fs::File::open(&file_path)?;
500 let reader = BufReader::new(f).lines();
501
502 let mut table: HashMap<usize, (usize, &str, &str)> =
503 HashMap::with_capacity(all_restore_lines.size_hint().1.unwrap_or(0));
504
505 all_restore_lines.for_each(|(line_num, pos, header, content)| {
506 table.insert(line_num, (pos, header, content));
507 });
508
509 let mut new_file = Vec::with_capacity(reader.size_hint().1.unwrap_or(0));
510 for (line_num, ll) in reader.enumerate() {
511 if let Some((pos, header, content)) = table.get(&(line_num + 1)) {
512 let mut new_l = ll?;
513 new_l.truncate(*pos);
514 new_l.push_str(*header);
515 new_l.push_str(" ");
516 new_l.push_str(*content);
517
518 new_file.push(new_l.into_bytes())
519 } else {
520 new_file.push(ll?.into_bytes());
521 }
522 }
523
524 write_file_atomically(file_path, &new_file)
525}
526
527pub fn run_format_command_to_file(
529 fmt_command: &str,
530 _files: impl IntoIterator<Item = String>,
531) -> std::result::Result<(), String> {
532 let mut command_splits = fmt_command.split(' ');
533 let first = command_splits
534 .next()
535 .ok_or("fmt_command cannot be empty".to_string())?;
536
537 let mut comm = Command::new(first);
538 let mut child = comm
539 .args(command_splits)
540 .spawn()
541 .expect("Cannot run the fmt_command");
542
543 println!("running fmt command: {}", fmt_command);
544 child
545 .wait()
546 .expect("fmt command wasn't running")
547 .exit_ok()
548 .map_err(|e| e.to_string())
549}
550
551pub fn handle_files(conf: Config) -> impl Iterator<Item = Bread> {
553 let mut all_files: Vec<File> = files_in_dir_or_file_vec(&conf.files, &conf).unwrap();
555
556 let threads_num: usize = thread::available_parallelism()
558 .unwrap_or(THREAD_NUM.unwrap())
559 .into();
560
561 let len = all_files.len();
562 let count = len / threads_num;
563 let mut groups: Vec<Vec<File>> = vec![];
564 for _ in 0..threads_num - 1 {
565 groups.push(all_files.drain(0..count).collect())
566 }
567 groups.push(all_files.drain(0..).collect());
568
569 let conf = Arc::new(RwLock::new(conf));
570 groups
571 .into_iter()
572 .map(move |fs| {
573 let kwreg = KEYWORDS_REGEX.lock().unwrap().clone();
574 let conf_c = Arc::clone(&conf);
575 thread::spawn(|| {
576 fs.into_iter()
577 .filter_map(move |f| op_file(f, &kwreg, conf_c.clone()).unwrap())
578 .collect::<Vec<Bread>>()
579 })
580 })
581 .map(|han| han.join().unwrap())
582 .flatten()
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
590 fn test_files_and_dirs_in_path() -> Result<()> {
591 let (fs, dirs) = files_and_dirs_in_path("./tests/testcases", &Default::default())?;
592
593 assert_eq!(dirs.len(), 0);
594 assert_eq!(fs[0].0, PathBuf::from("./tests/testcases/multilines.rs"),);
595 Ok(())
596 }
597
598 }