1use std::{
3 collections::{HashMap, HashSet},
4 fs,
5 io::{self, Read},
6 marker::PhantomData,
7};
8
9use memchr::memchr;
10use walkdir::WalkDir;
11
12use super::{PathNode, PattEnvIndex};
13use crate::lib::{logic::PatternsQueries, schema::ReadyJobExplorer};
14
15const CHUNK_SIZE: usize = 4096;
16const MAX_SCAN: usize = 16 * 1024; #[inline(always)]
19pub fn scan_file_is_binary(path: &str) -> io::Result<bool> {
20 let mut file = fs::File::open(path)?;
21 let mut buf = [0u8; CHUNK_SIZE];
22
23 let mut total_read = 0usize;
24 let mut bad = 0usize;
25
26 loop {
27 let n = file.read(&mut buf)?;
28 if n == 0 {
29 break;
30 }
31
32 let data = &buf[..n];
33 total_read += n;
34
35 if memchr(0, data).is_some() {
36 return Ok(true);
37 }
38
39 for &b in data {
40 if is_control_byte(b) {
41 bad += 1;
42 }
43 }
44
45 if bad * 100 > total_read {
46 return Ok(true);
47 }
48 if total_read >= MAX_SCAN {
49 break;
50 }
51 }
52
53 Ok(false)
54}
55
56#[inline(always)]
57fn is_control_byte(b: u8) -> bool { matches!(b, 0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F) }
58
59pub trait ScanMatchLabel {
64 type Node;
65 fn label() -> &'static str;
66}
67
68#[derive(Debug, Clone)]
69pub struct ScanMatchedFile;
70#[derive(Debug, Clone)]
71pub struct ScanMismatchedFile;
72#[derive(Debug, Clone)]
73pub struct ScanMatchedDir;
74#[derive(Debug, Clone)]
75pub struct ScanMismatchedDir;
76
77impl ScanMatchLabel for ScanMatchedFile {
78 type Node = ScanNodeFileScanned; fn label() -> &'static str { "✔F" } }
81
82impl ScanMatchLabel for ScanMismatchedFile {
83 type Node = ScanNodeFileScanned; fn label() -> &'static str { "✖F" } }
86
87impl ScanMatchLabel for ScanMatchedDir {
88 type Node = ScanNodeDirScanned; fn label() -> &'static str { "✔D" } }
91
92impl ScanMatchLabel for ScanMismatchedDir {
93 type Node = ScanNodeDirScanned; fn label() -> &'static str { "✖D" } }
96
97#[derive(Debug, Clone)]
98pub struct ScanPartition<L: ScanMatchLabel> {
99 pub nodes: Vec<L::Node>, pub label: &'static str,
101 pub tier_max: usize,
102 pub name_len_max: usize,
103 pub path_len_max: usize,
104 pub count: usize,
105 _marker: PhantomData<L>,
106}
107
108impl<L: ScanMatchLabel> ScanPartition<L> {
109 pub fn calculate_metadata(nodes: &[L::Node]) -> (usize, usize, usize)
111 where L::Node: ScanAsMetadataNode {
113 let t_max = nodes.iter().map(|n| n.get_tier()).max().unwrap_or(0);
114 let n_max = nodes.iter().map(|n| n.get_name().chars().count()).max().unwrap_or(0);
115 let p_max = nodes.iter().map(|n| n.get_path_len()).max().unwrap_or(0); (t_max, n_max, p_max)
117 }
118}
119pub trait ScanAsMetadataNode {
121 fn get_tier(&self) -> usize;
122 fn get_name(&self) -> &str;
123 fn get_path_len(&self) -> usize;
124}
125
126impl ScanAsMetadataNode for ScanNodeFileScanned {
127 fn get_tier(&self) -> usize { self.tier }
128 fn get_name(&self) -> &str { &self.name }
129 fn get_path_len(&self) -> usize { self.path.str.chars().count() }
130}
131
132impl ScanAsMetadataNode for ScanNodeDirScanned {
133 fn get_tier(&self) -> usize { self.tier }
134 fn get_name(&self) -> &str { &self.name }
135 fn get_path_len(&self) -> usize { self.path.str.chars().count() }
136}
137#[derive(Debug, Clone)]
142pub struct ScanStatsTreeFsScanned {
143 pub count_dirs: usize,
144 pub count_dirs_empty: usize,
145 pub count_files: usize,
146 pub count_files_empty: usize,
147 pub count_files_text: usize,
148 pub count_files_binary: usize,
149 pub count_symlinks_skipped: usize,
150}
151
152#[derive(Debug, Clone)]
153pub struct ScanStatsPartitioning {
154 pub count_m: usize,
155 pub count_x: usize,
156 pub count_m_f: usize,
157 pub count_x_f: usize,
158 pub count_m_d: usize,
159 pub count_x_d: usize,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum ScanNodeIs {
164 File,
165 Dir,
166}
167
168#[derive(Debug, Clone)]
169pub struct ScanNodeScanned {
170 pub name: String,
171 pub path: PathNode,
172 pub node: ScanNodeIs,
173 pub tier: usize,
174 pub id_self: usize, pub id_path: Vec<usize>, pub dir_has_subdirs: Option<usize>,
177 pub dir_has_files_binary: Option<usize>,
178 pub dir_has_files_text: Option<usize>,
179 pub dir_has_symlinks: Option<usize>,
180 pub file_is_binary: Option<bool>,
181 pub file_is_empty: Option<bool>,
182}
183
184#[derive(Debug, Clone)]
185pub struct ScanNodeDirScanned {
186 pub name: String,
187 pub path: PathNode,
188 pub tier: usize,
189 pub id_self: usize, pub id_path: Vec<usize>, pub has_subdirs: usize,
192 pub has_files_binary: usize,
193 pub has_files_text: usize,
194 pub has_symlinks: usize,
195}
196
197#[derive(Debug, Clone)]
198pub struct ScanNodeFileScanned {
199 pub name: String,
200 pub path: PathNode,
201 pub tier: usize,
202 pub id_self: usize, pub id_path: Vec<usize>, pub is_binary: bool,
205 pub is_empty: bool,
206}
207
208struct EnvIndex<'a> {
213 pub dirs: HashSet<&'a str>,
214 pub files: Vec<&'a str>,
215}
216
217impl<'a> PattEnvIndex for EnvIndex<'a> {
218 fn has_dir(&self, dir: &str) -> bool { self.dirs.contains(dir) }
219
220 fn has_file_with_prefix(&self, prefix: &str) -> bool {
221 let start = self.files.partition_point(|&f| f < prefix);
222 start < self.files.len() && self.files[start].starts_with(prefix)
223 }
224
225 fn any_file_in_dir(&self, dir: &str, check: &mut dyn FnMut(&str) -> bool) -> bool {
226 let start = self.files.partition_point(|&f| f < dir);
227 for &f in &self.files[start..] {
228 if !f.starts_with(dir) {
229 break;
230 }
231 if check(f) {
232 return true;
233 }
234 }
235 false
236 }
237}
238
239#[derive(Debug, Clone)]
245pub struct ScanPartitionScanned {
246 pub m_f: ScanPartition<ScanMatchedFile>,
248 pub x_f: ScanPartition<ScanMismatchedFile>,
249 pub m_d: ScanPartition<ScanMatchedDir>,
250 pub x_d: ScanPartition<ScanMismatchedDir>,
251
252 pub stat_scan: ScanStatsTreeFsScanned,
253 pub stat_part: ScanStatsPartitioning,
254 pub stat_patt: Vec<String>,
255 pub stat_work: String,
256}
257
258impl ScanPartitionScanned {
259 pub fn scan(explorer: &ReadyJobExplorer) -> Self {
260 let q = PatternsQueries::new(explorer.patterns(), explorer.ignore_case());
261 let raw_data = gather_raw_nodes(explorer);
263 let files = raw_data.files;
264 let dirs = build_directory_nodes(&raw_data.dirs_raw, &files, &raw_data.symlink_counts);
265 let stat_scan = raw_data.stat_scan;
266
267 let env_index = EnvIndex {
269 dirs: dirs.iter().map(|d| d.path.str.as_str()).collect(),
270 files: files.iter().map(|f| f.path.str.as_str()).collect(),
271 };
272
273 let files_matches: Vec<bool> = files.iter().map(|f| q.is_match(&f.path.str, &env_index)).collect();
276 let dirs_matches: Vec<bool> = dirs.iter().map(|d| q.is_match(&d.path.str, &env_index)).collect();
277
278 drop(env_index);
280
281 let mut vec_m_f = Vec::new();
282 let mut vec_x_f = Vec::new();
283 let mut vec_m_d = Vec::new();
284 let mut vec_x_d = Vec::new();
285
286 for (f, is_matched) in files.into_iter().zip(files_matches) {
289 if is_matched {
290 vec_m_f.push(f);
291 } else {
292 vec_x_f.push(f);
293 }
294 }
295
296 for (d, is_matched) in dirs.into_iter().zip(dirs_matches) {
297 if is_matched {
298 vec_m_d.push(d);
299 } else {
300 vec_x_d.push(d);
301 }
302 }
303
304 let stat_part = ScanStatsPartitioning {
306 count_m: vec_m_f.len() + vec_m_d.len(),
307 count_x: vec_x_f.len() + vec_x_d.len(),
308 count_m_f: vec_m_f.len(),
309 count_x_f: vec_x_f.len(),
310 count_m_d: vec_m_d.len(),
311 count_x_d: vec_x_d.len(),
312 };
313
314 let (mf_t, mf_n, mf_p) = ScanPartition::<ScanMatchedFile>::calculate_metadata(&vec_m_f);
315 let (xf_t, xf_n, xf_p) = ScanPartition::<ScanMismatchedFile>::calculate_metadata(&vec_x_f);
316 let (md_t, md_n, md_p) = ScanPartition::<ScanMatchedDir>::calculate_metadata(&vec_m_d);
317 let (xd_t, xd_n, xd_p) = ScanPartition::<ScanMismatchedDir>::calculate_metadata(&vec_x_d);
318
319 Self {
320 m_f: ScanPartition {
321 nodes: vec_m_f,
322 label: ScanMatchedFile::label(),
323 tier_max: mf_t,
324 name_len_max: mf_n,
325 path_len_max: mf_p,
326 count: stat_part.count_m_f,
327 _marker: PhantomData,
328 },
329 x_f: ScanPartition {
330 nodes: vec_x_f,
331 label: ScanMismatchedFile::label(),
332 tier_max: xf_t,
333 name_len_max: xf_n,
334 path_len_max: xf_p,
335 count: stat_part.count_x_f,
336 _marker: PhantomData,
337 },
338 m_d: ScanPartition {
339 nodes: vec_m_d,
340 label: ScanMatchedDir::label(),
341 tier_max: md_t,
342 name_len_max: md_n,
343 path_len_max: md_p,
344 count: stat_part.count_m_d,
345 _marker: PhantomData,
346 },
347 x_d: ScanPartition {
348 nodes: vec_x_d,
349 label: ScanMismatchedDir::label(),
350 tier_max: xd_t,
351 name_len_max: xd_n,
352 path_len_max: xd_p,
353 count: stat_part.count_x_d,
354 _marker: PhantomData,
355 },
356 stat_scan,
357 stat_part,
358 stat_patt: q.patterns.0.clone(),
359 stat_work: explorer.workspace_dir().to_string_lossy().into_owned(),
360 }
361 }
362
363 pub fn to_scanned_nodes(&self) -> Vec<ScanNodeScanned> {
369 let total_len = self.stat_part.count_m + self.stat_part.count_x;
370 let mut unified = Vec::with_capacity(total_len);
371
372 let map_dir = |d: &ScanNodeDirScanned| ScanNodeScanned {
373 name: d.name.clone(), path: d.path.clone(),
375 node: ScanNodeIs::Dir,
376 tier: d.tier,
377 id_self: d.id_self, id_path: d.id_path.clone(), dir_has_subdirs: Some(d.has_subdirs),
380 dir_has_files_binary: Some(d.has_files_binary),
381 dir_has_files_text: Some(d.has_files_text),
382 dir_has_symlinks: Some(d.has_symlinks),
383 file_is_binary: None,
384 file_is_empty: None,
385 };
386
387 let map_file = |f: &ScanNodeFileScanned| ScanNodeScanned {
388 name: f.name.clone(), path: f.path.clone(),
390 node: ScanNodeIs::File,
391 tier: f.tier,
392 id_self: f.id_self, id_path: f.id_path.clone(), dir_has_subdirs: None,
395 dir_has_files_binary: None,
396 dir_has_files_text: None,
397 dir_has_symlinks: None,
398 file_is_binary: Some(f.is_binary),
399 file_is_empty: Some(f.is_empty),
400 };
401
402 unified.extend(self.m_d.nodes.iter().chain(self.x_d.nodes.iter()).map(map_dir));
403 unified.extend(self.m_f.nodes.iter().chain(self.x_f.nodes.iter()).map(map_file));
404
405 unified.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
406 unified
407 }
408
409 pub fn iter_nodes(&self) -> impl Iterator<Item = ScanNodeScanned> + '_ { self.to_scanned_nodes().into_iter() }
414
415 pub fn iter_file_paths(&self) -> impl Iterator<Item = &str> + '_ {
416 self.m_f.nodes.iter().chain(self.x_f.nodes.iter()).map(|n| n.path.str.as_str())
417 }
418
419 pub fn iter_dir_paths(&self) -> impl Iterator<Item = &str> + '_ {
420 self.m_d.nodes.iter().chain(self.x_d.nodes.iter()).map(|n| n.path.str.as_str())
421 }
422
423 pub fn iter_paths(&self) -> impl Iterator<Item = &str> + '_ { self.iter_file_paths().chain(self.iter_dir_paths()) }
424}
425
426struct RawDirNode {
431 path: PathNode,
432 tier: usize,
433 id_self: usize,
434 id_path: Vec<usize>,
435}
436struct RawGatherData {
437 files: Vec<ScanNodeFileScanned>,
438 dirs_raw: Vec<RawDirNode>,
439 symlink_counts: HashMap<String, usize>,
440 stat_scan: ScanStatsTreeFsScanned,
441}
442
443fn gather_raw_nodes(explorer: &ReadyJobExplorer) -> RawGatherData {
445 let mut files = Vec::new();
446 let mut dirs_raw = Vec::new();
447 let mut symlink_counts = HashMap::new();
448
449 let mut count_dirs = 0;
450 let mut count_dirs_empty = 0;
451 let mut count_files = 0;
452 let mut count_files_empty = 0;
453 let mut count_files_text = 0;
454 let mut count_files_binary = 0;
455 let mut count_symlinks_skipped = 0;
456
457 let mut current_id = 1; let mut path_registry: HashMap<std::path::PathBuf, (usize, Vec<usize>)> = HashMap::new();
460
461 path_registry.insert(explorer.workspace_dir().clone(), (0, vec![]));
463
464 for e in WalkDir::new(explorer.workspace_dir()).into_iter().filter_map(|e| e.ok()) {
465 if e.depth() == 0 {
466 continue;
467 }
468
469 let tier = e.depth();
470
471 if e.path_is_symlink() {
472 count_symlinks_skipped += 1;
473 if let Some(parent) = e.path().strip_prefix(explorer.workspace_dir()).ok().and_then(|p| p.parent()) {
474 let parent_str = parent.to_string_lossy().replace('\\', "/");
475 let formatted = if parent_str.is_empty() { "./".to_string() } else { format!("./{}/", parent_str) };
476 *symlink_counts.entry(formatted).or_insert(0usize) += 1;
477 }
478 continue;
479 }
480
481 let id_self = current_id;
483 current_id += 1;
484
485 let parent_path = e.path().parent().expect("Błąd systemu plików: Węzeł nie ma rodzica");
487 let (parent_id, parent_id_path) = path_registry.get(parent_path).unwrap_or(&(0, vec![])).clone();
488
489 let mut id_path = parent_id_path;
492 id_path.push(parent_id);
493
494 if e.file_type().is_dir() {
496 path_registry.insert(e.path().to_path_buf(), (id_self, id_path.clone()));
497 }
498
499 let Ok(rel_path) = e.path().strip_prefix(explorer.workspace_dir()) else {
500 continue;
501 };
502 let mut path = rel_path.to_string_lossy().replace('\\', "/");
503
504 if e.file_type().is_dir() {
505 path.push('/');
506 count_dirs += 1;
507
508 let is_empty = e.metadata().map(|m| m.is_dir()).unwrap_or(false)
509 && e.path().read_dir().map(|mut r| r.next().is_none()).unwrap_or(false);
510
511 if is_empty {
512 count_dirs_empty += 1;
513 }
514 dirs_raw.push(RawDirNode {
515 path: PathNode::new(format!("./{}", path).into()),
516 tier,
517 id_self,
518 id_path: id_path.clone(),
519 });
520 } else {
521 count_files += 1;
522 let abs_path = e.path();
523 let is_empty = abs_path.metadata().map(|m| m.len() == 0).unwrap_or(false);
524 let mut is_binary = false;
525
526 if is_empty {
527 count_files_empty += 1;
528 } else {
529 is_binary = scan_file_is_binary(abs_path.to_str().unwrap_or("")).unwrap_or(false);
530 if is_binary {
531 count_files_binary += 1;
532 } else {
533 count_files_text += 1;
534 }
535 }
536
537 files.push(ScanNodeFileScanned {
538 name: abs_path.file_name().unwrap_or_default().to_string_lossy().to_string(),
539 path: PathNode::new(format!("./{}", path).into()),
540 id_self,
541 id_path,
542 tier,
543 is_binary,
544 is_empty,
545 });
546 }
547 }
548
549 files.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
551 dirs_raw.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
552
553 let stat_scan = ScanStatsTreeFsScanned {
554 count_dirs,
555 count_dirs_empty,
556 count_files,
557 count_files_empty,
558 count_files_text,
559 count_files_binary,
560 count_symlinks_skipped,
561 };
562
563 RawGatherData { files, dirs_raw, symlink_counts, stat_scan }
564}
565
566fn build_directory_nodes(
568 dirs_raw: &[RawDirNode],
569 files: &[ScanNodeFileScanned],
570 symlink_counts: &HashMap<String, usize>,
571) -> Vec<ScanNodeDirScanned> {
572 let mut dirs = Vec::with_capacity(dirs_raw.len());
573
574 for i in 0..dirs_raw.len() {
575 let raw = &dirs_raw[i];
576 let mut count_subdirs = 0;
577 let mut count_files_binary = 0;
578 let mut count_files_text = 0;
579 let count_symlinks = *symlink_counts.get(&raw.path.str).unwrap_or(&0);
580
581 for f in files {
583 if f.path.str.starts_with(&raw.path.str) {
584 let remainder = &f.path.str[raw.path.str.len()..];
585 if !remainder.contains('/') {
586 if f.is_binary {
587 count_files_binary += 1;
588 } else if !f.is_empty {
589 count_files_text += 1;
590 }
591 }
592 }
593 }
594
595 for j in (i + 1)..dirs_raw.len() {
597 if dirs_raw[j].path.str.starts_with(&raw.path.str) {
598 let remainder = &dirs_raw[j].path.str[raw.path.str.len()..];
599 if !remainder.is_empty() && !remainder.trim_end_matches('/').contains('/') {
600 count_subdirs += 1;
601 }
602 } else {
603 break;
604 }
605 }
606
607 dirs.push(ScanNodeDirScanned {
608 name: raw.path.str.trim_end_matches('/').split('/').next_back().unwrap_or("").to_string(),
609 path: raw.path.clone(),
610 id_self: raw.id_self, id_path: raw.id_path.clone(), tier: raw.tier,
613 has_subdirs: count_subdirs,
614 has_files_binary: count_files_binary,
615 has_files_text: count_files_text,
616 has_symlinks: count_symlinks,
617 });
618 }
619
620 dirs
621}