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
12use crate::lib::job;
13// Upewnij się, że PatternsQueries i PattEnvIndex są zaimportowane poprawnie
14use crate::lib::logic::{PathNode, PattEnvIndex};
15
16const CHUNK_SIZE: usize = 4096;
17const MAX_SCAN: usize = 16 * 1024; // 16 KB wystarcza w 99.9%
18
19#[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
60// ============================================================================
61// PARTITIONS & MARKERS
62// ============================================================================
63
64pub 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; // Związane z węzłem pliku!
80	fn label() -> &'static str { "✔F" } // ✔️📄
81}
82
83impl MatchLabel for MismatchedFile {
84	type Node = ScannedFileNode; // Związane z węzłem pliku!
85	fn label() -> &'static str { "✖F" } // ✖️📄
86}
87
88impl MatchLabel for MatchedDir {
89	type Node = ScannedDirNode; // Związane z węzłem katalogu!
90	fn label() -> &'static str { "✔D" } // ✔️📂
91}
92
93impl MatchLabel for MismatchedDir {
94	type Node = ScannedDirNode; // Związane z węzłem katalogu!
95	fn label() -> &'static str { "✖D" } // ✖️📂
96}
97
98#[derive(Debug, Clone)]
99pub struct Partition<L: MatchLabel> {
100	pub nodes: Vec<L::Node>, // Kompilator sam podstawi tu ScannedFileNode lub ScannedDirNode
101	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	// Pomocniczy trait-like look dla L::Node, aby dobrać się do pól tier i name
111	pub fn calculate_metadata(nodes: &[L::Node]) -> (usize, usize, usize)
112	where L::Node: AsMetadataNode // Musimy zdefiniować ten pomocniczy trait poniżej
113	{
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); // ⚡ NOWE
117		(t_max, n_max, p_max)
118	}
119}
120// Dodaj ten pomocniczy trait w fs_scanner.rs, aby Partition mogło operować na typach generycznych
121pub 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// ============================================================================
139// STRUKTURY DANYCH I STATYSTYK
140// ============================================================================
141
142#[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,      // ⚡ Własne ID
176	pub id_path: Vec<usize>, // ⚡ Ścieżka przodków [0, 1, 5...]
177	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,      // ⚡
191	pub id_path: Vec<usize>, // ⚡
192	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,      // ⚡
204	pub id_path: Vec<usize>, // ⚡
205	pub is_binary: bool,
206	pub is_empty: bool,
207}
208
209// ============================================================================
210// ENV INDEX (Baza dla filtrowania)
211// ============================================================================
212
213struct 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// ============================================================================
241// GŁÓWNY OBIEKT WYNIKOWY
242// ============================================================================
243
244/// Skaner i filter systemu plików (Zunifikowane)
245#[derive(Debug, Clone)]
246pub struct PartitionScanned {
247	// Rozbite na konkretne typy partycje (pozbywamy się sztucznego markera Partition)
248	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		// ░░░░░░░░░░░░░░░ 🅰️ ZBIERANIE DANYCH ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
263		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		// Zbudowanie EnvIndex na referencjach (pożyczamy ścieżki)
269		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		// ░░░░░░░░░░░░░░░ 🅱️ PIERWSZY PRZEBIEG ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
275		// zbieramy TYLKO wyniki true/false do masek
276		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		// Uwalniamy referencje
280		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		// ░░░░░░░░░░░░░░░ 🆎 DRUGI PRZEBIEG ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
288		// złączenie plików z zapisanymi wynikami
289		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		// ░░░░░░░░░░░░░░░ 🅾️ MONTOWANIE WYNIKU ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
306		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	// =========================================================================
371	// HELPER
372	// =========================================================================
373
374	/// Zwraca połączone i posortowane struktury ScannedNode (pliki + katalogi).
375	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(), // NOWE
381			path: d.path.clone(),
382			node: NodeIs::Dir,
383			tier: d.tier,
384			id_self: d.id_self,         // ⚡
385			id_path: d.id_path.clone(), // ⚡
386			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(), // NOWE
396			path: f.path.clone(),
397			node: NodeIs::File,
398			tier: f.tier,
399			id_self: f.id_self,         // ⚡
400			id_path: f.id_path.clone(), // ⚡
401			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	// =========================================================================
417	// HELPERY I ITERATORY
418	// =========================================================================
419
420	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
433// ============================================================================
434// HELPERY SKANERA (Przygotowanie danych)
435// ============================================================================
436
437struct 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
450/// Helper 1: Przechodzi po dysku, kategoryzuje i zbiera surowe węzły
451fn 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	// ⚡ REJESTR ID I ŚCIEŻEK (Materialized Path)
465	let mut current_id = 1; // Zaczynamy od 1, bo 0 to workspace_dir
466	let mut path_registry: HashMap<std::path::PathBuf, (usize, Vec<usize>)> = HashMap::new();
467
468	// Inicjalizujemy korzeń (workspace_dir) z ID 0 i pustą ścieżką przodków
469	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		// ⚡ NADAWANIE TOŻSAMOŚCI I BUDOWANIE ID_PATH
489		let id_self = current_id;
490		current_id += 1;
491
492		// Szukamy rodzica w rejestrze
493		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		// Budujemy ścieżkę z ID: bierzemy ścieżkę rodzica i doklejamy do niej ID rodzica
497		// Np. Rodzic to folder `src` (ID: 1, ścieżka: [0]). Nasza nowa ścieżka to [0, 1].
498		let mut id_path = parent_id_path;
499		id_path.push(parent_id);
500
501		// Jeśli to folder, wpisujemy go do rejestru dla jego przyszłych dzieci
502		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	// Sortowanie gwarantujące stabilność dla logiki analizy dzieci
557	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
573/// Helper 2: Analizuje relacje rodzic-dziecko i buduje pełne ScannedDirNode
574fn 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		// Zliczanie plików bezpośrednich
589		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		// Zliczanie subkatalogów bezpośrednich
603		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,         // ⚡ PRZEPISANIE ID
618			id_path: raw.id_path.clone(), // ⚡ PRZEPISANIE ŚCIEŻKI
619			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}