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 super::{PathNode, PattEnvIndex};
13use crate::lib::{logic::PatternsQueries, schema::ReadyJobExplorer};
14
15const CHUNK_SIZE: usize = 4096;
16const MAX_SCAN: usize = 16 * 1024; // 16 KB wystarcza w 99.9%
17
18#[inline(always)]
19pub fn scan_file_is_binary(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 ScanMatchLabel {
64	type Node;
65	fn label() -> &'static str;
66}
67
68#[derive(Debug, Clone)]
69pub struct ScanMatchedFile;
70#[derive(Debug, Clone)]
71pub struct ScanMismatchedFile;
72#[derive(Debug, Clone)]
73pub struct ScanMatchedDir;
74#[derive(Debug, Clone)]
75pub struct ScanMismatchedDir;
76
77impl ScanMatchLabel for ScanMatchedFile {
78	type Node = ScanNodeFileScanned; // Związane z węzłem pliku!
79	fn label() -> &'static str { "✔F" } // ✔️📄
80}
81
82impl ScanMatchLabel for ScanMismatchedFile {
83	type Node = ScanNodeFileScanned; // Związane z węzłem pliku!
84	fn label() -> &'static str { "✖F" } // ✖️📄
85}
86
87impl ScanMatchLabel for ScanMatchedDir {
88	type Node = ScanNodeDirScanned; // Związane z węzłem katalogu!
89	fn label() -> &'static str { "✔D" } // ✔️📂
90}
91
92impl ScanMatchLabel for ScanMismatchedDir {
93	type Node = ScanNodeDirScanned; // Związane z węzłem katalogu!
94	fn label() -> &'static str { "✖D" } // ✖️📂
95}
96
97#[derive(Debug, Clone)]
98pub struct ScanPartition<L: ScanMatchLabel> {
99	pub nodes: Vec<L::Node>, // Kompilator sam podstawi tu ScanNodeFileScanned lub ScanNodeDirScanned
100	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: ScanMatchLabel> ScanPartition<L> {
109	// Pomocniczy trait-like look dla L::Node, aby dobrać się do pól tier i name
110	pub fn calculate_metadata(nodes: &[L::Node]) -> (usize, usize, usize)
111	where L::Node: ScanAsMetadataNode // Musimy zdefiniować ten pomocniczy trait poniżej
112	{
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); // ⚡ NOWE
116		(t_max, n_max, p_max)
117	}
118}
119// Dodaj ten pomocniczy trait w fs_scanner.rs, aby ScanPartition mogło operować na typach generycznych
120pub trait ScanAsMetadataNode {
121	fn get_tier(&self) -> usize;
122	fn get_name(&self) -> &str;
123	fn get_path_len(&self) -> usize;
124}
125
126impl ScanAsMetadataNode for ScanNodeFileScanned {
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 ScanAsMetadataNode for ScanNodeDirScanned {
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// ============================================================================
138// STRUKTURY DANYCH I STATYSTYK
139// ============================================================================
140
141#[derive(Debug, Clone)]
142pub struct ScanStatsTreeFsScanned {
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 ScanStatsPartitioning {
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 ScanNodeIs {
164	File,
165	Dir,
166}
167
168#[derive(Debug, Clone)]
169pub struct ScanNodeScanned {
170	pub name: String,
171	pub path: PathNode,
172	pub node: ScanNodeIs,
173	pub tier: usize,
174	pub id_self: usize,      // ⚡ Własne ID
175	pub id_path: Vec<usize>, // ⚡ Ścieżka przodków [0, 1, 5...]
176	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 ScanNodeDirScanned {
186	pub name: String,
187	pub path: PathNode,
188	pub tier: usize,
189	pub id_self: usize,      // ⚡
190	pub id_path: Vec<usize>, // ⚡
191	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 ScanNodeFileScanned {
199	pub name: String,
200	pub path: PathNode,
201	pub tier: usize,
202	pub id_self: usize,      // ⚡
203	pub id_path: Vec<usize>, // ⚡
204	pub is_binary: bool,
205	pub is_empty: bool,
206}
207
208// ============================================================================
209// ENV INDEX (Baza dla filtrowania)
210// ============================================================================
211
212struct 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// ============================================================================
240// GŁÓWNY OBIEKT WYNIKOWY
241// ============================================================================
242
243/// Skaner i filter systemu plików (Zunifikowane)
244#[derive(Debug, Clone)]
245pub struct ScanPartitionScanned {
246	// Rozbite na konkretne typy partycje (pozbywamy się sztucznego markera ScanPartition)
247	pub m_f: ScanPartition<ScanMatchedFile>,
248	pub x_f: ScanPartition<ScanMismatchedFile>,
249	pub m_d: ScanPartition<ScanMatchedDir>,
250	pub x_d: ScanPartition<ScanMismatchedDir>,
251
252	pub stat_scan: ScanStatsTreeFsScanned,
253	pub stat_part: ScanStatsPartitioning,
254	pub stat_patt: Vec<String>,
255	pub stat_work: String,
256}
257
258impl ScanPartitionScanned {
259	pub fn scan(explorer: &ReadyJobExplorer) -> Self {
260		let q = PatternsQueries::new(explorer.patterns(), explorer.ignore_case());
261		// ░░░░░░░░░░░░░░░ 🅰️ ZBIERANIE DANYCH ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
262		let raw_data = gather_raw_nodes(explorer);
263		let files = raw_data.files;
264		let dirs = build_directory_nodes(&raw_data.dirs_raw, &files, &raw_data.symlink_counts);
265		let stat_scan = raw_data.stat_scan;
266
267		// Zbudowanie EnvIndex na referencjach (pożyczamy ścieżki)
268		let env_index = EnvIndex {
269			dirs: dirs.iter().map(|d| d.path.str.as_str()).collect(),
270			files: files.iter().map(|f| f.path.str.as_str()).collect(),
271		};
272
273		// ░░░░░░░░░░░░░░░ 🅱️ PIERWSZY PRZEBIEG ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
274		// zbieramy TYLKO wyniki true/false do masek
275		let files_matches: Vec<bool> = files.iter().map(|f| q.is_match(&f.path.str, &env_index)).collect();
276		let dirs_matches: Vec<bool> = dirs.iter().map(|d| q.is_match(&d.path.str, &env_index)).collect();
277
278		// Uwalniamy referencje
279		drop(env_index);
280
281		let mut vec_m_f = Vec::new();
282		let mut vec_x_f = Vec::new();
283		let mut vec_m_d = Vec::new();
284		let mut vec_x_d = Vec::new();
285
286		// ░░░░░░░░░░░░░░░ 🆎 DRUGI PRZEBIEG ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
287		// złączenie plików z zapisanymi wynikami
288		for (f, is_matched) in files.into_iter().zip(files_matches) {
289			if is_matched {
290				vec_m_f.push(f);
291			} else {
292				vec_x_f.push(f);
293			}
294		}
295
296		for (d, is_matched) in dirs.into_iter().zip(dirs_matches) {
297			if is_matched {
298				vec_m_d.push(d);
299			} else {
300				vec_x_d.push(d);
301			}
302		}
303
304		// ░░░░░░░░░░░░░░░ 🅾️ MONTOWANIE WYNIKU ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
305		let stat_part = ScanStatsPartitioning {
306			count_m: vec_m_f.len() + vec_m_d.len(),
307			count_x: vec_x_f.len() + vec_x_d.len(),
308			count_m_f: vec_m_f.len(),
309			count_x_f: vec_x_f.len(),
310			count_m_d: vec_m_d.len(),
311			count_x_d: vec_x_d.len(),
312		};
313
314		let (mf_t, mf_n, mf_p) = ScanPartition::<ScanMatchedFile>::calculate_metadata(&vec_m_f);
315		let (xf_t, xf_n, xf_p) = ScanPartition::<ScanMismatchedFile>::calculate_metadata(&vec_x_f);
316		let (md_t, md_n, md_p) = ScanPartition::<ScanMatchedDir>::calculate_metadata(&vec_m_d);
317		let (xd_t, xd_n, xd_p) = ScanPartition::<ScanMismatchedDir>::calculate_metadata(&vec_x_d);
318
319		Self {
320			m_f: ScanPartition {
321				nodes: vec_m_f,
322				label: ScanMatchedFile::label(),
323				tier_max: mf_t,
324				name_len_max: mf_n,
325				path_len_max: mf_p,
326				count: stat_part.count_m_f,
327				_marker: PhantomData,
328			},
329			x_f: ScanPartition {
330				nodes: vec_x_f,
331				label: ScanMismatchedFile::label(),
332				tier_max: xf_t,
333				name_len_max: xf_n,
334				path_len_max: xf_p,
335				count: stat_part.count_x_f,
336				_marker: PhantomData,
337			},
338			m_d: ScanPartition {
339				nodes: vec_m_d,
340				label: ScanMatchedDir::label(),
341				tier_max: md_t,
342				name_len_max: md_n,
343				path_len_max: md_p,
344				count: stat_part.count_m_d,
345				_marker: PhantomData,
346			},
347			x_d: ScanPartition {
348				nodes: vec_x_d,
349				label: ScanMismatchedDir::label(),
350				tier_max: xd_t,
351				name_len_max: xd_n,
352				path_len_max: xd_p,
353				count: stat_part.count_x_d,
354				_marker: PhantomData,
355			},
356			stat_scan,
357			stat_part,
358			stat_patt: q.patterns.0.clone(),
359			stat_work: explorer.workspace_dir().to_string_lossy().into_owned(),
360		}
361	}
362
363	// =========================================================================
364	// HELPER
365	// =========================================================================
366
367	/// Zwraca połączone i posortowane struktury ScanNodeScanned (pliki + katalogi).
368	pub fn to_scanned_nodes(&self) -> Vec<ScanNodeScanned> {
369		let total_len = self.stat_part.count_m + self.stat_part.count_x;
370		let mut unified = Vec::with_capacity(total_len);
371
372		let map_dir = |d: &ScanNodeDirScanned| ScanNodeScanned {
373			name: d.name.clone(), // NOWE
374			path: d.path.clone(),
375			node: ScanNodeIs::Dir,
376			tier: d.tier,
377			id_self: d.id_self,         // ⚡
378			id_path: d.id_path.clone(), // ⚡
379			dir_has_subdirs: Some(d.has_subdirs),
380			dir_has_files_binary: Some(d.has_files_binary),
381			dir_has_files_text: Some(d.has_files_text),
382			dir_has_symlinks: Some(d.has_symlinks),
383			file_is_binary: None,
384			file_is_empty: None,
385		};
386
387		let map_file = |f: &ScanNodeFileScanned| ScanNodeScanned {
388			name: f.name.clone(), // NOWE
389			path: f.path.clone(),
390			node: ScanNodeIs::File,
391			tier: f.tier,
392			id_self: f.id_self,         // ⚡
393			id_path: f.id_path.clone(), // ⚡
394			dir_has_subdirs: None,
395			dir_has_files_binary: None,
396			dir_has_files_text: None,
397			dir_has_symlinks: None,
398			file_is_binary: Some(f.is_binary),
399			file_is_empty: Some(f.is_empty),
400		};
401
402		unified.extend(self.m_d.nodes.iter().chain(self.x_d.nodes.iter()).map(map_dir));
403		unified.extend(self.m_f.nodes.iter().chain(self.x_f.nodes.iter()).map(map_file));
404
405		unified.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
406		unified
407	}
408
409	// =========================================================================
410	// HELPERY I ITERATORY
411	// =========================================================================
412
413	pub fn iter_nodes(&self) -> impl Iterator<Item = ScanNodeScanned> + '_ { self.to_scanned_nodes().into_iter() }
414
415	pub fn iter_file_paths(&self) -> impl Iterator<Item = &str> + '_ {
416		self.m_f.nodes.iter().chain(self.x_f.nodes.iter()).map(|n| n.path.str.as_str())
417	}
418
419	pub fn iter_dir_paths(&self) -> impl Iterator<Item = &str> + '_ {
420		self.m_d.nodes.iter().chain(self.x_d.nodes.iter()).map(|n| n.path.str.as_str())
421	}
422
423	pub fn iter_paths(&self) -> impl Iterator<Item = &str> + '_ { self.iter_file_paths().chain(self.iter_dir_paths()) }
424}
425
426// ============================================================================
427// HELPERY SKANERA (Przygotowanie danych)
428// ============================================================================
429
430struct RawDirNode {
431	path: PathNode,
432	tier: usize,
433	id_self: usize,
434	id_path: Vec<usize>,
435}
436struct RawGatherData {
437	files: Vec<ScanNodeFileScanned>,
438	dirs_raw: Vec<RawDirNode>,
439	symlink_counts: HashMap<String, usize>,
440	stat_scan: ScanStatsTreeFsScanned,
441}
442
443/// Helper 1: Przechodzi po dysku, kategoryzuje i zbiera surowe węzły
444fn gather_raw_nodes(explorer: &ReadyJobExplorer) -> RawGatherData {
445	let mut files = Vec::new();
446	let mut dirs_raw = Vec::new();
447	let mut symlink_counts = HashMap::new();
448
449	let mut count_dirs = 0;
450	let mut count_dirs_empty = 0;
451	let mut count_files = 0;
452	let mut count_files_empty = 0;
453	let mut count_files_text = 0;
454	let mut count_files_binary = 0;
455	let mut count_symlinks_skipped = 0;
456
457	// REJESTR ID I ŚCIEŻEK (Materialized Path)
458	let mut current_id = 1; // Zaczynamy od 1, bo 0 to workspace_dir
459	let mut path_registry: HashMap<std::path::PathBuf, (usize, Vec<usize>)> = HashMap::new();
460
461	// Inicjalizujemy korzeń (workspace_dir) z ID 0 i pustą ścieżką przodków
462	path_registry.insert(explorer.workspace_dir().clone(), (0, vec![]));
463
464	for e in WalkDir::new(explorer.workspace_dir()).into_iter().filter_map(|e| e.ok()) {
465		if e.depth() == 0 {
466			continue;
467		}
468
469		let tier = e.depth();
470
471		if e.path_is_symlink() {
472			count_symlinks_skipped += 1;
473			if let Some(parent) = e.path().strip_prefix(explorer.workspace_dir()).ok().and_then(|p| p.parent()) {
474				let parent_str = parent.to_string_lossy().replace('\\', "/");
475				let formatted = if parent_str.is_empty() { "./".to_string() } else { format!("./{}/", parent_str) };
476				*symlink_counts.entry(formatted).or_insert(0usize) += 1;
477			}
478			continue;
479		}
480
481		// ⚡ NADAWANIE TOŻSAMOŚCI I BUDOWANIE ID_PATH
482		let id_self = current_id;
483		current_id += 1;
484
485		// Szukamy rodzica w rejestrze
486		let parent_path = e.path().parent().expect("Błąd systemu plików: Węzeł nie ma rodzica");
487		let (parent_id, parent_id_path) = path_registry.get(parent_path).unwrap_or(&(0, vec![])).clone();
488
489		// Budujemy ścieżkę z ID: bierzemy ścieżkę rodzica i doklejamy do niej ID rodzica
490		// Np. Rodzic to folder `src` (ID: 1, ścieżka: [0]). Nasza nowa ścieżka to [0, 1].
491		let mut id_path = parent_id_path;
492		id_path.push(parent_id);
493
494		// Jeśli to folder, wpisujemy go do rejestru dla jego przyszłych dzieci
495		if e.file_type().is_dir() {
496			path_registry.insert(e.path().to_path_buf(), (id_self, id_path.clone()));
497		}
498
499		let Ok(rel_path) = e.path().strip_prefix(explorer.workspace_dir()) else {
500			continue;
501		};
502		let mut path = rel_path.to_string_lossy().replace('\\', "/");
503
504		if e.file_type().is_dir() {
505			path.push('/');
506			count_dirs += 1;
507
508			let is_empty = e.metadata().map(|m| m.is_dir()).unwrap_or(false)
509				&& e.path().read_dir().map(|mut r| r.next().is_none()).unwrap_or(false);
510
511			if is_empty {
512				count_dirs_empty += 1;
513			}
514			dirs_raw.push(RawDirNode {
515				path: PathNode::new(format!("./{}", path).into()),
516				tier,
517				id_self,
518				id_path: id_path.clone(),
519			});
520		} else {
521			count_files += 1;
522			let abs_path = e.path();
523			let is_empty = abs_path.metadata().map(|m| m.len() == 0).unwrap_or(false);
524			let mut is_binary = false;
525
526			if is_empty {
527				count_files_empty += 1;
528			} else {
529				is_binary = scan_file_is_binary(abs_path.to_str().unwrap_or("")).unwrap_or(false);
530				if is_binary {
531					count_files_binary += 1;
532				} else {
533					count_files_text += 1;
534				}
535			}
536
537			files.push(ScanNodeFileScanned {
538				name: abs_path.file_name().unwrap_or_default().to_string_lossy().to_string(),
539				path: PathNode::new(format!("./{}", path).into()),
540				id_self,
541				id_path,
542				tier,
543				is_binary,
544				is_empty,
545			});
546		}
547	}
548
549	// Sortowanie gwarantujące stabilność dla logiki analizy dzieci
550	files.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
551	dirs_raw.sort_unstable_by(|a, b| a.path.str.cmp(&b.path.str));
552
553	let stat_scan = ScanStatsTreeFsScanned {
554		count_dirs,
555		count_dirs_empty,
556		count_files,
557		count_files_empty,
558		count_files_text,
559		count_files_binary,
560		count_symlinks_skipped,
561	};
562
563	RawGatherData { files, dirs_raw, symlink_counts, stat_scan }
564}
565
566/// Helper 2: Analizuje relacje rodzic-dziecko i buduje pełne ScanNodeDirScanned
567fn build_directory_nodes(
568	dirs_raw: &[RawDirNode],
569	files: &[ScanNodeFileScanned],
570	symlink_counts: &HashMap<String, usize>,
571) -> Vec<ScanNodeDirScanned> {
572	let mut dirs = Vec::with_capacity(dirs_raw.len());
573
574	for i in 0..dirs_raw.len() {
575		let raw = &dirs_raw[i];
576		let mut count_subdirs = 0;
577		let mut count_files_binary = 0;
578		let mut count_files_text = 0;
579		let count_symlinks = *symlink_counts.get(&raw.path.str).unwrap_or(&0);
580
581		// Zliczanie plików bezpośrednich
582		for f in files {
583			if f.path.str.starts_with(&raw.path.str) {
584				let remainder = &f.path.str[raw.path.str.len()..];
585				if !remainder.contains('/') {
586					if f.is_binary {
587						count_files_binary += 1;
588					} else if !f.is_empty {
589						count_files_text += 1;
590					}
591				}
592			}
593		}
594
595		// Zliczanie subkatalogów bezpośrednich
596		for j in (i + 1)..dirs_raw.len() {
597			if dirs_raw[j].path.str.starts_with(&raw.path.str) {
598				let remainder = &dirs_raw[j].path.str[raw.path.str.len()..];
599				if !remainder.is_empty() && !remainder.trim_end_matches('/').contains('/') {
600					count_subdirs += 1;
601				}
602			} else {
603				break;
604			}
605		}
606
607		dirs.push(ScanNodeDirScanned {
608			name: raw.path.str.trim_end_matches('/').split('/').next_back().unwrap_or("").to_string(),
609			path: raw.path.clone(),
610			id_self: raw.id_self,         // ⚡ PRZEPISANIE ID
611			id_path: raw.id_path.clone(), // ⚡ PRZEPISANIE ŚCIEŻKI
612			tier: raw.tier,
613			has_subdirs: count_subdirs,
614			has_files_binary: count_files_binary,
615			has_files_text: count_files_text,
616			has_symlinks: count_symlinks,
617		});
618	}
619
620	dirs
621}