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, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub struct NativePathKey(Vec<native::NativeComponentKey>);
33
34impl NativePathKey {
35 pub fn new(path: &Path) -> Self {
37 let path = normalize_lexical_path(path);
38 Self(
39 path.components()
40 .filter(|component| *component != Component::CurDir)
41 .map(native::component_key)
42 .collect(),
43 )
44 }
45}
46
47pub fn contains_parent_traversal(path: &Path) -> bool {
49 path.components()
50 .any(|component| component == Component::ParentDir)
51}
52
53pub fn normalize_lexical_path(path: &Path) -> Cow<'_, Path> {
60 #[cfg(windows)]
61 let path = native::normalize_windows_device_path(path);
62 #[cfg(not(windows))]
63 let path = Cow::Borrowed(path);
64
65 let normalized = path
66 .components()
67 .filter(|component| *component != Component::CurDir)
68 .collect::<PathBuf>();
69 if normalized.as_os_str() == path.as_os_str() {
70 path
71 } else {
72 Cow::Owned(normalized)
73 }
74}
75
76pub fn paths_equal(left: &Path, right: &Path) -> bool {
78 #[cfg(windows)]
79 {
80 NativePathKey::new(left) == NativePathKey::new(right)
81 }
82 #[cfg(not(windows))]
83 let mut left = left
84 .components()
85 .filter(|component| *component != Component::CurDir);
86 #[cfg(not(windows))]
87 let mut right = right
88 .components()
89 .filter(|component| *component != Component::CurDir);
90 #[cfg(not(windows))]
91 loop {
92 match (left.next(), right.next()) {
93 (None, None) => return true,
94 (Some(left), Some(right)) if components_equal(left, right) => {}
95 _ => return false,
96 }
97 }
98}
99
100pub fn is_within(path: &Path, root: &Path) -> bool {
106 if contains_parent_traversal(path) || contains_parent_traversal(root) {
107 return false;
108 }
109 #[cfg(windows)]
110 {
111 let path = NativePathKey::new(path);
112 let root = NativePathKey::new(root);
113 if root.0.is_empty() {
114 return !matches!(
115 path.0.first(),
116 Some(native::NativeComponentKey::Prefix(_) | native::NativeComponentKey::RootDir)
117 );
118 }
119 path.0.starts_with(&root.0)
120 }
121 #[cfg(not(windows))]
122 let mut path = path
123 .components()
124 .filter(|component| *component != Component::CurDir);
125 #[cfg(not(windows))]
126 let mut root = root
127 .components()
128 .filter(|component| *component != Component::CurDir);
129 #[cfg(not(windows))]
130 if root.clone().next().is_none() {
131 return path.clone().next() != Some(Component::RootDir);
132 }
133 #[cfg(not(windows))]
134 loop {
135 match (root.next(), path.next()) {
136 (None, _) => return true,
137 (Some(root), Some(path)) if components_equal(path, root) => {}
138 (Some(_), _) => return false,
139 }
140 }
141}
142
143pub fn contains_component_path(path: &Path, needle: &Path) -> bool {
150 #[cfg(windows)]
151 {
152 let path = NativePathKey::new(path);
153 let needle = NativePathKey::new(needle);
154 !needle.0.is_empty()
155 && needle.0.len() <= path.0.len()
156 && path
157 .0
158 .windows(needle.0.len())
159 .any(|window| window == needle.0)
160 }
161 #[cfg(not(windows))]
162 let path_components: Vec<_> = path
163 .components()
164 .filter(|component| *component != Component::CurDir)
165 .collect();
166 #[cfg(not(windows))]
167 let needle_components: Vec<_> = needle
168 .components()
169 .filter(|component| *component != Component::CurDir)
170 .collect();
171 #[cfg(not(windows))]
172 if needle_components.is_empty() || needle_components.len() > path_components.len() {
173 return false;
174 }
175 #[cfg(not(windows))]
176 path_components
177 .windows(needle_components.len())
178 .any(|window| {
179 window
180 .iter()
181 .zip(&needle_components)
182 .all(|(left, right)| components_equal(*left, *right))
183 })
184}
185
186pub fn components_equal(left: Component<'_>, right: Component<'_>) -> bool {
188 native::components_equal(left, right)
189}
190
191pub fn strings_equal(left: &str, right: &str) -> bool {
193 #[cfg(windows)]
194 {
195 case_fold(left) == case_fold(right)
196 }
197 #[cfg(not(windows))]
198 {
199 left == right
200 }
201}
202
203pub fn case_fold(value: &str) -> String {
205 #[cfg(windows)]
206 {
207 value.to_lowercase()
208 }
209 #[cfg(not(windows))]
210 {
211 value.to_owned()
212 }
213}