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}