Skip to main content

weavatrix_scan/ignore/repository/
matching.rs

1use super::{
2    Cow, Error, IgnoreRules, IgnoreSourceEvidence, Path, PathBuf, RepositoryMatch,
3    RepositoryMatcher, Result, RuleAction, ScanWarning, io, is_hidden_with_hint,
4    match_prepared_rules, match_rules, normalized_relative_path,
5};
6
7impl RepositoryMatcher {
8    /// Returns the winning typed decision from overrides, ignore rules, and
9    /// the optional hidden-file filter.
10    ///
11    /// # Errors
12    ///
13    /// Returns an error when the path escapes the matcher root or a required
14    /// rule file cannot be read under `ErrorPolicy::Abort`.
15    pub fn matched(
16        &mut self,
17        path: impl AsRef<Path>,
18        is_directory: bool,
19    ) -> Result<RepositoryMatch> {
20        let absolute = self.absolute_path(path.as_ref())?;
21        self.ensure_scan_scope(&absolute)?;
22        let parent = absolute.parent().unwrap_or(&self.match_root).to_path_buf();
23        self.prepare_directory_inner(&parent)?;
24        let scan_relative = absolute.strip_prefix(&self.scan_root).map_err(|_| {
25            Error::io(
26                &absolute,
27                io::Error::new(io::ErrorKind::InvalidInput, "path escapes matcher root"),
28            )
29        })?;
30        Ok(self.matched_prepared(
31            &normalized_relative_path(scan_relative),
32            &parent,
33            &absolute,
34            is_directory,
35            None,
36            false,
37        ))
38    }
39
40    /// Returns whether a path is excluded by the effective selection policy.
41    ///
42    /// # Errors
43    ///
44    /// Returns an error under the same conditions as [`Self::matched`].
45    pub fn is_ignored(&mut self, path: impl AsRef<Path>, is_directory: bool) -> Result<bool> {
46        self.matched(path, is_directory)
47            .map(RepositoryMatch::is_ignored)
48    }
49
50    /// Normalizes a path and returns its lossless root-relative representation.
51    ///
52    /// Relative inputs are interpreted from this matcher's root. Absolute
53    /// inputs outside the root and paths containing parent traversal are
54    /// rejected.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error when the path escapes the configured scan root.
59    pub fn normalize(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
60        let absolute = self.absolute_path(path.as_ref())?;
61        self.ensure_scan_scope(&absolute)?;
62        absolute
63            .strip_prefix(&self.scan_root)
64            .map(Path::to_path_buf)
65            .map_err(|_| {
66                Error::io(
67                    &absolute,
68                    io::Error::new(io::ErrorKind::InvalidInput, "path escapes matcher root"),
69                )
70            })
71    }
72
73    /// Preloads a directory's rules for subsequent child checks.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error under the same conditions as [`Self::is_ignored`].
78    pub fn prepare_directory(&mut self, directory: impl AsRef<Path>) -> Result<()> {
79        let absolute = self.absolute_path(directory.as_ref())?;
80        self.ensure_scan_scope(&absolute)?;
81        self.prepare_directory_inner(&absolute)
82    }
83
84    /// Reloads ignore inputs while preserving the configured matcher policy.
85    ///
86    /// Returns true when effective selection inputs changed.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error under the same conditions as [`Self::with_options`].
91    pub fn refresh(&mut self) -> Result<bool> {
92        let refreshed = Self::with_options(&self.scan_root, &self.options)?;
93        let changed = self.sources != refreshed.sources
94            || self.portable != refreshed.portable
95            || self.warnings != refreshed.warnings;
96        *self = refreshed;
97        Ok(changed)
98    }
99
100    #[must_use]
101    pub fn root(&self) -> &Path {
102        &self.scan_root
103    }
104
105    #[must_use]
106    pub fn sources(&self) -> &[IgnoreSourceEvidence] {
107        &self.sources
108    }
109
110    #[must_use]
111    pub fn warnings(&self) -> &[ScanWarning] {
112        &self.warnings
113    }
114
115    #[must_use]
116    pub const fn portable(&self) -> bool {
117        self.portable
118    }
119
120    pub(crate) fn matched_prepared(
121        &self,
122        scan_relative: &str,
123        parent: &Path,
124        absolute: &Path,
125        is_directory: bool,
126        hidden: Option<bool>,
127        ancestors_prepared: bool,
128    ) -> RepositoryMatch {
129        let rules = self.directories.get(parent).unwrap_or(&self.base_rules);
130        self.matched_with_rules(
131            scan_relative,
132            absolute,
133            is_directory,
134            hidden,
135            rules,
136            ancestors_prepared,
137        )
138    }
139
140    pub(crate) fn prepared_rules(&self, parent: &Path) -> &IgnoreRules {
141        self.directories.get(parent).unwrap_or(&self.base_rules)
142    }
143
144    pub(crate) fn matched_with_rules(
145        &self,
146        scan_relative: &str,
147        absolute: &Path,
148        is_directory: bool,
149        hidden: Option<bool>,
150        rules: &IgnoreRules,
151        ancestors_prepared: bool,
152    ) -> RepositoryMatch {
153        let override_match = self.overrides.matched(scan_relative, is_directory);
154        if override_match != RepositoryMatch::None {
155            return override_match;
156        }
157        let candidate = if self.scan_base.is_empty() {
158            Cow::Borrowed(scan_relative)
159        } else {
160            Cow::Owned(format!("{}/{scan_relative}", self.scan_base))
161        };
162        let matched = if ancestors_prepared {
163            match_prepared_rules(&candidate, is_directory, rules)
164        } else {
165            match_rules(&candidate, is_directory, rules)
166        };
167        match matched {
168            Some(RuleAction::Ignore) => RepositoryMatch::Ignore,
169            Some(RuleAction::Include) => RepositoryMatch::Include,
170            None if self.skip_hidden && is_hidden_with_hint(absolute, hidden) => {
171                RepositoryMatch::Hidden
172            }
173            None => RepositoryMatch::None,
174        }
175    }
176}