1use lazy_regex::*;
32
33pub(crate) mod common;
34pub(crate) mod flexible;
35
36#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug)]
44pub enum FlexPathVariant {
45 Common,
47 Windows,
49}
50
51impl FlexPathVariant {
52 pub(crate) const NATIVE: Self = {
53 #[cfg(target_os = "windows")] {
54 Self::Windows
55 }
56 #[cfg(not(target_os = "windows"))] {
57 Self::Common
58 }
59 };
60
61 pub const fn native() -> Self {
63 Self::NATIVE
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
70pub struct FlexPath(String, FlexPathVariant);
71
72impl FlexPath {
73 pub fn new(path: &str, variant: FlexPathVariant) -> Self {
76 Self(flexible::resolve_one(path, variant), variant)
77 }
78
79 pub fn new_common(path: &str) -> Self {
82 Self(flexible::resolve_one(path, FlexPathVariant::Common), FlexPathVariant::Common)
83 }
84
85 pub fn new_native(path: &str) -> Self {
88 Self(flexible::resolve_one(path, FlexPathVariant::NATIVE), FlexPathVariant::NATIVE)
89 }
90
91 pub fn from_n<'a, T: IntoIterator<Item = &'a str>>(paths: T, variant: FlexPathVariant) -> Self {
93 Self(flexible::resolve_n(paths, variant), variant)
94 }
95
96 pub fn from_n_common<'a, T: IntoIterator<Item = &'a str>>(paths: T) -> Self {
98 Self::from_n(paths, FlexPathVariant::Common)
99 }
100
101 pub fn from_n_native<'a, T: IntoIterator<Item = &'a str>>(paths: T) -> Self {
104 Self::from_n(paths, FlexPathVariant::NATIVE)
105 }
106
107 pub fn variant(&self) -> FlexPathVariant {
109 self.1
110 }
111
112 pub fn is_absolute(&self) -> bool {
114 flexible::is_absolute(&self.0, self.1)
115 }
116
117 pub fn resolve(&self, path2: &str) -> FlexPath {
126 FlexPath(flexible::resolve(&self.0, path2, self.1), self.1)
127 }
128
129 pub fn resolve_n<'a, T: IntoIterator<Item = &'a str>>(&self, paths: T) -> FlexPath {
133 FlexPath(flexible::resolve(&self.0, &flexible::resolve_n(paths, self.1), self.1), self.1)
134 }
135
136 pub fn relative(&self, to_path: &str) -> String {
162 flexible::relative(&self.0, to_path, self.1)
163 }
164
165 pub fn change_extension(&self, extension: &str) -> FlexPath {
182 Self(change_extension(&self.0, extension), self.1)
183 }
184
185 pub fn change_last_extension(&self, extension: &str) -> FlexPath {
194 Self(change_last_extension(&self.0, extension), self.1)
195 }
196
197 pub fn has_extension(&self, extension: &str) -> bool {
201 has_extension(&self.0, extension)
202 }
203
204 pub fn has_extensions<'a, T: IntoIterator<Item = &'a str>>(&self, extensions: T) -> bool {
208 has_extensions(&self.0, extensions)
209 }
210
211 pub fn base_name(&self) -> String {
220 base_name(&self.0)
221 }
222
223 pub fn base_name_without_ext<'a, T>(&self, extensions: T) -> String
234 where T: IntoIterator<Item = &'a str>
235 {
236 base_name_without_ext(&self.0, extensions)
237 }
238
239 pub fn to_string_with_flex_separator(&self) -> String {
243 if self.variant() == FlexPathVariant::Windows {
244 self.0.replace('/', "\\")
245 } else {
246 self.0.clone()
247 }
248 }
249}
250
251impl ToString for FlexPath {
252 fn to_string(&self) -> String {
255 self.0.clone()
256 }
257}
258
259static STARTS_WITH_PATH_SEPARATOR: Lazy<Regex> = lazy_regex!(r"^[/\\]");
260
261fn change_extension(path: &str, extension: &str) -> String {
262 let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension;
263 if regex_find!(r"(\.[^\.]+)+$", path).is_none() {
264 return path.to_owned() + &extension;
265 }
266 regex_replace!(r"(\.[^\.]+)+$", path, |_, _| &extension).into_owned()
267}
268
269fn change_last_extension(path: &str, extension: &str) -> String {
270 let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension;
271 assert!(
272 extension[1..].find('.').is_none(),
273 "The argument to hydroperfox_filepaths::change_last_extension() must only contain one extension; got {}",
274 extension
275 );
276 if regex_find!(r"(\..+)$", path).is_none() {
277 return path.to_owned() + &extension;
278 }
279 regex_replace!(r"(\..+)$", path, |_, _| &extension).into_owned()
280}
281
282fn extension_arg(extension: &str) -> String {
284 (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension
285}
286
287fn has_extension(path: &str, extension: &str) -> bool {
288 let extension = (if extension.starts_with('.') { "" } else { "." }).to_owned() + extension;
289 path.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
311#[cfg(test)]
312mod test {
313 use super::*;
314
315 #[test]
316 fn extension_and_base_name() {
317 assert!(FlexPath::new_common("a.x").has_extensions([".x", ".y"]));
318 assert_eq!("a.y", FlexPath::new_common("a.x").change_extension(".y").to_string());
319 assert_eq!("a.0", FlexPath::new_common("a.x.y").change_extension(".0").to_string());
320 assert_eq!("a.0.1", FlexPath::new_common("a.x.y").change_extension(".0.1").to_string());
321
322 assert_eq!("qux.html", FlexPath::new_common("foo/qux.html").base_name());
323 assert_eq!("qux", FlexPath::new_common("foo/qux.html").base_name_without_ext([".html"]));
324 }
325
326 #[test]
327 fn resolution() {
328 assert_eq!("a", FlexPath::from_n_common(["a/b/.."]).to_string());
329 assert_eq!("a", FlexPath::from_n_common(["a", "b", ".."]).to_string());
330 assert_eq!("/a/b", FlexPath::new_common("/c").resolve("/a/b").to_string());
331 assert_eq!("a", FlexPath::new_common("a/b").resolve("..").to_string());
332 assert_eq!("a/b", FlexPath::new_common("a/b/").to_string());
333 assert_eq!("a/b", FlexPath::new_common("a//b").to_string());
334
335 let windows = FlexPathVariant::Windows;
336 assert_eq!(r"\\Whack/a/Box", FlexPath::from_n(["foo", r"\\Whack////a//Box", "..", "Box"], windows).to_string());
337 assert_eq!("C:/a", FlexPath::new("C:/", windows).resolve("a").to_string());
338 assert_eq!("D:/", FlexPath::new("C:/", windows).resolve("D:/").to_string());
339 assert_eq!("D:/a", FlexPath::new("D:/a", windows).to_string());
340 assert_eq!("C:/a/f/b", FlexPath::new("a", windows).resolve("C:/a///f//b").to_string());
341 }
342
343 #[test]
344 fn relativity() {
345 assert_eq!("", FlexPath::new_common("/a/b").relative("/a/b"));
346 assert_eq!("c", FlexPath::new_common("/a/b").relative("/a/b/c"));
347 assert_eq!("../../c/d", FlexPath::new_common("/a/b/c").relative("/a/c/d"));
348 assert_eq!("..", FlexPath::new_common("/a/b/c").relative("/a/b"));
349 assert_eq!("../..", FlexPath::new_common("/a/b/c").relative("/a"));
350 assert_eq!("..", FlexPath::new_common("/a").relative("/"));
351 assert_eq!("a", FlexPath::new_common("/").relative("/a"));
352 assert_eq!("", FlexPath::new_common("/").relative("/"));
353 assert_eq!("../../c/d", FlexPath::new_common("/a/b").relative("/c/d"));
354 assert_eq!("../c", FlexPath::new_common("/a/b").relative("/a/c"));
355
356 let windows = FlexPathVariant::Windows;
357 assert_eq!("", FlexPath::new("C:/", windows).relative("C:/"));
358 assert_eq!("", FlexPath::new("C:/foo", windows).relative("C:/foo"));
359 assert_eq!(r"\\foo", FlexPath::new("C:/", windows).relative(r"\\foo"));
360 assert_eq!("../../foo", FlexPath::new(r"\\a/b", windows).relative(r"\\foo"));
361 assert_eq!("D:/", FlexPath::new("C:/", windows).relative(r"D:"));
362 }
363}