1#![doc = include_str!("../README.md")]
19#![deny(missing_docs)]
20
21use std::borrow::Cow;
22use std::path::{Component, Path, PathBuf};
23
24mod native;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub enum PathDialect {
33 Posix,
35 Windows,
37}
38
39impl PathDialect {
40 pub const fn native() -> Self {
42 #[cfg(target_os = "windows")]
43 {
44 Self::Windows
45 }
46 #[cfg(not(target_os = "windows"))]
47 {
48 Self::Posix
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
59pub struct PlatformPathKey {
60 dialect: PathDialect,
61 components: Vec<PlatformComponentKey>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
65enum PlatformComponentKey {
66 Prefix(String),
67 Root,
68 Parent,
69 Normal(String),
70}
71
72impl PlatformPathKey {
73 pub fn new(value: &str, dialect: PathDialect) -> Self {
75 Self {
76 dialect,
77 components: lexical_components(value, dialect),
78 }
79 }
80}
81
82pub fn is_absolute_text(value: &str, dialect: PathDialect) -> bool {
84 match dialect {
85 PathDialect::Posix => value.starts_with('/'),
86 PathDialect::Windows => {
87 let bytes = value.as_bytes();
88 value.starts_with("\\\\")
89 || value.starts_with("//")
90 || (bytes.len() >= 3
91 && bytes[0].is_ascii_alphabetic()
92 && bytes[1] == b':'
93 && matches!(bytes[2], b'/' | b'\\'))
94 }
95 }
96}
97
98pub fn contains_parent_traversal_text(value: &str, dialect: PathDialect) -> bool {
100 let is_separator = |character: char| match dialect {
101 PathDialect::Posix => character == '/',
102 PathDialect::Windows => matches!(character, '/' | '\\'),
103 };
104 let mut component = String::new();
105 for character in value.chars().chain(std::iter::once('/')) {
106 if is_separator(character) {
107 if component == ".." {
108 return true;
109 }
110 component.clear();
111 } else {
112 component.push(character);
113 }
114 }
115 false
116}
117
118fn lexical_components(value: &str, dialect: PathDialect) -> Vec<PlatformComponentKey> {
119 match dialect {
120 PathDialect::Posix => lexical_posix_components(value),
121 PathDialect::Windows => lexical_windows_components(value),
122 }
123}
124
125fn lexical_posix_components(value: &str) -> Vec<PlatformComponentKey> {
126 let mut components = Vec::new();
127 if value.starts_with('/') {
128 components.push(PlatformComponentKey::Root);
129 }
130 for component in value.split('/') {
131 match component {
132 "" | "." => {}
133 ".." => components.push(PlatformComponentKey::Parent),
134 value => components.push(PlatformComponentKey::Normal(value.to_owned())),
135 }
136 }
137 components
138}
139
140fn lexical_windows_components(value: &str) -> Vec<PlatformComponentKey> {
141 let mut value = value.replace('\\', "/");
142 if value
143 .get(..8)
144 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("//?/unc/"))
145 || value
146 .get(..8)
147 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("//./unc/"))
148 {
149 value = format!("//{}", &value[8..]);
150 } else if value
151 .get(..4)
152 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("//?/"))
153 || value
154 .get(..4)
155 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("//./"))
156 {
157 value = value[4..].to_owned();
158 }
159
160 let mut components = Vec::new();
161 let mut parts = value.split('/');
162 if let Some(first) = parts.next()
163 && first.len() == 2
164 && first.as_bytes()[1] == b':'
165 && first.as_bytes()[0].is_ascii_alphabetic()
166 {
167 components.push(PlatformComponentKey::Prefix(first.to_ascii_lowercase()));
168 if value.as_bytes().get(2) == Some(&b'/') {
169 components.push(PlatformComponentKey::Root);
170 }
171 } else {
172 if value.starts_with("//") {
173 components.push(PlatformComponentKey::Root);
174 components.push(PlatformComponentKey::Prefix("unc".to_owned()));
175 }
176 parts = value.split('/');
177 }
178
179 for component in parts {
180 match component {
181 "" | "." => {}
182 value
183 if value.len() == 2
184 && value.as_bytes()[1] == b':'
185 && value.as_bytes()[0].is_ascii_alphabetic()
186 && components
187 .iter()
188 .any(|entry| matches!(entry, PlatformComponentKey::Prefix(_))) => {}
189 ".." => components.push(PlatformComponentKey::Parent),
190 value => components.push(PlatformComponentKey::Normal(value.to_lowercase())),
191 }
192 }
193 components
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
202pub struct NativePathKey(Vec<native::NativeComponentKey>);
203
204impl NativePathKey {
205 pub fn new(path: &Path) -> Self {
207 let path = normalize_lexical_path(path);
208 Self(
209 path.components()
210 .filter(|component| *component != Component::CurDir)
211 .map(native::component_key)
212 .collect(),
213 )
214 }
215}
216
217pub fn contains_parent_traversal(path: &Path) -> bool {
219 path.components()
220 .any(|component| component == Component::ParentDir)
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum PathResolutionError {
226 Empty,
228 ContainsNul,
230 ParentTraversal,
232}
233
234impl std::fmt::Display for PathResolutionError {
235 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 let message = match self {
237 Self::Empty => "path declaration is empty",
238 Self::ContainsNul => "path declaration contains NUL",
239 Self::ParentTraversal => "path declaration contains parent traversal",
240 };
241 formatter.write_str(message)
242 }
243}
244
245impl std::error::Error for PathResolutionError {}
246
247pub fn resolve_lexical_path(
253 base: &Path,
254 declaration: &Path,
255) -> Result<PathBuf, PathResolutionError> {
256 if declaration.as_os_str().is_empty() {
257 return Err(PathResolutionError::Empty);
258 }
259 if declaration.as_os_str().to_string_lossy().contains('\0') {
260 return Err(PathResolutionError::ContainsNul);
261 }
262 if contains_parent_traversal(declaration) {
263 return Err(PathResolutionError::ParentTraversal);
264 }
265 let resolved = if declaration.is_absolute() {
266 declaration.to_path_buf()
267 } else {
268 base.join(declaration)
269 };
270 Ok(normalize_lexical_path(&resolved).into_owned())
271}
272
273pub fn normalize_lexical_path(path: &Path) -> Cow<'_, Path> {
280 #[cfg(windows)]
281 let path = native::normalize_windows_device_path(path);
282 #[cfg(not(windows))]
283 let path = Cow::Borrowed(path);
284
285 let normalized = path
286 .components()
287 .filter(|component| *component != Component::CurDir)
288 .collect::<PathBuf>();
289 if normalized.as_os_str() == path.as_os_str() {
290 path
291 } else {
292 Cow::Owned(normalized)
293 }
294}
295
296pub fn paths_equal(left: &Path, right: &Path) -> bool {
298 #[cfg(windows)]
299 {
300 NativePathKey::new(left) == NativePathKey::new(right)
301 }
302 #[cfg(not(windows))]
303 let mut left = left
304 .components()
305 .filter(|component| *component != Component::CurDir);
306 #[cfg(not(windows))]
307 let mut right = right
308 .components()
309 .filter(|component| *component != Component::CurDir);
310 #[cfg(not(windows))]
311 loop {
312 match (left.next(), right.next()) {
313 (None, None) => return true,
314 (Some(left), Some(right)) if components_equal(left, right) => {}
315 _ => return false,
316 }
317 }
318}
319
320pub fn is_within(path: &Path, root: &Path) -> bool {
326 if contains_parent_traversal(path) || contains_parent_traversal(root) {
327 return false;
328 }
329 #[cfg(windows)]
330 {
331 let path = NativePathKey::new(path);
332 let root = NativePathKey::new(root);
333 if root.0.is_empty() {
334 return !matches!(
335 path.0.first(),
336 Some(native::NativeComponentKey::Prefix(_) | native::NativeComponentKey::RootDir)
337 );
338 }
339 path.0.starts_with(&root.0)
340 }
341 #[cfg(not(windows))]
342 let mut path = path
343 .components()
344 .filter(|component| *component != Component::CurDir);
345 #[cfg(not(windows))]
346 let mut root = root
347 .components()
348 .filter(|component| *component != Component::CurDir);
349 #[cfg(not(windows))]
350 if root.clone().next().is_none() {
351 return path.clone().next() != Some(Component::RootDir);
352 }
353 #[cfg(not(windows))]
354 loop {
355 match (root.next(), path.next()) {
356 (None, _) => return true,
357 (Some(root), Some(path)) if components_equal(path, root) => {}
358 (Some(_), _) => return false,
359 }
360 }
361}
362
363pub fn contains_component_path(path: &Path, needle: &Path) -> bool {
370 #[cfg(windows)]
371 {
372 let path = NativePathKey::new(path);
373 let needle = NativePathKey::new(needle);
374 !needle.0.is_empty()
375 && needle.0.len() <= path.0.len()
376 && path
377 .0
378 .windows(needle.0.len())
379 .any(|window| window == needle.0)
380 }
381 #[cfg(not(windows))]
382 let path_components: Vec<_> = path
383 .components()
384 .filter(|component| *component != Component::CurDir)
385 .collect();
386 #[cfg(not(windows))]
387 let needle_components: Vec<_> = needle
388 .components()
389 .filter(|component| *component != Component::CurDir)
390 .collect();
391 #[cfg(not(windows))]
392 if needle_components.is_empty() || needle_components.len() > path_components.len() {
393 return false;
394 }
395 #[cfg(not(windows))]
396 path_components
397 .windows(needle_components.len())
398 .any(|window| {
399 window
400 .iter()
401 .zip(&needle_components)
402 .all(|(left, right)| components_equal(*left, *right))
403 })
404}
405
406pub fn components_equal(left: Component<'_>, right: Component<'_>) -> bool {
408 native::components_equal(left, right)
409}
410
411pub fn strings_equal(left: &str, right: &str) -> bool {
413 #[cfg(windows)]
414 {
415 case_fold(left) == case_fold(right)
416 }
417 #[cfg(not(windows))]
418 {
419 left == right
420 }
421}
422
423pub fn case_fold(value: &str) -> String {
425 #[cfg(windows)]
426 {
427 value.to_lowercase()
428 }
429 #[cfg(not(windows))]
430 {
431 value.to_owned()
432 }
433}