ntfsanitise/path.rs
1use std::path::Component;
2
3use crate::{is_clean, is_dirty, sanitise};
4
5/// Extension trait for [`std::path::Path`].
6pub trait PathExt {
7 /// Returns `true` if the path does not contain [banned characters](crate::BANNED).
8 ///
9 /// # Example
10 /// ```rust
11 /// use ntfsanitise::PathExt;
12 /// use std::path::PathBuf;
13 ///
14 /// assert!(PathBuf::from("/a/clean/path").is_clean());
15 /// ```
16 fn is_clean(&self) -> bool;
17
18 /// Returns `true` if the path contains [banned characters](crate::BANNED).
19 ///
20 /// # Example
21 /// ```rust
22 /// use ntfsanitise::PathExt;
23 /// use std::path::PathBuf;
24 ///
25 /// assert!(PathBuf::from("/a/dirty?/p<a>th").is_dirty())
26 /// ```
27 fn is_dirty(&self) -> bool;
28
29 /// Returns `true` if the path's file name does not contain [banned characters](crate::BANNED), or if the path has
30 /// no file name.
31 ///
32 /// # Example
33 /// ```rust
34 /// use ntfsanitise::PathExt;
35 /// use std::path::PathBuf;
36 ///
37 /// assert!(PathBuf::from("/my/clean_file.name").has_clean_file_name());
38 /// assert!(PathBuf::from("/no/file/name/").has_clean_file_name());
39 /// assert!(PathBuf::from("/dirty<path>/with/clean_file.name").has_clean_file_name());
40 /// ```
41 fn has_clean_file_name(&self) -> bool;
42
43 /// Returns `true` if the path's file name contains [banned characters](crate::BANNED).
44 ///
45 /// # Example
46 /// ```rust
47 /// use ntfsanitise::PathExt;
48 /// use std::path::PathBuf;
49 ///
50 /// assert!(PathBuf::from("/my/dirty<file>.name").has_dirty_file_name());
51 /// ```
52 fn has_dirty_file_name(&self) -> bool;
53}
54
55impl PathExt for std::path::Path {
56 fn is_clean(&self) -> bool { self.components().all(|c| component_is_clean(&c)) }
57
58 fn is_dirty(&self) -> bool { self.components().any(|c| !component_is_clean(&c)) }
59
60 fn has_clean_file_name(&self) -> bool {
61 self.file_name().is_none_or(|file_name| is_clean(file_name.to_string_lossy()))
62 }
63
64 fn has_dirty_file_name(&self) -> bool {
65 self.file_name().is_some_and(|file_name| is_dirty(file_name.to_string_lossy()))
66 }
67}
68
69/// Extension trait for [`std::path::PathBuf`].
70pub trait PathBufExt {
71 /// Replaces [banned characters](crate::BANNED) in the file name with the given replacement.
72 ///
73 /// Returns `None` if the path does not have a file name, `Some(true)` if the file name was sanitised, or
74 /// `Some(false)` if the file name could not be sanitised due to containing invalid UTF-8.
75 ///
76 /// # Example
77 /// ```rust
78 /// use ntfsanitise::{PathExt, PathBufExt};
79 /// use std::ffi::OsString;
80 /// use std::path::PathBuf;
81 ///
82 /// let mut path = PathBuf::from("/example/<name>.txt");
83 /// assert!(path.is_dirty());
84 ///
85 /// path.sanitise_filename("_");
86 /// let expected = OsString::from("_name_.txt");
87 /// let expected = expected.as_os_str();
88 ///
89 /// assert_eq!(path.file_name(), Some(expected));
90 /// assert!(path.is_clean());
91 /// ```
92 #[doc(alias = "sanitize_filename")]
93 fn sanitise_filename(&mut self, to: &str) -> Option<bool>;
94}
95
96impl PathBufExt for std::path::PathBuf {
97 fn sanitise_filename(&mut self, to: &str) -> Option<bool> {
98 if let Some(file_name) = self.file_name() {
99 if let Some(file_name) = file_name.to_str() {
100 self.set_file_name(sanitise(file_name, to));
101 Some(true)
102 } else {
103 Some(false)
104 }
105 } else {
106 None
107 }
108 }
109}
110
111fn component_is_clean(component: &Component) -> bool {
112 match component {
113 Component::Normal(component) => is_clean(component.to_string_lossy()),
114 _ => true,
115 }
116}