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 crate::lib::logic::{AnchoredPathsDatum, PathNode, PattEnvIndex, PatternsQueries};
14
15const CHUNK_SIZE: usize = 4096;
16const MAX_SCAN: usize = 16 * 1024; #[inline(always)]
19pub fn is_binary_file(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 MatchLabel {
64 type Node;
65 fn label() -> &'static str;
66}
67
68#[derive(Debug, Clone)]
69pub struct MatchedFile;
70#[derive(Debug, Clone)]
71pub struct MismatchedFile;
72#[derive(Debug, Clone)]
73pub struct MatchedDir;
74#[derive(Debug, Clone)]
75pub struct MismatchedDir;
76
77impl MatchLabel for MatchedFile {
78 type Node = ScannedFileNode; fn label() -> &'static str { "✔F" } }
81
82impl MatchLabel for MismatchedFile {
83 type Node = ScannedFileNode; fn label() -> &'static str { "✖F" } }
86
87impl MatchLabel for MatchedDir {
88 type Node = ScannedDirNode; fn label() -> &'static str { "✔D" } }
91
92impl MatchLabel for MismatchedDir {
93 type Node = ScannedDirNode; fn label() -> &'static str { "✖D" } }
96
97#[derive(Debug, Clone)]
98pub struct Partition<L: MatchLabel> {
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: MatchLabel> Partition<L> {
109 pub fn calculate_metadata(nodes: &[L::Node]) -> (usize, usize, usize)
111 where L::Node: AsMetadataNode {
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 AsMetadataNode {
121 fn get_tier(&self) -> usize;
122 fn get_name(&self) -> &str;
123 fn get_path_len(&self) -> usize;
124}
125
126impl AsMetadataNode for ScannedFileNode {
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 AsMetadataNode for ScannedDirNode {
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 StatsScannedTreeFs {
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 StatsPartitioning {
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 NodeIs {
164 File,
165 Dir,
166}
167
168#[derive(Debug, Clone)]
169pub struct ScannedNode {
170 pub name: String,
171 pub path: PathNode,
172 pub node: NodeIs,
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 ScannedDirNode {
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 ScannedFileNode {
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 PartitionScanned {
246 pub m_f: Partition<MatchedFile>,
248 pub x_f: Partition<MismatchedFile>,
249 pub m_d: Partition<MatchedDir>,
250 pub x_d: Partition<MismatchedDir>,
251
252 pub stat_scan: StatsScannedTreeFs,
253 pub stat_part: StatsPartitioning,
254 pub stat_patt: Vec<String>,
255 pub stat_work: String,
256}
257
258impl PartitionScanned {
259 pub fn scan(p: &AnchoredPathsDatum, q: &PatternsQueries) -> Self {
260 let raw_data = gather_raw_nodes(p);
262 let files = raw_data.files;
263 let dirs = build_directory_nodes(&raw_data.dirs_raw, &files, &raw_data.symlink_counts);
264 let stat_scan = raw_data.stat_scan;
265
266 let env_index = EnvIndex {
268 dirs: dirs.iter().map(|d| d.path.str.as_str()).collect(),
269 files: files.iter().map(|f| f.path.str.as_str()).collect(),
270 };
271
272 let files_matches: Vec<bool> = files.iter().map(|f| q.is_match(&f.path.str, &env_index)).collect();
275 let dirs_matches: Vec<bool> = dirs.iter().map(|d| q.is_match(&d.path.str, &env_index)).collect();
276
277 drop(env_index);
279
280 let mut vec_m_f = Vec::new();
281 let mut vec_x_f = Vec::new();
282 let mut vec_m_d = Vec::new();
283 let mut vec_x_d = Vec::new();
284
285 for (f, is_matched) in files.into_iter().zip(files_matches) {
288 if is_matched {
289 vec_m_f.push(f);
290 } else {
291 vec_x_f.push(f);
292 }
293 }
294
295 for (d, is_matched) in dirs.into_iter().zip(dirs_matches) {
296 if is_matched {
297 vec_m_d.push(d);
298 } else {
299 vec_x_d.push(d);
300 }
301 }
302
303 let stat_part = StatsPartitioning {
305 count_m: vec_m_f.len() + vec_m_d.len(),
306 count_x: vec_x_f.len() + vec_x_d.len(),
307 count_m_f: vec_m_f.len(),
308 count_x_f: vec_x_f.len(),
309 count_m_d: vec_m_d.len(),
310 count_x_d: vec_x_d.len(),
311 };
312
313 let (mf_t, mf_n, mf_p) = Partition::<MatchedFile>::calculate_metadata(&vec_m_f);
314 let (xf_t, xf_n, xf_p) = Partition::<MismatchedFile>::calculate_metadata(&vec_x_f);
315 let (md_t, md_n, md_p) = Partition::<MatchedDir>::calculate_metadata(&vec_m_d);
316 let (xd_t, xd_n, xd_p) = Partition::<MismatchedDir>::calculate_metadata(&vec_x_d);
317
318 Self {
319 m_f: Partition {
320 nodes: vec_m_f,
321 label: MatchedFile::label(),
322 tier_max: mf_t,
323 name_len_max: mf_n,
324 path_len_max: mf_p,
325 count: stat_part.count_m_f,
326 _marker: PhantomData,
327 },
328 x_f: Partition {
329 nodes: vec_x_f,
330 label: MismatchedFile::label(),
331 tier_max: xf_t,
332 name_len_max: xf_n,
333 path_len_max: xf_p,
334 count: stat_part.count_x_f,
335 _marker: PhantomData,
336 },
337 m_d: Partition {
338 nodes: vec_m_d,
339 label: MatchedDir::label(),
340 tier_max: md_t,
341 name_len_max: md_n,
342 path_len_max: md_p,
343 count: stat_part.count_m_d,
344 _marker: PhantomData,
345 },
346 x_d: Partition {
347 nodes: vec_x_d,
348 label: MismatchedDir::label(),
349 tier_max: xd_t,
350 name_len_max: xd_n,
351 path_len_max: xd_p,
352 count: stat_part.count_x_d,
353 _marker: PhantomData,
354 },
355 stat_scan,
356 stat_part,
357 stat_patt: q.patterns.0.clone(),
358 stat_work: p.workspace_dir.str.clone(),
359 }
360 }
361
362 pub fn to_scanned_nodes(&self) -> Vec<ScannedNode> {
368 let total_len = self.stat_part.count_m + self.stat_part.count_x;
369 let mut unified = Vec::with_capacity(total_len);
370
371 let map_dir = |d: &ScannedDirNode| ScannedNode {
372 name: d.name.clone(), path: d.path.clone(),
374 node: NodeIs::Dir,
375 tier: d.tier,
376 id_self: d.id_self, id_path: d.id_path.clone(), dir_has_subdirs: Some(d.has_subdirs),
379 dir_has_files_binary: Some(d.has_files_binary),
380 dir_has_files_text: Some(d.has_files_text),
381 dir_has_symlinks: Some(d.has_symlinks),
382 file_is_binary: None,
383 file_is_empty: None,
384 };
385
386 let map_file = |f: &ScannedFileNode| ScannedNode {
387 name: f.name.clone(), path: f.path.clone(),
389 node: NodeIs::File,
390 tier: f.tier,
391 id_self: f.id_self, id_path: f.id_path.clone(), dir_has_subdirs: None,
394 dir_has_files_binary: None,
395 dir_has_files_text: None,
396 dir_has_symlinks: None,
397 file_is_binary: Some(f.is_binary),
398 file_is_empty: Some(f.is_empty),
399 };
400
401 unified.extend(self.m_d.nodes.iter().chain(self.x_d.nodes.iter()).map(map_dir));
402 unified.extend(self.m_f.nodes.iter().chain(self.x_f.nodes.iter()).map(map_file));
403
404 unified.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
405 unified
406 }
407
408 pub fn iter_nodes(&self) -> impl Iterator<Item = ScannedNode> + '_ { self.to_scanned_nodes().into_iter() }
413
414 pub fn iter_file_paths(&self) -> impl Iterator<Item = &str> + '_ {
415 self.m_f.nodes.iter().chain(self.x_f.nodes.iter()).map(|n| n.path.str.as_str())
416 }
417
418 pub fn iter_dir_paths(&self) -> impl Iterator<Item = &str> + '_ {
419 self.m_d.nodes.iter().chain(self.x_d.nodes.iter()).map(|n| n.path.str.as_str())
420 }
421
422 pub fn iter_paths(&self) -> impl Iterator<Item = &str> + '_ { self.iter_file_paths().chain(self.iter_dir_paths()) }
423}
424
425struct RawDirNode {
430 path: PathNode,
431 tier: usize,
432 id_self: usize,
433 id_path: Vec<usize>,
434}
435struct RawGatherData {
436 files: Vec<ScannedFileNode>,
437 dirs_raw: Vec<RawDirNode>,
438 symlink_counts: HashMap<String, usize>,
439 stat_scan: StatsScannedTreeFs,
440}
441
442fn gather_raw_nodes(p: &AnchoredPathsDatum) -> RawGatherData {
444 let mut files = Vec::new();
445 let mut dirs_raw = Vec::new();
446 let mut symlink_counts = HashMap::new();
447
448 let mut count_dirs = 0;
449 let mut count_dirs_empty = 0;
450 let mut count_files = 0;
451 let mut count_files_empty = 0;
452 let mut count_files_text = 0;
453 let mut count_files_binary = 0;
454 let mut count_symlinks_skipped = 0;
455
456 let mut current_id = 1; let mut path_registry: HashMap<std::path::PathBuf, (usize, Vec<usize>)> = HashMap::new();
459
460 path_registry.insert(p.workspace_dir.buf.clone(), (0, vec![]));
462
463 for e in WalkDir::new(&p.workspace_dir.buf).into_iter().filter_map(|e| e.ok()) {
464 if e.depth() == 0 {
465 continue;
466 }
467
468 let tier = e.depth();
469
470 if e.path_is_symlink() {
471 count_symlinks_skipped += 1;
472 if let Some(parent) = e.path().strip_prefix(&p.workspace_dir.buf).ok().and_then(|p| p.parent()) {
473 let parent_str = parent.to_string_lossy().replace('\\', "/");
474 let formatted = if parent_str.is_empty() { "./".to_string() } else { format!("./{}/", parent_str) };
475 *symlink_counts.entry(formatted).or_insert(0usize) += 1;
476 }
477 continue;
478 }
479
480 let id_self = current_id;
482 current_id += 1;
483
484 let parent_path = e.path().parent().expect("Błąd systemu plików: Węzeł nie ma rodzica");
486 let (parent_id, parent_id_path) = path_registry.get(parent_path).unwrap_or(&(0, vec![])).clone();
487
488 let mut id_path = parent_id_path;
491 id_path.push(parent_id);
492
493 if e.file_type().is_dir() {
495 path_registry.insert(e.path().to_path_buf(), (id_self, id_path.clone()));
496 }
497
498 let Ok(rel_path) = e.path().strip_prefix(&p.workspace_dir.buf) else {
499 continue;
500 };
501 let mut path = rel_path.to_string_lossy().replace('\\', "/");
502
503 if e.file_type().is_dir() {
504 path.push('/');
505 count_dirs += 1;
506
507 let is_empty = e.metadata().map(|m| m.is_dir()).unwrap_or(false)
508 && e.path().read_dir().map(|mut r| r.next().is_none()).unwrap_or(false);
509
510 if is_empty {
511 count_dirs_empty += 1;
512 }
513 dirs_raw.push(RawDirNode {
514 path: PathNode::new(format!("./{}", path).into()),
515 tier,
516 id_self,
517 id_path: id_path.clone(),
518 });
519 } else {
520 count_files += 1;
521 let abs_path = e.path();
522 let is_empty = abs_path.metadata().map(|m| m.len() == 0).unwrap_or(false);
523 let mut is_binary = false;
524
525 if is_empty {
526 count_files_empty += 1;
527 } else {
528 is_binary = is_binary_file(abs_path.to_str().unwrap_or("")).unwrap_or(false);
529 if is_binary {
530 count_files_binary += 1;
531 } else {
532 count_files_text += 1;
533 }
534 }
535
536 files.push(ScannedFileNode {
537 name: abs_path.file_name().unwrap_or_default().to_string_lossy().to_string(),
538 path: PathNode::new(format!("./{}", path).into()),
539 id_self,
540 id_path,
541 tier,
542 is_binary,
543 is_empty,
544 });
545 }
546 }
547
548 files.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
550 dirs_raw.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
551
552 let stat_scan = StatsScannedTreeFs {
553 count_dirs,
554 count_dirs_empty,
555 count_files,
556 count_files_empty,
557 count_files_text,
558 count_files_binary,
559 count_symlinks_skipped,
560 };
561
562 RawGatherData { files, dirs_raw, symlink_counts, stat_scan }
563}
564
565fn build_directory_nodes(
567 dirs_raw: &[RawDirNode],
568 files: &[ScannedFileNode],
569 symlink_counts: &HashMap<String, usize>,
570) -> Vec<ScannedDirNode> {
571 let mut dirs = Vec::with_capacity(dirs_raw.len());
572
573 for i in 0..dirs_raw.len() {
574 let raw = &dirs_raw[i];
575 let mut count_subdirs = 0;
576 let mut count_files_binary = 0;
577 let mut count_files_text = 0;
578 let count_symlinks = *symlink_counts.get(&raw.path.str).unwrap_or(&0);
579
580 for f in files {
582 if f.path.str.starts_with(&raw.path.str) {
583 let remainder = &f.path.str[raw.path.str.len()..];
584 if !remainder.contains('/') {
585 if f.is_binary {
586 count_files_binary += 1;
587 } else if !f.is_empty {
588 count_files_text += 1;
589 }
590 }
591 }
592 }
593
594 for j in (i + 1)..dirs_raw.len() {
596 if dirs_raw[j].path.str.starts_with(&raw.path.str) {
597 let remainder = &dirs_raw[j].path.str[raw.path.str.len()..];
598 if !remainder.is_empty() && !remainder.trim_end_matches('/').contains('/') {
599 count_subdirs += 1;
600 }
601 } else {
602 break;
603 }
604 }
605
606 dirs.push(ScannedDirNode {
607 name: raw.path.str.trim_end_matches('/').split('/').next_back().unwrap_or("").to_string(),
608 path: raw.path.clone(),
609 id_self: raw.id_self, id_path: raw.id_path.clone(), tier: raw.tier,
612 has_subdirs: count_subdirs,
613 has_files_binary: count_files_binary,
614 has_files_text: count_files_text,
615 has_symlinks: count_symlinks,
616 });
617 }
618
619 dirs
620}