Skip to main content

plot/lib/logic/
fs_scanner.rs

1// 3
2use std::{
3	collections::{HashMap, HashSet},
4	fs,
5	io::{self, Read},
6	marker::PhantomData,
7};
8
9use memchr::memchr;
10use walkdir::WalkDir;
11
12// Upewnij się, że PatternsQueries i PattEnvIndex są zaimportowane poprawnie
13use crate::lib::logic::{AnchoredPathsDatum, PathNode, PattEnvIndex, PatternsQueries};
14
15const CHUNK_SIZE: usize = 4096;
16const MAX_SCAN: usize = 16 * 1024; // 16 KB wystarcza w 99.9%
17
18#[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
59// ============================================================================
60// PARTITIONS & MARKERS
61// ============================================================================
62
63pub 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; // Związane z węzłem pliku!
79	fn label() -> &'static str { "✔F" } // ✔️📄
80}
81
82impl MatchLabel for MismatchedFile {
83	type Node = ScannedFileNode; // Związane z węzłem pliku!
84	fn label() -> &'static str { "✖F" } // ✖️📄
85}
86
87impl MatchLabel for MatchedDir {
88	type Node = ScannedDirNode; // Związane z węzłem katalogu!
89	fn label() -> &'static str { "✔D" } // ✔️📂
90}
91
92impl MatchLabel for MismatchedDir {
93	type Node = ScannedDirNode; // Związane z węzłem katalogu!
94	fn label() -> &'static str { "✖D" } // ✖️📂
95}
96
97#[derive(Debug, Clone)]
98pub struct Partition<L: MatchLabel> {
99	pub nodes: Vec<L::Node>, // Kompilator sam podstawi tu ScannedFileNode lub ScannedDirNode
100	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	// Pomocniczy trait-like look dla L::Node, aby dobrać się do pól tier i name
109	pub fn calculate_metadata(nodes: &[L::Node]) -> (usize, usize)
110	where L::Node: AsMetadataNode // Musimy zdefiniować ten pomocniczy trait poniżej
111	{
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}
117// Dodaj ten pomocniczy trait w fs_scanner.rs, aby Partition mogło operować na typach generycznych
118pub 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// ============================================================================
133// STRUKTURY DANYCH I STATYSTYK
134// ============================================================================
135
136#[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
197// ============================================================================
198// ENV INDEX (Baza dla filtrowania)
199// ============================================================================
200
201struct 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// ============================================================================
229// GŁÓWNY OBIEKT WYNIKOWY
230// ============================================================================
231
232/// Skaner i filter systemu plików (Zunifikowane)
233#[derive(Debug, Clone)]
234pub struct PartitionScanned {
235	// Rozbite na konkretne typy partycje (pozbywamy się sztucznego markera Partition)
236	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			// Zliczanie plików bezpośrednich
333			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						// Tylko bezpośrednie dzieci
338						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			// Zliczanie subkatalogów bezpośrednich
348			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		// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
381		// FAZA FILTROWANIA (Rozwiązanie problemu 'Borrow Checkera')
382		// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
383
384		// 1. Zbudowanie EnvIndex na referencjach (pożyczamy ścieżki)
385		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		// 2. Pierwszy przebieg: zbieramy TYLKO wyniki true/false do masek
391		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		/* 3. Uwalniamy referencje - od tego momentu env_index przestaje istnieć,
395		 * więc wektory files i dirs znów są 'wolne' i można je przenosić.
396		 * */
397		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		// 4. Drugi przebieg: złączenie (zip) plików z zapisanymi wynikami (bez żadnego klonowania!)
405		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, // ⚡ Używamy policzonej wartości
441				_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, // ⚡ Używamy policzonej wartości
449				_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, // ⚡ Używamy policzonej wartości
457				_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, // ⚡ Używamy policzonej wartości
465				_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	// =========================================================================
475	// HELPER
476	// =========================================================================
477
478	/// Zwraca połączone i posortowane struktury ScannedNode (pliki + katalogi).
479	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(), // NOWE
485			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(), // NOWE
498			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	// =========================================================================
517	// HELPERY I ITERATORY
518	// =========================================================================
519
520	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}