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