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 count: usize,
104 _marker: PhantomData<L>,
105}
106
107impl<L: MatchLabel> Partition<L> {
108 pub fn calculate_metadata(nodes: &[L::Node]) -> (usize, usize)
110 where L::Node: AsMetadataNode {
112 let t_max = nodes.iter().map(|n| n.get_tier()).max().unwrap_or(0);
113 let n_max = nodes.iter().map(|n| n.get_name().chars().count()).max().unwrap_or(0);
114 (t_max, n_max)
115 }
116}
117pub trait AsMetadataNode {
119 fn get_tier(&self) -> usize;
120 fn get_name(&self) -> &str;
121}
122
123impl AsMetadataNode for ScannedFileNode {
124 fn get_tier(&self) -> usize { self.tier }
125 fn get_name(&self) -> &str { &self.name }
126}
127
128impl AsMetadataNode for ScannedDirNode {
129 fn get_tier(&self) -> usize { self.tier }
130 fn get_name(&self) -> &str { &self.name }
131}
132#[derive(Debug, Clone)]
137pub struct StatsScannedTreeFs {
138 pub count_dirs: usize,
139 pub count_dirs_empty: usize,
140 pub count_files: usize,
141 pub count_files_empty: usize,
142 pub count_files_text: usize,
143 pub count_files_binary: usize,
144 pub count_symlinks_skipped: usize,
145}
146
147#[derive(Debug, Clone)]
148pub struct StatsPartitioning {
149 pub count_m: usize,
150 pub count_x: usize,
151 pub count_m_f: usize,
152 pub count_x_f: usize,
153 pub count_m_d: usize,
154 pub count_x_d: usize,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum NodeIs {
159 File,
160 Dir,
161}
162
163#[derive(Debug, Clone)]
164pub struct ScannedNode {
165 pub name: String,
166 pub path: PathNode,
167 pub node: NodeIs,
168 pub tier: usize,
169 pub dir_has_subdirs: Option<usize>,
170 pub dir_has_files_binary: Option<usize>,
171 pub dir_has_files_text: Option<usize>,
172 pub dir_has_symlinks: Option<usize>,
173 pub file_is_binary: Option<bool>,
174 pub file_is_empty: Option<bool>,
175}
176
177#[derive(Debug, Clone)]
178pub struct ScannedDirNode {
179 pub name: String,
180 pub path: PathNode,
181 pub tier: usize,
182 pub has_subdirs: usize,
183 pub has_files_binary: usize,
184 pub has_files_text: usize,
185 pub has_symlinks: usize,
186}
187
188#[derive(Debug, Clone)]
189pub struct ScannedFileNode {
190 pub name: String,
191 pub path: PathNode,
192 pub tier: usize,
193 pub is_binary: bool,
194 pub is_empty: bool,
195}
196
197struct EnvIndex<'a> {
202 pub dirs: HashSet<&'a str>,
203 pub files: Vec<&'a str>,
204}
205
206impl<'a> PattEnvIndex for EnvIndex<'a> {
207 fn has_dir(&self, dir: &str) -> bool { self.dirs.contains(dir) }
208
209 fn has_file_with_prefix(&self, prefix: &str) -> bool {
210 let start = self.files.partition_point(|&f| f < prefix);
211 start < self.files.len() && self.files[start].starts_with(prefix)
212 }
213
214 fn any_file_in_dir(&self, dir: &str, check: &mut dyn FnMut(&str) -> bool) -> bool {
215 let start = self.files.partition_point(|&f| f < dir);
216 for &f in &self.files[start..] {
217 if !f.starts_with(dir) {
218 break;
219 }
220 if check(f) {
221 return true;
222 }
223 }
224 false
225 }
226}
227
228#[derive(Debug, Clone)]
234pub struct PartitionScanned {
235 pub m_f: Partition<MatchedFile>,
237 pub x_f: Partition<MismatchedFile>,
238 pub m_d: Partition<MatchedDir>,
239 pub x_d: Partition<MismatchedDir>,
240
241 pub stat_scan: StatsScannedTreeFs,
242 pub stat_part: StatsPartitioning,
243 pub stat_patt: Vec<String>,
244 pub stat_work: String,
245}
246
247impl PartitionScanned {
248 pub fn scan(p: &AnchoredPathsDatum, q: &PatternsQueries) -> Self {
249 let mut files = Vec::new();
250 let mut dirs_raw = Vec::new();
251 let mut symlink_counts = HashMap::new();
252
253 let mut count_dirs = 0;
254 let mut count_dirs_empty = 0;
255 let mut count_files = 0;
256 let mut count_files_empty = 0;
257 let mut count_files_text = 0;
258 let mut count_files_binary = 0;
259 let mut count_symlinks_skipped = 0;
260
261 for e in WalkDir::new(&p.workspace_dir.buf).into_iter().filter_map(|e| e.ok()) {
262 if e.depth() == 0 {
263 continue;
264 }
265
266 let tier = e.depth();
267
268 if e.path_is_symlink() {
269 count_symlinks_skipped += 1;
270 if let Some(parent) = e.path().strip_prefix(&p.workspace_dir.buf).ok().and_then(|p| p.parent()) {
271 let parent_str = parent.to_string_lossy().replace('\\', "/");
272 let formatted = if parent_str.is_empty() { "./".to_string() } else { format!("./{}/", parent_str) };
273 *symlink_counts.entry(formatted).or_insert(0usize) += 1;
274 }
275 continue;
276 }
277
278 let Ok(rel_path) = e.path().strip_prefix(&p.workspace_dir.buf) else {
279 continue;
280 };
281 let mut path = rel_path.to_string_lossy().replace('\\', "/");
282
283 if e.file_type().is_dir() {
284 path.push('/');
285 count_dirs += 1;
286
287 let is_empty = e.metadata().map(|m| m.is_dir()).unwrap_or(false)
288 && e.path().read_dir().map(|mut r| r.next().is_none()).unwrap_or(false);
289
290 if is_empty {
291 count_dirs_empty += 1;
292 }
293 dirs_raw.push((PathNode::new(format!("./{}", path).into()), tier));
294 } else {
295 count_files += 1;
296 let abs_path = e.path();
297 let is_empty = abs_path.metadata().map(|m| m.len() == 0).unwrap_or(false);
298 let mut is_binary = false;
299
300 if is_empty {
301 count_files_empty += 1;
302 } else {
303 is_binary = is_binary_file(abs_path.to_str().unwrap_or("")).unwrap_or(false);
304 if is_binary {
305 count_files_binary += 1;
306 } else {
307 count_files_text += 1;
308 }
309 }
310
311 files.push(ScannedFileNode {
312 name: abs_path.file_name().unwrap_or_default().to_string_lossy().to_string(),
313 path: PathNode::new(format!("./{}", path).into()),
314 tier,
315 is_binary,
316 is_empty,
317 });
318 }
319 }
320
321 files.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
322 dirs_raw.sort_unstable_by(|a, b| a.0.str.cmp(&b.0.str));
323
324 let mut dirs = Vec::with_capacity(dirs_raw.len());
325 for i in 0..dirs_raw.len() {
326 let (path, tier) = dirs_raw[i].clone();
327 let mut count_subdirs = 0;
328 let mut count_files_binary = 0;
329 let mut count_files_text = 0;
330 let count_symlinks = *symlink_counts.get(&path.str).unwrap_or(&0);
331
332 for f in &files {
334 if f.path.str.starts_with(&path.str) {
335 let remainder = &f.path.str[path.str.len()..];
336 if !remainder.contains('/') {
337 if f.is_binary {
339 count_files_binary += 1;
340 } else if !f.is_empty {
341 count_files_text += 1;
342 }
343 }
344 }
345 }
346
347 for j in (i + 1)..dirs_raw.len() {
349 if dirs_raw[j].0.str.starts_with(&path.str) {
350 let remainder = &dirs_raw[j].0.str[path.str.len()..];
351 if !remainder.is_empty() && !remainder.trim_end_matches('/').contains('/') {
352 count_subdirs += 1;
353 }
354 } else {
355 break;
356 }
357 }
358
359 dirs.push(ScannedDirNode {
360 name: path.str.trim_end_matches('/').split('/').next_back().unwrap_or("").to_string(),
361 path,
362 tier,
363 has_subdirs: count_subdirs,
364 has_files_binary: count_files_binary,
365 has_files_text: count_files_text,
366 has_symlinks: count_symlinks,
367 });
368 }
369
370 let stat_scan = StatsScannedTreeFs {
371 count_dirs,
372 count_dirs_empty,
373 count_files,
374 count_files_empty,
375 count_files_text,
376 count_files_binary,
377 count_symlinks_skipped,
378 };
379
380 let env_index = EnvIndex {
386 dirs: dirs.iter().map(|d| d.path.str.as_str()).collect(),
387 files: files.iter().map(|f| f.path.str.as_str()).collect(),
388 };
389
390 let files_matches: Vec<bool> = files.iter().map(|f| q.is_match(&f.path.str, &env_index)).collect();
392 let dirs_matches: Vec<bool> = dirs.iter().map(|d| q.is_match(&d.path.str, &env_index)).collect();
393
394 drop(env_index);
398
399 let mut vec_m_f = Vec::new();
400 let mut vec_x_f = Vec::new();
401 let mut vec_m_d = Vec::new();
402 let mut vec_x_d = Vec::new();
403
404 for (f, is_matched) in files.into_iter().zip(files_matches) {
406 if is_matched {
407 vec_m_f.push(f);
408 } else {
409 vec_x_f.push(f);
410 }
411 }
412
413 for (d, is_matched) in dirs.into_iter().zip(dirs_matches) {
414 if is_matched {
415 vec_m_d.push(d);
416 } else {
417 vec_x_d.push(d);
418 }
419 }
420
421 let stat_part = StatsPartitioning {
422 count_m: vec_m_f.len() + vec_m_d.len(),
423 count_x: vec_x_f.len() + vec_x_d.len(),
424 count_m_f: vec_m_f.len(),
425 count_x_f: vec_x_f.len(),
426 count_m_d: vec_m_d.len(),
427 count_x_d: vec_x_d.len(),
428 };
429 let (mf_t, mf_n) = Partition::<MatchedFile>::calculate_metadata(&vec_m_f);
430 let (xf_t, xf_n) = Partition::<MismatchedFile>::calculate_metadata(&vec_x_f);
431 let (md_t, md_n) = Partition::<MatchedDir>::calculate_metadata(&vec_m_d);
432 let (xd_t, xd_n) = Partition::<MismatchedDir>::calculate_metadata(&vec_x_d);
433
434 Self {
435 m_f: Partition {
436 nodes: vec_m_f,
437 label: MatchedFile::label(),
438 tier_max: mf_t,
439 name_len_max: mf_n,
440 count: stat_part.count_m_f, _marker: PhantomData,
442 },
443 x_f: Partition {
444 nodes: vec_x_f,
445 label: MismatchedFile::label(),
446 tier_max: xf_t,
447 name_len_max: xf_n,
448 count: stat_part.count_x_f, _marker: PhantomData,
450 },
451 m_d: Partition {
452 nodes: vec_m_d,
453 label: MatchedDir::label(),
454 tier_max: md_t,
455 name_len_max: md_n,
456 count: stat_part.count_m_d, _marker: PhantomData,
458 },
459 x_d: Partition {
460 nodes: vec_x_d,
461 label: MismatchedDir::label(),
462 tier_max: xd_t,
463 name_len_max: xd_n,
464 count: stat_part.count_x_d, _marker: PhantomData,
466 },
467 stat_scan,
468 stat_part,
469 stat_patt: q.patterns.0.clone(),
470 stat_work: p.workspace_dir.str.clone(),
471 }
472 }
473
474 pub fn to_scanned_nodes(&self) -> Vec<ScannedNode> {
480 let total_len = self.stat_part.count_m + self.stat_part.count_x;
481 let mut unified = Vec::with_capacity(total_len);
482
483 let map_dir = |d: &ScannedDirNode| ScannedNode {
484 name: d.name.clone(), path: d.path.clone(),
486 node: NodeIs::Dir,
487 tier: d.tier,
488 dir_has_subdirs: Some(d.has_subdirs),
489 dir_has_files_binary: Some(d.has_files_binary),
490 dir_has_files_text: Some(d.has_files_text),
491 dir_has_symlinks: Some(d.has_symlinks),
492 file_is_binary: None,
493 file_is_empty: None,
494 };
495
496 let map_file = |f: &ScannedFileNode| ScannedNode {
497 name: f.name.clone(), path: f.path.clone(),
499 node: NodeIs::File,
500 tier: f.tier,
501 dir_has_subdirs: None,
502 dir_has_files_binary: None,
503 dir_has_files_text: None,
504 dir_has_symlinks: None,
505 file_is_binary: Some(f.is_binary),
506 file_is_empty: Some(f.is_empty),
507 };
508
509 unified.extend(self.m_d.nodes.iter().chain(self.x_d.nodes.iter()).map(map_dir));
510 unified.extend(self.m_f.nodes.iter().chain(self.x_f.nodes.iter()).map(map_file));
511
512 unified.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
513 unified
514 }
515
516 pub fn iter_nodes(&self) -> impl Iterator<Item = ScannedNode> + '_ { self.to_scanned_nodes().into_iter() }
521
522 pub fn iter_file_paths(&self) -> impl Iterator<Item = &str> + '_ {
523 self.m_f.nodes.iter().chain(self.x_f.nodes.iter()).map(|n| n.path.str.as_str())
524 }
525
526 pub fn iter_dir_paths(&self) -> impl Iterator<Item = &str> + '_ {
527 self.m_d.nodes.iter().chain(self.x_d.nodes.iter()).map(|n| n.path.str.as_str())
528 }
529
530 pub fn iter_paths(&self) -> impl Iterator<Item = &str> + '_ { self.iter_file_paths().chain(self.iter_dir_paths()) }
531}