Skip to main content

weavatrix_scan/selection/
mod.rs

1use crate::config::ScanOptions;
2use crate::error::{Error, Result};
3use crate::ignore::{RepositoryMatch, RepositoryMatcher};
4use crate::report::{IgnoreSourceEvidence, ScanWarning, SkipKind};
5use crate::scan_match::skip_kind_for_match;
6use crate::walk_platform::directory_info;
7use crate::walk_types::{FileSystemId, WalkEntry, WalkSkipReason};
8use std::fs;
9use std::path::{Component, Path, PathBuf};
10
11mod classification;
12mod construction;
13mod queries;
14
15/// Final selection outcome for one filesystem entry.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum SelectionDisposition {
19    /// A regular file accepted by every configured selection filter.
20    SelectedFile,
21    /// A directory accepted for traversal.
22    TraverseDirectory,
23    /// An entry excluded by a typed scanner policy.
24    Skipped(SkipKind),
25    /// An entry suppressed without a typed skip, such as a file below
26    /// `min_depth` or a non-file filesystem object.
27    Unselected,
28}
29
30/// A complete, typed selection result.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct SelectionDecision {
33    disposition: SelectionDisposition,
34    repository_match: RepositoryMatch,
35}
36
37impl SelectionDecision {
38    /// Returns the final entry disposition.
39    #[must_use]
40    pub const fn disposition(self) -> SelectionDisposition {
41        self.disposition
42    }
43
44    /// Returns the winning repository/override decision, when it was reached.
45    #[must_use]
46    pub const fn repository_match(self) -> RepositoryMatch {
47        self.repository_match
48    }
49
50    /// Returns whether this entry is a selected regular file.
51    #[must_use]
52    pub const fn is_selected(self) -> bool {
53        matches!(self.disposition, SelectionDisposition::SelectedFile)
54    }
55
56    /// Returns whether traversal should descend into this directory.
57    #[must_use]
58    pub const fn should_descend(self) -> bool {
59        matches!(self.disposition, SelectionDisposition::TraverseDirectory)
60    }
61
62    /// Returns the typed exclusion reason, if the entry was skipped.
63    #[must_use]
64    pub const fn skip_kind(self) -> Option<SkipKind> {
65        match self.disposition {
66            SelectionDisposition::Skipped(kind) => Some(kind),
67            SelectionDisposition::SelectedFile
68            | SelectionDisposition::TraverseDirectory
69            | SelectionDisposition::Unselected => None,
70        }
71    }
72
73    const fn selected(repository_match: RepositoryMatch) -> Self {
74        Self {
75            disposition: SelectionDisposition::SelectedFile,
76            repository_match,
77        }
78    }
79
80    const fn directory(repository_match: RepositoryMatch) -> Self {
81        Self {
82            disposition: SelectionDisposition::TraverseDirectory,
83            repository_match,
84        }
85    }
86
87    const fn skipped(kind: SkipKind, repository_match: RepositoryMatch) -> Self {
88        Self {
89            disposition: SelectionDisposition::Skipped(kind),
90            repository_match,
91        }
92    }
93
94    const fn unselected() -> Self {
95        Self {
96            disposition: SelectionDisposition::Unselected,
97            repository_match: RepositoryMatch::None,
98        }
99    }
100}
101
102/// Reusable matcher for the complete scanner selection policy.
103///
104/// Unlike [`RepositoryMatcher`], this applies depth, symlink, standard
105/// directory, named file-type, extension, and maximum-size policies in
106/// addition to hierarchical ignore and override rules. [`Self::matched`]
107/// performs the metadata read needed to classify a standalone path;
108/// [`Self::matched_entry`] reuses metadata already captured by a [`WalkEntry`].
109///
110/// Stateful traversal-only conditions such as symlink-cycle ancestry are
111/// reported by [`WalkEntry::skip_reason`] when `matched_entry` is used.
112#[derive(Debug, Clone)]
113pub struct SelectionMatcher {
114    repository: RepositoryMatcher,
115    options: ScanOptions,
116    root_file_system: Option<FileSystemId>,
117}
118
119fn relative_depth(path: &Path) -> usize {
120    path.components()
121        .filter(|component| matches!(component, Component::Normal(_)))
122        .count()
123}
124
125const fn skip_kind(reason: WalkSkipReason) -> SkipKind {
126    match reason {
127        WalkSkipReason::MaxDepth => SkipKind::MaxDepth,
128        WalkSkipReason::FileSystemBoundary => SkipKind::FileSystemBoundary,
129        WalkSkipReason::PathEscape => SkipKind::PathEscape,
130        WalkSkipReason::SymlinkLoop => SkipKind::SymlinkLoop,
131    }
132}