1use lazy_regex::*;
32use std::{path::{Path, PathBuf}, str::FromStr};
33
34pub(crate) mod common;
35pub(crate) mod flexible;
36
37#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug)]
45pub enum FlexPathVariant {
46 Common,
48 Windows,
50}
51
52impl FlexPathVariant {
53 pub(crate) const NATIVE: Self = {
54 #[cfg(target_os = "windows")] {
55 Self::Windows
56 }
57 #[cfg(not(target_os = "windows"))] {
58 Self::Common
59 }
60 };
61
62 pub const fn native() -> Self {
64 Self::NATIVE
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
71pub struct FlexPath(String, FlexPathVariant);
72
73impl FlexPath {
74 pub fn new(path: &str, variant: FlexPathVariant) -> Self {
77 Self(flexible::resolve_one(path, variant), variant)
78 }
79
80 pub fn new_common(path: &str) -> Self {
83 Self(flexible::resolve_one(path, FlexPathVariant::Common), FlexPathVariant::Common)
84 }
85
86 pub fn new_native(path: &str) -> Self {
89 Self(flexible::resolve_one(path, FlexPathVariant::NATIVE), FlexPathVariant::NATIVE)
90 }
91
92 pub fn from_n<'a, T: IntoIterator<Item = &'a str>>(paths: T, variant: FlexPathVariant) -> Self {
94 Self(flexible::resolve_n(paths, variant), variant)
95 }
96
97 pub fn from_n_common<'a, T: IntoIterator<Item = &'a str>>(paths: T) -> Self {
99 Self::from_n(paths, FlexPathVariant::Common)
100 }
101
102 pub fn from_n_native<'a, T: IntoIterator<Item = &'a str>>(paths: T) -> Self {
105 Self::from_n(paths, FlexPathVariant::NATIVE)
106 }
107
108 pub fn variant(&self) -> FlexPathVariant {
110 self.1
111 }
112
113 pub fn is_absolute(&self) -> bool {
115 flexible::is_absolute(&self.0, self.1)
116 }
117
118 pub fn resolve(&self, path2: &str) -> FlexPath {
127 FlexPath(flexible::resolve(&self.0, path2, self.1), self.1)
128 }
129
130 pub fn resolve_n<'a, T: IntoIterator<Item = &'a str>>(&self, paths: T) -> FlexPath {
134 FlexPath(flexible::resolve(&self.0, &flexible::resolve_n(paths, self.1), self.1), self.1)
135 }
136
137 pub fn relative(&self, to_path: &str) -> String {
163 flexible::relative(&self.0, to_path, self.1)
164 }
165
166 pub fn change_extension(&self, extension: &str) -> FlexPath {
183 Self(change_extension(&self.0, extension), self.1)
184 }
185
186 pub fn change_last_extension(&self, extension: &str) -> FlexPath {
195 Self(change_last_extension(&self.0, extension), self.1)
196 }
197
198 pub fn has_extension(&self, extension: &str) -> bool {
202 has_extension(&self.0, extension)
203 }
204
205 pub fn has_extensions<'a, T: IntoIterator<Item = &'a str>>(&self, extensions: T) -> bool {
209 has_extensions(&self.0, extensions)
210 }
211
212 pub fn base_name(&self) -> String {
221 base_name(&self.0)
222 }
223
224 pub fn base_name_without_ext<'a, T>(&self, extensions: T) -> String
235 where T: IntoIterator<Item = &'a str>
236 {
237 base_name_without_ext(&self.0, extensions)
238 }
239
240 pub fn to_path_buf(&self) -> PathBuf {
241 PathBuf::from_str(&self.to_string()).unwrap_or(PathBuf::new())
242 }
243}
244
245impl ToString for FlexPath {
246 fn to_string(&self) -> String {
250 if self.variant() == FlexPathVariant::Windows {
251 self.0.replace('/', "\\")
252 } else {
253 self.0.clone()
254 }
255 }
256}
257
258static STARTS_WITH_PATH_SEPARATOR: Lazy<Regex> = lazy_regex!(r"^[/\\]");
259
260fn change_extension(path: &str, extension: &str) -> String {
261 let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension;
262 if regex_find!(r"(\.[^\.]+)+$", path).is_none() {
263 return path.to_owned() + &extension;
264 }
265 regex_replace!(r"(\.[^\.]+)+$", path, |_, _| &extension).into_owned()
266}
267
268fn change_last_extension(path: &str, extension: &str) -> String {
269 let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension;
270 assert!(
271 extension[1..].find('.').is_none(),
272 "The argument to realhydroper_path::change_last_extension() must only contain one extension; got {}",
273 extension
274 );
275 if regex_find!(r"(\..+)$", path).is_none() {
276 return path.to_owned() + &extension;
277 }
278 regex_replace!(r"(\..+)$", path, |_, _| &extension).into_owned()
279}
280
281fn extension_arg(extension: &str) -> String {
283 (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension
284}
285
286fn has_extension(path: &str, extension: &str) -> bool {
287 let extension = extension.to_lowercase();
288 let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + &extension;
289 path.to_lowercase().ends_with(&extension_arg(&extension))
290}
291
292fn has_extensions<'a, T: IntoIterator<Item = &'a str>>(path: &str, extensions: T) -> bool {
293 extensions.into_iter().any(|ext| has_extension(path, ext))
294}
295
296fn base_name(path: &str) -> String {
297 path.split('/').last().map_or("", |s| s).to_owned()
298}
299
300fn base_name_without_ext<'a, T>(path: &str, extensions: T) -> String
301 where T: IntoIterator<Item = &'a str>
302{
303 let extensions = extensions.into_iter().map(extension_arg).collect::<Vec<String>>();
304 path.split('/').last().map_or("".to_owned(), |base| {
305 regex_replace!(r"(\.[^\.]+)+$", base, |_, prev_ext: &str| {
306 (if extensions.iter().any(|ext| ext == prev_ext) { "" } else { prev_ext }).to_owned()
307 }).into_owned()
308 })
309}
310
311pub fn normalize_path(p: impl AsRef<Path>) -> PathBuf {
324 let cwd = std::env::current_dir().unwrap_or(PathBuf::from_str("/").unwrap());
325 let p = FlexPath::from_n_native([cwd.to_str().unwrap(), &p.as_ref().to_string_lossy().to_owned()]).to_string();
326 let p = regex_replace!(r"[^\\/][\\/]+$", &p, |a: &str| {
327 a.chars().collect::<Vec<_>>()[0].to_string()
328 }).into_owned();
329
330 if regex_is_match!(r"\\\\\?\\[Uu][Nn][Cc]", &p) {
333 return PathBuf::from_str(&(r"\\?\UNC".to_owned() + &p[7..].to_lowercase())).unwrap_or(PathBuf::new());
334 }
335 if let Some(d) = regex_captures!(r"\\\\\?\\[A-Za-z]\:", &p) {
336 return PathBuf::from_str(&(d.to_uppercase() + &p[6..].to_lowercase())).unwrap_or(PathBuf::new());
337 }
338
339 if let Some(d) = regex_captures!(r"^[A-Za-z]\:", &p) {
341 return PathBuf::from_str(&(r"\\?\".to_owned() + &d.to_uppercase() + &p[2..].to_lowercase())).unwrap_or(PathBuf::new());
342 }
343 if regex_is_match!(r"^(\\\\([^?]|$))", &p) {
344 return PathBuf::from_str(&(r"\\?\UNC".to_owned() + &p[1..].to_lowercase())).unwrap_or(PathBuf::new());
345 }
346
347 PathBuf::from_str(&p).unwrap_or(PathBuf::new())
348}
349
350#[cfg(test)]
351mod test {
352 use super::*;
353
354 #[test]
355 fn extension_and_base_name() {
356 assert!(FlexPath::new_common("a.x").has_extensions([".x", ".y"]));
357 assert_eq!("a.y", FlexPath::new_common("a.x").change_extension(".y").to_string());
358 assert_eq!("a.0", FlexPath::new_common("a.x.y").change_extension(".0").to_string());
359 assert_eq!("a.0.1", FlexPath::new_common("a.x.y").change_extension(".0.1").to_string());
360
361 assert_eq!("qux.html", FlexPath::new_common("foo/qux.html").base_name());
362 assert_eq!("qux", FlexPath::new_common("foo/qux.html").base_name_without_ext([".html"]));
363 }
364
365 #[test]
366 fn resolution() {
367 assert_eq!("a", FlexPath::from_n_common(["a/b/.."]).to_string());
368 assert_eq!("a", FlexPath::from_n_common(["a", "b", ".."]).to_string());
369 assert_eq!("/a/b", FlexPath::new_common("/c").resolve("/a/b").to_string());
370 assert_eq!("a", FlexPath::new_common("a/b").resolve("..").to_string());
371 assert_eq!("a/b", FlexPath::new_common("a/b/").to_string());
372 assert_eq!("a/b", FlexPath::new_common("a//b").to_string());
373
374 let windows = FlexPathVariant::Windows;
375 assert_eq!(r"\\Whack\a\Box", FlexPath::from_n(["foo", r"\\Whack////a//Box", "..", "Box"], windows).to_string());
376 assert_eq!(r"\\?\X:\", FlexPath::from_n([r"\\?\X:", r".."], windows).to_string());
377 assert_eq!(r"\\?\X:\", FlexPath::from_n([r"\\?\X:\", r".."], windows).to_string());
378 assert_eq!(r"\\?\UNC\Whack\a\Box", FlexPath::from_n([r"\\?\UNC\Whack\a\Box", r"..", "Box"], windows).to_string());
379 assert_eq!(r"C:\a", FlexPath::new("C:/", windows).resolve("a").to_string());
380 assert_eq!(r"D:\", FlexPath::new("C:/", windows).resolve("D:/").to_string());
381 assert_eq!(r"D:\a", FlexPath::new("D:/a", windows).to_string());
382 assert_eq!(r"C:\a\f\b", FlexPath::new("a", windows).resolve("C:/a///f//b").to_string());
383 }
384
385 #[test]
386 fn relativity() {
387 assert_eq!("", FlexPath::new_common("/a/b").relative("/a/b"));
388 assert_eq!("c", FlexPath::new_common("/a/b").relative("/a/b/c"));
389 assert_eq!("../../c/d", FlexPath::new_common("/a/b/c").relative("/a/c/d"));
390 assert_eq!("..", FlexPath::new_common("/a/b/c").relative("/a/b"));
391 assert_eq!("../..", FlexPath::new_common("/a/b/c").relative("/a"));
392 assert_eq!("..", FlexPath::new_common("/a").relative("/"));
393 assert_eq!("a", FlexPath::new_common("/").relative("/a"));
394 assert_eq!("", FlexPath::new_common("/").relative("/"));
395 assert_eq!("../../c/d", FlexPath::new_common("/a/b").relative("/c/d"));
396 assert_eq!("../c", FlexPath::new_common("/a/b").relative("/a/c"));
397
398 let windows = FlexPathVariant::Windows;
399 assert_eq!("", FlexPath::new("C:/", windows).relative("C:/"));
400 assert_eq!("", FlexPath::new("C:/foo", windows).relative("C:/foo"));
401 assert_eq!(r"\\foo", FlexPath::new("C:/", windows).relative(r"\\foo"));
402 assert_eq!("../../foo", FlexPath::new(r"\\a/b", windows).relative(r"\\foo"));
403 assert_eq!("D:/", FlexPath::new("C:/", windows).relative(r"D:"));
404 assert_eq!("../bar", FlexPath::new(r"\\?\C:\foo", windows).relative(r"\\?\C:\bar"));
405 }
406
407 #[test]
408 fn normalization() {
409 assert_eq!(PathBuf::from_str(r"\\?\C:\program files").unwrap(), normalize_path(r"C:/Program Files/"));
410 assert_eq!(PathBuf::from_str(r"\\?\UNC\server\foo").unwrap(), normalize_path(r"\\server\foo\"));
411 assert_eq!(PathBuf::from_str(r"\\?\C:\foo").unwrap(), normalize_path(r"\\?\c:/foo/"));
412 assert_eq!(PathBuf::from_str(r"\\?\UNC\server\foo").unwrap(), normalize_path(r"\\?\unc\server\Foo\"));
413 }
414}