1use crate::config::REGEX_TABLE_MUL;
2
3use super::config::{Config, FALLBACK_REGEX, KEYWORDS_REGEX, REGEX_TABLE};
4use super::datatypes::*;
5use log::debug;
6use regex::Regex;
7use std::collections::{HashMap, HashSet};
8use std::ffi::OsString;
9use std::fs::{self, OpenOptions, read_dir};
10use std::io::{self, BufReader, prelude::*};
11use std::num::NonZeroUsize;
12use std::process::Command;
13use std::sync::{Arc, RwLock};
14use std::{io::Result, path::Path, path::PathBuf, thread};
15
16const THREAD_NUM: Option<NonZeroUsize> = NonZeroUsize::new(4);
18
19type Dirs = Vec<PathBuf>;
21
22#[derive(Debug)]
25struct File(
26 PathBuf,
27 Option<&'static Regex>,
28 Option<&'static (Regex, Regex)>,
29);
30
31impl File {
32 fn to_string(&self) -> String {
34 self.0.as_os_str().to_os_string().into_string().unwrap()
35 }
36}
37
38type Files = Vec<File>;
39
40fn files_in_dir_or_file_vec(paths_or_files: &[impl AsRef<Path>], conf: &Config) -> Result<Files> {
43 let mut result: Files = vec![];
44 for ele in paths_or_files {
45 if ele.as_ref().is_dir() {
46 result.append(&mut all_files_in_dir(ele, conf)?)
47 } else {
48 file_checker(
49 &mut result,
50 ele.as_ref(),
51 &conf.filetypes,
52 conf.filetypes.len(),
53 true,
54 )
55 }
56 }
57 Ok(result)
58}
59
60fn all_files_in_dir<T>(p: T, conf: &Config) -> Result<Files>
62where
63 T: AsRef<Path>,
64{
65 let mut result = vec![];
66 let (mut files, dirs) = files_and_dirs_in_path(p, &conf)?;
67 result.append(&mut files);
68
69 if dirs.len() != 0 {
70 result.append(
71 &mut dirs
72 .iter()
73 .map(|d| all_files_in_dir(d, conf).unwrap())
74 .flatten()
75 .collect::<Files>(),
76 )
77 }
78
79 Ok(result)
80}
81
82fn files_and_dirs_in_path(p: impl AsRef<Path>, conf: &Config) -> Result<(Files, Dirs)> {
84 let (mut f, mut d): (Files, Dirs) = (vec![], vec![]);
85
86 let filetypes = &conf.filetypes;
88 let filetypes_count = filetypes.len();
89
90 let ignore_dirs = &conf.ignore_dirs;
92 let ignore_dirs_count = ignore_dirs.len();
93
94 for entry in read_dir(p)? {
95 let dir = entry?;
96 let path = dir.path();
97
98 if path.is_dir() {
99 if ignore_dirs_count != 0 {
101 if let Some(d_name) = path.file_name() {
102 if !ignore_dirs.contains(&d_name.to_os_string()) {
103 d.push(path)
104 }
105 }
106 } else {
107 d.push(path)
108 }
109 } else {
110 file_checker(&mut f, &path, &filetypes, filetypes_count, false)
111 }
112 }
113 Ok((f, d))
114}
115
116fn file_checker(
118 files: &mut Files,
119 path: &Path,
120 filetypes: &[OsString],
121 filetypes_count: usize,
122 is_explicit: bool,
123) {
124 let ext = path.extension();
125 let file_name = path.file_name();
126 let ext_str = ext.and_then(|t| t.to_str());
127 let file_name_str = file_name.and_then(|f| f.to_str()).map(|s| s.to_lowercase());
128
129 if filetypes_count != 0 {
131 if let Some(t) = ext {
133 if filetypes.contains(&t.to_os_string()) {
135 let aa = REGEX_TABLE.lock();
137 let bb = REGEX_TABLE_MUL.lock();
138
139 if let Some(t_str) = ext_str {
140 let single_line_re = match aa.as_ref().unwrap().get(t_str) {
141 Some(re) => unsafe { (re as *const Regex).as_ref() },
142 _ => None,
143 };
144
145 let mul_line_re = match bb.as_ref().unwrap().get(t_str) {
146 Some(re) => unsafe { (re as *const (Regex, Regex)).as_ref() },
147 _ => None,
148 };
149
150 if single_line_re.is_some() || mul_line_re.is_some() {
151 files.push(File(path.to_path_buf(), single_line_re, mul_line_re));
152 }
153 }
154 }
155 }
156 } else {
157 let aa = REGEX_TABLE.lock();
158 let aa_guard = aa.as_ref().unwrap();
159
160 let bb = REGEX_TABLE_MUL.lock();
161 let bb_guard = bb.as_ref().unwrap();
162
163 if let Some(t_str) = ext_str {
165 let single_line_re = match aa_guard.get(t_str) {
166 Some(re) => unsafe { (re as *const Regex).as_ref() },
167 None => None,
168 };
169
170 let mul_line_re = match bb_guard.get(t_str) {
171 Some(re) => unsafe { (re as *const (Regex, Regex)).as_ref() },
172 None => None,
173 };
174
175 if single_line_re.is_some() || mul_line_re.is_some() {
176 files.push(File(path.to_path_buf(), single_line_re, mul_line_re));
177 return;
178 }
179 }
180
181 if let Some(ref name) = file_name_str {
183 let single_line_re = match aa_guard.get(name) {
184 Some(re) => unsafe { (re as *const Regex).as_ref() },
185 None => None,
186 };
187
188 let mul_line_re = match bb_guard.get(name) {
189 Some(re) => unsafe { (re as *const (Regex, Regex)).as_ref() },
190 None => None,
191 };
192
193 if single_line_re.is_some() || mul_line_re.is_some() {
194 files.push(File(path.to_path_buf(), single_line_re, mul_line_re));
195 return;
196 }
197 }
198
199 if is_explicit {
201 let re = unsafe {
202 match (&*FALLBACK_REGEX as *const Regex).as_ref() {
203 Some(a) => a,
204 None => return,
205 }
206 };
207 files.push(File(path.to_path_buf(), Some(re), None));
208 }
209 }
210}
211
212enum FilterLineStatus {
214 None,
217
218 InMulLine,
220}
221
222struct FilterLiner<'this_file> {
223 regex_single_line: Option<&'this_file Regex>,
224
225 regex_multiple_line: Option<&'this_file (Regex, Regex)>,
226
227 status: FilterLineStatus,
228}
229
230impl<'this_file> FilterLiner<'this_file> {
231 fn filter_line(&mut self, line: &str, line_num: usize) -> Option<Crumb> {
232 match self.status {
233 FilterLineStatus::None => {
234 if let Some(aa) = self.regex_multiple_line {
236 match aa.0.find(line) {
237 Some(mat) => {
238 let position = mat.start();
239 let cap = aa.0.captures(line).unwrap();
240 let content = cap[2].to_string();
241 let comment_symbol_header = cap[1].to_string();
242 let mut res = if content.starts_with('!') {
243 Crumb::new(
244 line_num,
245 position,
246 content,
247 comment_symbol_header,
248 String::new(),
249 )
250 .add_ignore_flag()
251 } else {
252 Crumb::new(
253 line_num,
254 position,
255 content,
256 comment_symbol_header,
257 String::new(),
258 )
259 };
260
261 res.has_tail = true;
263
264 self.status = FilterLineStatus::InMulLine;
266
267 return Some(res);
268 }
269 None => (),
270 }
271 }
272
273 if let Some(bb) = self.regex_single_line {
274 match bb.find(line) {
275 Some(mat) => {
276 let position = mat.start();
277 let cap = bb.captures(line).unwrap();
278 let content = cap[2].to_string();
279 let comment_symbol_header = cap[1].to_string();
280 let res = if content.starts_with('!') {
281 Crumb::new(
282 line_num,
283 position,
284 content,
285 comment_symbol_header,
286 String::new(),
287 )
288 .add_ignore_flag()
289 } else {
290 Crumb::new(
291 line_num,
292 position,
293 content,
294 comment_symbol_header,
295 String::new(),
296 )
297 };
298
299 return Some(res);
300 }
301 None => (),
302 }
303 }
304 return None;
305 }
306 FilterLineStatus::InMulLine => {
307 let aa = self.regex_multiple_line.unwrap();
308 if let Some(_mat) = aa.1.find(line) {
309 self.status = FilterLineStatus::None;
310 let cap = aa.1.captures(line).unwrap();
311 let raw_content = &cap[1];
312 let content = raw_content
313 .trim_start()
314 .trim_end_matches(['\r', '\n'])
315 .to_string();
316 let position = line.len() - line.trim_start().len();
317 let comment_symbol_endding = cap[2].to_string();
318 let cr = Crumb::new(
319 line_num,
320 position,
321 content,
322 String::new(),
323 comment_symbol_endding,
324 );
325 Some(cr)
326 } else {
327 let content = line.trim_start().trim_end_matches(['\r', '\n']);
328 let position = line.len() - line.trim_start().len();
329 let mut cr = Crumb::new(
330 line_num,
331 position,
332 content.to_string(),
333 String::new(),
334 String::new(),
335 );
336 cr.has_tail = true;
337 Some(cr)
338 }
339 }
340 }
341 }
342}
343
344fn op_file(file: File, kwreg: &Option<Regex>, conf: Arc<RwLock<Config>>) -> Result<Option<Bread>> {
346 let breads = match bake_bread(&file, kwreg, &conf.read().unwrap()) {
347 Ok(b) => b,
348 Err(e) => {
349 debug!("file {} had error {}", file.to_string(), e.to_string());
350 return Ok(None);
351 }
352 };
353
354 if !conf.read().unwrap().delete {
355 Ok(breads)
356 } else {
357 match breads {
358 Some(bb) => {
359 delete_the_crumbs(bb)?;
360 Ok(None)
361 }
362 None => Ok(None),
363 }
364 }
365}
366
367fn bake_bread(file: &File, kwreg: &Option<Regex>, conf: &Config) -> Result<Option<Bread>> {
370 let mut buf = vec![];
372 let file_p = file.to_string();
373 let mut f: std::fs::File = std::fs::File::open(file.0.clone())?;
374 f.read_to_end(&mut buf)?;
375
376 let mut line_num = 0;
377 let mut ss = String::new(); let mut buf = buf.as_slice();
379 let mut result = vec![];
380 let mut head: Option<Crumb> = None; let mut shadow_file = vec![]; let mut keyword_checker_and_push = |mut cb: Crumb| {
385 cb.has_tail = false;
386 if kwreg.is_some() {
387 if cb.filter_keywords(kwreg.as_ref().unwrap()) {
389 result.push(cb)
390 }
391 } else {
392 if !cb.is_ignore() || conf.show_ignored {
393 result.push(cb)
394 }
395 }
396 };
397
398 let mut fl = FilterLiner {
400 regex_single_line: file.1,
401 regex_multiple_line: file.2,
402 status: FilterLineStatus::None,
403 };
404
405 loop {
406 line_num += 1;
407 match buf.read_line(&mut ss) {
408 Ok(0) => {
409 if head.is_some() {
410 keyword_checker_and_push(head.unwrap());
411 }
412 break;
413 }
414 Err(e) => {
415 eprintln!(
416 "Warning: file {} had read error at line {}: {}",
417 file_p, line_num, e
418 );
419 if head.is_some() {
420 keyword_checker_and_push(head.unwrap());
421 }
422 break;
423 }
424 Ok(_) => match fl.filter_line(&ss, line_num) {
425 Some(mut cb) => {
426 match head {
428 Some(ref mut h) => {
429 if h.has_tail() {
430 h.add_tail(cb);
432 ss.clear(); continue;
434 } else {
435 keyword_checker_and_push(head.unwrap());
437 head = None;
438 }
439 }
440 None => (),
441 }
442
443 if cb.has_tail() {
444 head = Some(cb);
446 } else {
447 keyword_checker_and_push(cb)
449 }
450 }
451 None => {
452 if head.is_some() {
453 keyword_checker_and_push(head.unwrap());
454 head = None;
455 }
456 }
457 },
458 }
459
460 if conf.range > 0 {
461 shadow_file.push(ss.clone());
462 }
463
464 ss.clear()
465 }
466
467 if conf.range > 0 {
469 result.iter_mut().for_each(|crumb| {
470 let ahead_ind = (crumb.line_num - 1).saturating_sub(conf.range as usize);
471 let tail_ind = (crumb.line_num - 1)
472 .saturating_add(conf.range as usize)
473 .min(shadow_file.len());
474 crumb.range_content = Some(
475 (ahead_ind + 1..tail_ind + 1)
476 .zip(
477 shadow_file
478 .get(ahead_ind..tail_ind)
479 .map(|x| x.to_vec())
480 .unwrap(),
481 )
482 .collect(),
483 );
484 });
485 }
486
487 if result.len() == 0 {
488 Ok(None)
489 } else {
490 Ok(Some(Bread::new(file_p, result)))
491 }
492}
493
494pub fn delete_the_crumbs(Bread { file_path, crumbs }: Bread) -> Result<String> {
496 let all_delete_line_postion_pairs = crumbs
497 .iter()
498 .map(|crumb| crumb.all_lines_num_postion_pair())
499 .flatten();
500
501 delete_lines_on(&file_path, all_delete_line_postion_pairs)?;
502
503 println!("deleted the crumbs in {}", file_path);
504 Ok(file_path)
505}
506
507pub fn delete_the_crumbs_on_special_index(
509 Bread { file_path, crumbs }: Bread,
510 indexes: HashSet<usize>,
511) -> Result<String> {
512 let mut all_delete_lines = vec![];
513 for ind in &indexes {
514 match crumbs.get(*ind) {
515 Some(c) => all_delete_lines.append(&mut c.all_lines_num_postion_pair()),
516 None => return Err(io::Error::other("cannot find crumb index in bread")),
517 }
518 }
519
520 delete_lines_on(&file_path, all_delete_lines.into_iter())?;
521
522 println!("deleted {} crumbs in {}", indexes.len(), file_path);
523
524 Ok(file_path)
525}
526
527fn write_file_atomically(file_path: &str, lines: &[Vec<u8>]) -> Result<()> {
528 let temp_path = format!("{}.tmp", file_path);
529 let write_res = (|| {
530 let mut temp_file = OpenOptions::new()
531 .create(true)
532 .write(true)
533 .truncate(true)
534 .open(&temp_path)?;
535 for line in lines {
536 temp_file.write_all(line)?;
537 temp_file.write_all(b"\n")?;
538 }
539 Ok(())
540 })();
541
542 if let Err(e) = write_res {
543 let _ = fs::remove_file(&temp_path);
544 return Err(e);
545 }
546
547 if let Err(e) = fs::rename(&temp_path, file_path) {
548 let _ = fs::remove_file(&temp_path);
549 return Err(e);
550 }
551
552 Ok(())
553}
554
555fn delete_lines_on(
557 file_path: &str,
558 line_num_pos_pairs: impl Iterator<Item = (usize, usize)>,
559) -> Result<()> {
560 let f = fs::File::open(&file_path)?;
561 let reader = BufReader::new(f).lines();
562
563 let all_delete_lines = line_num_pos_pairs.collect();
564
565 let finish_deleted = delete_nth_lines(reader, all_delete_lines)?
566 .into_iter()
567 .map(|line| line.into_bytes())
568 .collect::<Vec<_>>();
569
570 write_file_atomically(file_path, &finish_deleted)
571}
572
573fn delete_nth_lines(
575 f: impl Iterator<Item = Result<String>>,
576 nm: HashMap<usize, usize>,
577) -> Result<Vec<String>> {
578 let mut result = vec![];
579
580 for (line_num, ll) in f.enumerate() {
581 if nm.contains_key(&(line_num + 1)) {
582 let mut new_l = ll?;
583 new_l.truncate(*nm.get(&(line_num + 1)).unwrap());
584 if new_l == "" {
585 continue;
587 }
588 result.push(new_l);
589 } else {
590 result.push(ll?);
591 }
592 }
593
594 Ok(result)
595}
596
597pub fn restore_the_crumb(Bread { file_path, crumbs }: Bread) -> Result<String> {
599 let all_restore_lines = crumbs
600 .iter()
601 .map(|c| c.all_lines_num_postion_and_header_content())
602 .flatten();
603
604 restore_lines_on(&file_path, all_restore_lines)?;
605
606 println!("restored the crumbs in {}", file_path);
607 Ok(file_path)
608}
609
610pub fn restore_the_crumb_on_special_index(
612 Bread { file_path, crumbs }: Bread,
613 indexes: HashSet<usize>,
614) -> Result<String> {
615 let mut all_restore_lines = Vec::with_capacity(indexes.len());
616 for ind in &indexes {
617 match crumbs.get(*ind) {
618 Some(c) => all_restore_lines.append(&mut c.all_lines_num_postion_and_header_content()),
619 None => return Err(io::Error::other("cannot find crumb index in bread")),
620 }
621 }
622
623 restore_lines_on(&file_path, all_restore_lines.into_iter())?;
624
625 println!("restored {} crumbs in {}", indexes.len(), file_path);
626 Ok(file_path)
627}
628
629fn restore_lines_on<'a>(
630 file_path: &'a str,
631 all_restore_lines: impl Iterator<Item = (usize, usize, &'a str, &'a str, &'a str)>,
632) -> Result<()> {
633 let f = fs::File::open(&file_path)?;
634 let reader = BufReader::new(f).lines();
635
636 let mut table: HashMap<usize, (usize, &str, &str, &str)> =
637 HashMap::with_capacity(all_restore_lines.size_hint().1.unwrap_or(0));
638
639 all_restore_lines.for_each(|(line_num, pos, header, endding, content)| {
640 table.insert(line_num, (pos, header, content, endding));
641 });
642
643 let mut new_file = Vec::with_capacity(reader.size_hint().1.unwrap_or(0));
644 for (line_num, ll) in reader.enumerate() {
645 if let Some((pos, header, content, endding)) = table.get(&(line_num + 1)) {
646 let mut new_l = ll?;
647 new_l.truncate(*pos);
648 new_l.push_str(*header);
649 if *header != "" {
650 new_l.push_str(" ");
651 }
652 new_l.push_str(*content);
653 new_l.push_str(*endding);
654
655 new_file.push(new_l.into_bytes())
656 } else {
657 new_file.push(ll?.into_bytes());
658 }
659 }
660
661 write_file_atomically(file_path, &new_file)
662}
663
664pub fn run_format_command_to_file(
666 fmt_command: &str,
667 _files: impl IntoIterator<Item = String>,
668) -> std::result::Result<(), String> {
669 let mut command_splits = fmt_command.split(' ');
670 let first = command_splits
671 .next()
672 .ok_or("fmt_command cannot be empty".to_string())?;
673
674 let mut comm = Command::new(first);
675 let mut child = comm
676 .args(command_splits)
677 .spawn()
678 .expect("Cannot run the fmt_command");
679
680 println!("running fmt command: {}", fmt_command);
681 child
682 .wait()
683 .expect("fmt command wasn't running")
684 .exit_ok()
685 .map_err(|e| e.to_string())
686}
687
688pub fn handle_files(conf: Config) -> impl Iterator<Item = Bread> {
690 let mut all_files: Vec<File> = files_in_dir_or_file_vec(&conf.files, &conf).unwrap();
692
693 let threads_num: usize = thread::available_parallelism()
695 .unwrap_or(THREAD_NUM.unwrap())
696 .into();
697
698 let len = all_files.len();
699 let count = len / threads_num;
700 let mut groups: Vec<Vec<File>> = vec![];
701 for _ in 0..threads_num - 1 {
702 groups.push(all_files.drain(0..count).collect())
703 }
704 groups.push(all_files.drain(0..).collect());
705
706 let conf = Arc::new(RwLock::new(conf));
707 groups
708 .into_iter()
709 .map(move |fs| {
710 let kwreg = KEYWORDS_REGEX.lock().unwrap().clone();
711 let conf_c = Arc::clone(&conf);
712 thread::spawn(|| {
713 fs.into_iter()
714 .filter_map(move |f| op_file(f, &kwreg, conf_c.clone()).unwrap())
715 .collect::<Vec<Bread>>()
716 })
717 })
718 .map(|han| han.join().unwrap())
719 .flatten()
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 #[test]
727 fn test_files_and_dirs_in_path() -> Result<()> {
728 let (fs, dirs) = files_and_dirs_in_path("./tests/testcases", &Default::default())?;
729
730 assert_eq!(dirs.len(), 0);
731 assert_eq!(fs[0].0, PathBuf::from("./tests/testcases/multilines.rs"),);
732 Ok(())
733 }
734
735 }