Skip to main content

keramics_vfs/pathfilter/
filter.rs

1/* Copyright 2024-2026 Joachim Metz <joachim.metz@gmail.com>
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may
5 * obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software
8 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 * License for the specific language governing permissions and limitations
11 * under the License.
12 */
13
14use std::sync::Arc;
15
16use keramics_core::ErrorTrace;
17use keramics_formats::{Path, PathCharacterMappings, PathComponent};
18
19use super::enums::ScanTreeType;
20use super::scan_tree::ScanTree;
21use super::signature::PathFilterSignature;
22
23/// Path filter.
24pub struct PathFilter {
25    /// Scan paths.
26    signatures: Vec<Arc<PathFilterSignature>>,
27
28    /// Case folding mappings.
29    case_folding_mappings: Option<PathCharacterMappings>,
30
31    /// Prefix scan tree.
32    prefix_scan_tree: ScanTree,
33
34    /// Suffix scan tree.
35    suffix_scan_tree: ScanTree,
36}
37
38impl PathFilter {
39    /// Creates a new path filter.
40    pub fn new() -> Self {
41        Self {
42            signatures: Vec::new(),
43            case_folding_mappings: None,
44            prefix_scan_tree: ScanTree::new(ScanTreeType::Prefix),
45            suffix_scan_tree: ScanTree::new(ScanTreeType::Suffix),
46        }
47    }
48
49    /// Adds a signature.
50    pub fn add_signature(&mut self, signature: PathFilterSignature) {
51        self.signatures.push(Arc::new(signature));
52    }
53
54    /// Builds the scan trees.
55    pub fn build(&mut self) -> Result<(), ErrorTrace> {
56        let mut signatures: Vec<Arc<PathFilterSignature>> = Vec::new();
57
58        for signature in self.signatures.iter() {
59            match &self.case_folding_mappings {
60                Some(mappings) => match signature.new_with_case_folding(mappings) {
61                    Ok(case_folded_signature) => signatures.push(Arc::new(case_folded_signature)),
62                    Err(mut error) => {
63                        keramics_core::error_trace_add_frame!(
64                            error,
65                            "create path filter signature with case folding"
66                        );
67                        return Err(error);
68                    }
69                },
70                None => signatures.push(signature.clone()),
71            }
72        }
73        match self.prefix_scan_tree.build(&signatures) {
74            Ok(_) => {}
75            Err(mut error) => {
76                keramics_core::error_trace_add_frame!(error, "Unable to build prefix scan tree");
77                return Err(error);
78            }
79        }
80        // TODO: add support for suffix scan tree
81
82        Ok(())
83    }
84
85    /// Determines whether the given path matches the filter.
86    pub fn is_match(
87        &self,
88        path: &Path,
89        data_fork_name: Option<&PathComponent>,
90    ) -> Result<bool, ErrorTrace> {
91        let (path, data_fork_name): (Path, Option<PathComponent>) =
92            match &self.case_folding_mappings {
93                Some(mappings) => {
94                    let path: Path = match path.new_with_case_folding(mappings) {
95                        Ok(case_folded_path) => case_folded_path,
96                        Err(mut error) => {
97                            keramics_core::error_trace_add_frame!(
98                                error,
99                                "Unable to apply case folding on path"
100                            );
101                            return Err(error);
102                        }
103                    };
104                    let data_fork_name: Option<PathComponent> = match data_fork_name {
105                        Some(name) => match name.new_with_case_folding(mappings) {
106                            Ok(case_folded_name) => Some(case_folded_name),
107                            Err(mut error) => {
108                                keramics_core::error_trace_add_frame!(
109                                    error,
110                                    "Unable to apply case folding on data fork name"
111                                );
112                                return Err(error);
113                            }
114                        },
115                        None => None,
116                    };
117                    (path, data_fork_name)
118                }
119                None => (path.clone(), data_fork_name.cloned()),
120            };
121
122        match self.prefix_scan_tree.scan_path(&path) {
123            Some(signature) => Ok(data_fork_name.as_ref() == signature.data_fork_name.as_ref()),
124            None => {
125                // TODO: add support for suffix scan tree
126                Ok(false)
127            }
128        }
129    }
130
131    /// Sets the case folding mappings.
132    pub fn set_case_folding(&mut self, case_folding_mappings: PathCharacterMappings) {
133        self.case_folding_mappings = Some(case_folding_mappings);
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    use std::sync::Arc;
142
143    use keramics_types::Ucs2CharacterMappings;
144    use keramics_types::constants::UCS2_CASE_MAPPINGS;
145
146    #[test]
147    fn test_add_signature() -> Result<(), ErrorTrace> {
148        let mut path_filter: PathFilter = PathFilter::new();
149
150        let data_fork_name: PathComponent = PathComponent::from("rsrc");
151        path_filter.add_signature(PathFilterSignature::new(
152            Path::from("/Library/Caches/com.apple.Safari"),
153            Some(data_fork_name.clone()),
154        ));
155
156        assert_eq!(path_filter.signatures.len(), 1);
157        assert_eq!(
158            path_filter.signatures[0].path,
159            Path::from("/Library/Caches/com.apple.Safari")
160        );
161        assert_eq!(
162            path_filter.signatures[0].data_fork_name,
163            Some(data_fork_name)
164        );
165        Ok(())
166    }
167
168    #[test]
169    fn test_build() -> Result<(), ErrorTrace> {
170        let mut path_filter: PathFilter = PathFilter::new();
171
172        path_filter.add_signature(PathFilterSignature::new(
173            Path::from("/Windows/System32/winevt/Logs/Application.evtx"),
174            None,
175        ));
176        path_filter.build()
177    }
178
179    #[test]
180    fn test_is_match() -> Result<(), ErrorTrace> {
181        let mut path_filter: PathFilter = PathFilter::new();
182
183        path_filter.add_signature(PathFilterSignature::new(
184            Path::from("/Windows/System32/winevt/Logs/Application.evtx"),
185            None,
186        ));
187        path_filter.add_signature(PathFilterSignature::new(
188            Path::from("/Windows/SoftwareDistribution/DataStore/DataStore.edb"),
189            None,
190        ));
191        path_filter.build()?;
192
193        let path: Path = Path::from("/Windows/System32/winevt/Logs/Application.evtx");
194        let result: bool = path_filter.is_match(&path, None)?;
195        assert_eq!(result, true);
196
197        let path: Path = Path::from("/Windows/SoftwareDistribution/DataStore/DataStore.edb");
198        let result: bool = path_filter.is_match(&path, None)?;
199        assert_eq!(result, true);
200
201        let path: Path = Path::from("/WINDOWS/SoftwareDistribution/DataStore/DataStore.edb");
202        let result: bool = path_filter.is_match(&path, None)?;
203        assert_eq!(result, false);
204
205        let path: Path = Path::from("/Windows/System32/winevt/Logs/Security.evtx");
206        let result: bool = path_filter.is_match(&path, None)?;
207        assert_eq!(result, false);
208
209        Ok(())
210    }
211
212    #[test]
213    fn test_is_match_with_data_fork_name() -> Result<(), ErrorTrace> {
214        let mut path_filter: PathFilter = PathFilter::new();
215
216        path_filter.add_signature(PathFilterSignature::new(
217            Path::from("/Library/Caches/com.apple.Safari"),
218            Some(PathComponent::from("rsrc")),
219        ));
220        path_filter.build()?;
221
222        let path: Path = Path::from("/Library/Caches/com.apple.Safari");
223
224        let data_fork_name: PathComponent = PathComponent::from("rsrc");
225        let result: bool = path_filter.is_match(&path, Some(&data_fork_name))?;
226        assert_eq!(result, true);
227
228        let data_fork_name: PathComponent = PathComponent::from("data");
229        let result: bool = path_filter.is_match(&path, Some(&data_fork_name))?;
230        assert_eq!(result, false);
231
232        let result: bool = path_filter.is_match(&path, None)?;
233        assert_eq!(result, false);
234
235        Ok(())
236    }
237
238    #[test]
239    fn test_is_match_with_case_folding() -> Result<(), ErrorTrace> {
240        let mut path_filter: PathFilter = PathFilter::new();
241
242        let mappings: PathCharacterMappings = PathCharacterMappings::Ucs2(Arc::new(
243            Ucs2CharacterMappings::from(UCS2_CASE_MAPPINGS.as_slice()),
244        ));
245        path_filter.set_case_folding(mappings);
246
247        path_filter.add_signature(PathFilterSignature::new(
248            Path::from("/Windows/System32/winevt/Logs/Application.evtx"),
249            None,
250        ));
251        path_filter.build()?;
252
253        let path: Path = Path::from("/windows/system32/winevt/logs/application.evtx");
254        let result: bool = path_filter.is_match(&path, None)?;
255        assert_eq!(result, true);
256
257        let path: Path = Path::from("/windows/system32/winevt/logs/security.evtx");
258        let result: bool = path_filter.is_match(&path, None)?;
259        assert_eq!(result, false);
260
261        Ok(())
262    }
263
264    #[test]
265    fn test_is_match_without_signatures() -> Result<(), ErrorTrace> {
266        let mut path_filter: PathFilter = PathFilter::new();
267        path_filter.build()?;
268
269        let path: Path = Path::from("/Library/Caches/com.apple.Safari");
270
271        let data_fork_name: PathComponent = PathComponent::from("rsrc");
272        let result: bool = path_filter.is_match(&path, Some(&data_fork_name))?;
273        assert_eq!(result, false);
274
275        Ok(())
276    }
277}