dia_files/
filter.rs

1/*
2==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--
3
4Dia-Files
5
6Copyright (C) 2019-2024  Anonymous
7
8There are several releases over multiple years,
9they are listed as ranges, such as: "2019-2024".
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU Lesser General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19GNU Lesser General Public License for more details.
20
21You should have received a copy of the GNU Lesser General Public License
22along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
24::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
25*/
26
27//! # Filter
28//!
29//! This module provides some common filters.
30
31#[cfg(unix)]
32use crate::Result;
33
34#[cfg(not(feature="async-std"))]
35use std::path::Path;
36
37#[cfg(feature="async-std")]
38use {
39    core::future::Future,
40    async_std::path::Path,
41};
42
43/// # Filter
44///
45/// ## Notes
46///
47/// - File extensions do _not_ contain leading dot `.`.
48#[derive(Debug, Clone, Eq, PartialEq)]
49pub enum Filter<'a> {
50
51    /// # A filter that accepts all paths
52    AllPaths,
53
54    /// # A filter that combines all filters you provide, using _logical conjunction_
55    ///
56    /// ## Notes
57    ///
58    /// - If you provide an empty list, this filter _rejects_ all paths.
59    /// - Files are only accepted if _all_ filters accept them.
60    All(&'a [Filter<'a>]),
61
62    /// # A filter that combines all filters you provide, using _logical disjunction_
63    ///
64    /// ## Notes
65    ///
66    /// - If you provide an empty list, this filter _rejects_ all paths.
67    /// - Files are accepted if _any_ of the filters accepts them.
68    Any(&'a [Filter<'a>]),
69
70    /// # A filter that ignores directories by names you provide
71    ///
72    /// ## Notes
73    ///
74    /// Providing an empty list means _no names are ignored_. So all directories will be _accepted_.
75    IgnoredDirNames {
76
77        /// # Directory names
78        names: &'a [&'a str],
79
80        /// # Case
81        case: Case,
82
83    },
84
85    /// # A filter that ignores files by extensions you provide
86    ///
87    /// ## Notes
88    ///
89    /// If you provide an empty list, that means no extensions are ignored, so _all files having extension_ will be _accepted_.
90    IgnoredFileExts {
91
92        /// # File extensions
93        exts: &'a [&'a str],
94
95        /// # Case
96        case: Case,
97
98        /// # Flag for files which have no extension
99        files_without_extension: bool,
100
101    },
102
103    /// # A filter that ignores files by names you provide
104    ///
105    /// ## Notes
106    ///
107    /// Providing an empty list means _no names are ignored_. So all files will be _accepted_.
108    IgnoredFileNames {
109
110        /// # File names
111        names: &'a [&'a str],
112
113        /// # Case
114        case: Case,
115
116    },
117
118    /// # A filter that _rejects_ symbolic link files
119    NonSymlinkFiles,
120
121    /// # Same Device filter
122    ///
123    /// A filter which only accepts files on a same device of a file you provide.
124    #[cfg(unix)]
125    #[doc(cfg(unix))]
126    SameDevice {
127
128        /// # Device ID
129        device_id: u64,
130
131    },
132
133    /// # A filter that only accepts directories by names you provide
134    ///
135    /// ## Notes
136    ///
137    /// Providing an empty list means you have no names to accept. So _all directories_ will be _ignored_.
138    SomeDirNames {
139
140        /// # Names
141        names: &'a [&'a str],
142
143        /// # Case
144        case: Case,
145
146    },
147
148    /// # A filter that only accepts files by extensions you provide
149    ///
150    /// ## Notes
151    ///
152    /// Providing an empty list means you have _nothing_ to pick. So _all files having extension_ will be _ignored_.
153    SomeFileExts {
154
155        /// # File extensions
156        exts: &'a [&'a str],
157
158        /// # Case
159        case: Case,
160
161        /// # Flag for files which have no extension
162        files_without_extension: bool,
163
164    },
165
166    /// # A filter that only accepts files by names you provide
167    ///
168    /// ## Notes
169    ///
170    /// Providing an empty list means you have no names to accept. So _all files_ will be _ignored_.
171    SomeFileNames {
172
173        /// # Names
174        names: &'a [&'a str],
175
176        /// # Case
177        case: Case,
178
179    },
180
181    /// # A filter that only accepts symbolic link files
182    SymlinkFiles,
183
184    /// # User defined filter
185    ///
186    ///  [`accept()`](#variant.UserDefined.field.accept) should return `true` to accept given path, `false` to ignore it
187    ///
188    /// ## Notes
189    ///
190    /// - Filters of _files_ should always return `true` if input path is a _directory_.
191    /// - Filters of _directories_ should always return `true` if input path is a _file_.
192    ///
193    /// - Because of above rules, `accept(...) == false` is _not_ always the opposite meaning that one might expect. For instance:
194    ///
195    ///     + If [`SymlinkFiles`](#variant.SymlinkFiles) ignores a file, that means the file is not a symbolic link file. _However..._
196    ///     + If it accepts that file, it does _not_ mean that the file is a symbolic link file. Obviously, it might be a directory.
197    UserDefined {
198
199        /// # User defined filter
200        #[cfg(not(feature="async-std"))]
201        #[doc(cfg(not(feature="async-std")))]
202        accept: fn(&Path) -> bool,
203
204        /// # User defined filter
205        #[cfg(feature="async-std")]
206        #[doc(cfg(feature="async-std"))]
207        accept: fn(&Path) -> Box<dyn Future<Output=bool> + Unpin>,
208
209    },
210
211}
212
213/// # Case
214#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
215pub enum Case {
216
217    /// # Sensitive
218    Sensitive,
219
220    /// # Insensitive
221    Insensitive,
222
223}
224
225#[cfg(unix)]
226macro_rules! make_same_device { ($path: ident) => {{
227    use std::os::unix::fs::MetadataExt;
228    Ok(Self::SameDevice {
229        device_id: async_call!($path.as_ref().metadata()).map(|m| m.dev())?,
230    })
231}}}
232
233/// # Returns `true` to accept given path, `false` to ignore it
234///
235/// ## Notes
236///
237/// - Filters of _files_ should always return `true` if input path is a _directory_.
238/// - Filters of _directories_ should always return `true` if input path is a _file_.
239///
240/// - Because of above rules, `!accept(...)` is _not_ always the opposite meaning that one might expect. For instance:
241///
242///     + [`SymlinkFiles::accept(some_file) == false`][::SymlinkFiles] means `some_file` is not a symbolic link file. _However..._
243///     + `SymlinkFiles::accept(some_file) == true` does _not_ mean that `some_file` is a symbolic link file. Obviously, `some_file` might
244///       be a directory.
245///
246/// [::SymlinkFiles]: struct.SymlinkFiles.html
247macro_rules! accept { ($self: ident, $path: ident) => {{
248    match $self {
249        Self::AllPaths => true,
250        Self::All(filters) => accept_all!(filters, $path),
251        Self::Any(filters) => accept_any!(filters, $path),
252        Self::IgnoredDirNames { names, case } => ignored_dir_names!(names, case, $path),
253        Self::IgnoredFileExts { exts, case, files_without_extension: fwe } => ignored_file_exts!(exts, case, fwe, $path),
254        Self::IgnoredFileNames { names, case } => ignored_file_names!(names, case, $path),
255        Self::NonSymlinkFiles => non_symlink_files!($path),
256        #[cfg(unix)]
257        Self::SameDevice { device_id } => same_device!(device_id, $path),
258        Self::SomeDirNames { names, case } => some_dir_names!(names, case, $path),
259        Self::SomeFileExts { exts, case, files_without_extension } => some_file_exts!(exts, case, files_without_extension, $path),
260        Self::SomeFileNames { names, case } => some_file_names!(names, case, $path),
261        Self::SymlinkFiles => symlink_files!($path),
262        Self::UserDefined { accept } => {
263            let f = accept($path);
264            #[cfg(feature="async-std")]
265            let f = Box::pin(f);
266            async_call!(f)
267        },
268    }
269}}}
270
271macro_rules! accept_all { ($filters: ident, $path: ident) => {{
272    if $filters.is_empty() {
273        false
274    } else {
275        for f in *$filters {
276            let f = f.accept($path);
277            #[cfg(feature="async-std")]
278            let f = Box::pin(f);
279            if async_call!(f) == false {
280                return false
281            }
282        }
283        true
284    }
285}}}
286
287macro_rules! accept_any { ($filters: ident, $path: ident) => {{
288    for f in *$filters {
289        let f = f.accept($path);
290        #[cfg(feature="async-std")]
291        let f = Box::pin(f);
292        if async_call!(f) {
293            return true
294        }
295    }
296    false
297}}}
298
299macro_rules! ignored_dir_names { ($dir_names: ident, $case: ident, $path: ident) => {{
300    if $dir_names.is_empty() {
301        return true;
302    }
303
304    if async_call!($path.is_dir()) {
305        if let Some(Some(name)) = $path.file_name().map(|n| n.to_str()) {
306            return match $case {
307                Case::Sensitive => $dir_names.contains(&name) == false,
308                Case::Insensitive => {
309                    let name = name.to_lowercase();
310                    $dir_names.iter().all(|n| n.to_lowercase() != name)
311                },
312            };
313        }
314    }
315
316    true
317}}}
318
319macro_rules! ignored_file_exts { ($file_exts: ident, $case: ident, $files_without_extension: ident, $path: ident) => {{
320    if async_call!($path.is_file()) {
321        return match $path.extension().map(|e| e.to_str()) {
322            Some(Some(ext)) => $file_exts.is_empty() || match $case {
323                Case::Sensitive => $file_exts.contains(&ext) == false,
324                Case::Insensitive => {
325                    let ext = ext.to_lowercase();
326                    $file_exts.iter().all(|e| e.to_lowercase() != ext)
327                },
328            },
329            Some(None) => true,
330            None => $files_without_extension == &false,
331        };
332    }
333
334    true
335}}}
336
337macro_rules! ignored_file_names { ($file_names: ident, $case: ident, $path: ident) => {{
338    if $file_names.is_empty() {
339        return true;
340    }
341
342    if async_call!($path.is_file()) {
343        if let Some(Some(name)) = $path.file_name().map(|n| n.to_str()) {
344            return match $case {
345                Case::Sensitive => $file_names.contains(&name) == false,
346                Case::Insensitive => {
347                    let name = name.to_lowercase();
348                    $file_names.iter().all(|n| n.to_lowercase() != name)
349                },
350            };
351        }
352    }
353
354    true
355}}}
356
357macro_rules! non_symlink_files { ($path: ident) => {{
358    if async_call!($path.is_file()) {
359        if let Ok(true) = async_call!($path.symlink_metadata()).map(|m| m.file_type().is_symlink()) {
360            return false;
361        }
362    }
363    true
364}}}
365
366#[cfg(unix)]
367macro_rules! same_device { ($device_id: ident, $path: ident) => {{
368    use std::os::unix::fs::MetadataExt;
369    match async_call!($path.metadata()).map(|m| m.dev()) {
370        Ok(id) => $device_id == &id,
371        Err(_) => false,
372    }
373}}}
374
375macro_rules! some_dir_names { ($names: ident, $case: ident, $path: ident) => {{
376    if async_call!($path.is_dir()) {
377        if $names.is_empty() {
378            return false;
379        }
380
381        return match $path.file_name().map(|n| n.to_str()) {
382            Some(Some(name)) => match $case {
383                Case::Sensitive => $names.contains(&name),
384                Case::Insensitive => {
385                    let name = name.to_lowercase();
386                    $names.iter().any(|n| n.to_lowercase() == name)
387                },
388            },
389            _ => false,
390        };
391    }
392
393    true
394}}}
395
396macro_rules! some_file_exts { ($file_exts: ident, $case: ident, $files_without_extension: ident, $path: ident) => {{
397    if async_call!($path.is_file()) {
398        return match $path.extension().map(|e| e.to_str()) {
399            Some(Some(ext)) => $file_exts.is_empty() == false && match $case {
400                Case::Sensitive => $file_exts.contains(&ext),
401                Case::Insensitive => {
402                    let ext = ext.to_lowercase();
403                    $file_exts.iter().any(|e| e.to_lowercase() == ext)
404                },
405            },
406            Some(None) => false,
407            None => *$files_without_extension,
408        };
409    }
410
411    true
412}}}
413
414macro_rules! some_file_names { ($names: ident, $case: ident, $path: ident) => {{
415    if async_call!($path.is_file()) {
416        if $names.is_empty() {
417            return false;
418        }
419
420        return match $path.file_name().map(|n| n.to_str()) {
421            Some(Some(name)) => match $case {
422                Case::Sensitive => $names.contains(&name),
423                Case::Insensitive => {
424                    let name = name.to_lowercase();
425                    $names.iter().any(|n| n.to_lowercase() == name)
426                },
427            },
428            _ => false,
429        };
430    }
431
432    true
433}}}
434
435macro_rules! symlink_files { ($path: ident) => {{
436    if async_call!($path.is_file()) {
437        return match async_call!($path.symlink_metadata()).map(|m| m.file_type().is_symlink()) {
438            Ok(true) => true,
439            _ => false,
440        };
441    }
442    true
443}}}
444
445impl Filter<'_> {
446
447
448    /// # Makes [`SameDevice`](#variant.SameDevice) from a path
449    #[cfg(all(not(feature="async-std"), unix))]
450    #[doc(cfg(all(not(feature="async-std"), unix)))]
451    pub fn make_same_device<P>(path: P) -> Result<Self> where P: AsRef<Path> {
452        make_same_device!(path)
453    }
454
455    /// # Makes [`SameDevice`](#variant.SameDevice) from a path
456    #[cfg(all(feature="async-std", unix))]
457    #[doc(cfg(all(feature="async-std", unix)))]
458    pub async fn make_same_device<P>(path: P) -> Result<Self> where P: AsRef<Path> {
459        make_same_device!(path)
460    }
461
462    #[cfg(not(feature="async-std"))]
463    #[doc(cfg(not(feature="async-std")))]
464    pub (crate) fn accept(&self, path: &Path) -> bool {
465        accept!(self, path)
466    }
467
468    #[cfg(feature="async-std")]
469    #[doc(cfg(feature="async-std"))]
470    pub (crate) async fn accept(&self, path: &Path) -> bool {
471        accept!(self, path)
472    }
473
474}