Skip to main content

ferrijs_std/
pathutil.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use std::{
4    borrow::Cow,
5    path::{Component, Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR},
6};
7
8use crate::utils::module::{export_default, ModuleInfo};
9use rquickjs::{
10    function::Opt,
11    module::{Declarations, Exports, ModuleDef},
12    prelude::{Func, Rest},
13    Ctx, Object, Result,
14};
15
16pub struct PathModule;
17
18#[cfg(windows)]
19const DELIMITER: char = ';';
20#[cfg(not(windows))]
21const DELIMITER: char = ':';
22
23#[cfg(windows)]
24pub const CURRENT_DIR_STR: &str = ".\\";
25
26#[cfg(windows)]
27const FORWARD_SLASH_STR: &str = "/";
28
29#[cfg(not(windows))]
30pub const CURRENT_DIR_STR: &str = "./";
31
32#[cfg(windows)]
33use memchr::{memchr, memchr2, memchr2_iter};
34
35#[cfg(windows)]
36pub fn replace_backslash(path: impl Into<String>) -> String {
37    let mut path = path.into();
38    let bytes = unsafe { path.as_bytes_mut() };
39
40    let mut start = 0;
41    while let Some(pos) = memchr(b'\\', &bytes[start..]) {
42        bytes[start + pos] = b'/';
43        start += pos + 1;
44    }
45    path
46}
47
48#[cfg(not(windows))]
49pub fn replace_backslash(path: impl Into<String>) -> String {
50    path.into().replace('\\', "/")
51}
52
53#[cfg(windows)]
54fn find_next_separator(s: &str) -> Option<usize> {
55    memchr2(b'\\', b'/', s.as_bytes())
56}
57
58#[cfg(not(windows))]
59fn find_next_separator(s: &str) -> Option<usize> {
60    s.find(MAIN_SEPARATOR)
61}
62
63#[cfg(windows)]
64fn find_last_sep(path: &str) -> Option<usize> {
65    memchr2_iter(b'\\', b'/', path.as_bytes()).next_back()
66}
67
68#[cfg(not(windows))]
69fn find_last_sep(path: &str) -> Option<usize> {
70    path.rfind(MAIN_SEPARATOR)
71}
72
73pub fn dirname<'a, P: Into<Cow<'a, str>>>(path: P) -> String {
74    let path = path.into();
75    let len = path.len();
76
77    if len == 0 {
78        return ".".into();
79    }
80
81    let bytes = path.as_bytes();
82
83    #[cfg(windows)]
84    {
85        if len == 1 {
86            return if is_sep(bytes[0]) {
87                path.into_owned()
88            } else {
89                ".".to_string()
90            };
91        }
92
93        // Determine root end and search offset
94        let (root_end, offset) = if is_sep(bytes[0]) {
95            if is_sep(bytes[1]) {
96                // UNC path: \\server\share
97                parse_unc_root(bytes, len).unwrap_or((1, 1))
98            } else {
99                (1, 1)
100            }
101        } else if bytes.len() > 1 && is_drive_letter(bytes[0]) && bytes[1] == b':' {
102            let r = if len > 2 && is_sep(bytes[2]) { 3 } else { 2 };
103            (r, r)
104        } else {
105            (0, 0)
106        };
107
108        // Find last separator (skipping trailing separators)
109        let end = find_dirname_end(bytes, offset);
110
111        match end {
112            Some(e) => &path[..e],
113            None if root_end > 0 => &path[..root_end],
114            None => ".",
115        }
116        .into()
117    }
118
119    #[cfg(not(windows))]
120    {
121        if len == 1 {
122            return if bytes[0] == b'/' {
123                path.into_owned()
124            } else {
125                ".".into()
126            };
127        }
128
129        let has_root = bytes[0] == b'/';
130        let end = find_dirname_end(bytes, 1);
131
132        match end {
133            Some(e) if has_root && e == 1 => "//",
134            Some(e) => &path[..e],
135            None if has_root => "/",
136            None => ".",
137        }
138        .into()
139    }
140}
141
142#[cfg(windows)]
143fn is_sep(c: u8) -> bool {
144    c == b'/' || c == b'\\'
145}
146
147#[cfg(windows)]
148fn is_drive_letter(c: u8) -> bool {
149    c.is_ascii_alphabetic()
150}
151
152#[cfg(windows)]
153fn parse_unc_root(bytes: &[u8], len: usize) -> Option<(usize, usize)> {
154    let mut j = 2;
155    // Skip server name
156    while j < len && !is_sep(bytes[j]) {
157        j += 1;
158    }
159    if j >= len || j == 2 {
160        return None;
161    }
162    // Skip separators
163    while j < len && is_sep(bytes[j]) {
164        j += 1;
165    }
166    if j >= len {
167        return None;
168    }
169    let share_start = j;
170    // Skip share name
171    while j < len && !is_sep(bytes[j]) {
172        j += 1;
173    }
174    if j == share_start {
175        return None;
176    }
177    if j == len {
178        return None;
179    } // UNC root only - caller handles this
180    Some((j + 1, j + 1))
181}
182
183fn find_dirname_end(bytes: &[u8], offset: usize) -> Option<usize> {
184    let mut matched_slash = true;
185    for i in (offset..bytes.len()).rev() {
186        #[cfg(windows)]
187        let is_separator = is_sep(bytes[i]);
188        #[cfg(not(windows))]
189        let is_separator = bytes[i] == b'/';
190
191        if is_separator {
192            if !matched_slash {
193                return Some(i);
194            }
195        } else {
196            matched_slash = false;
197        }
198    }
199    None
200}
201
202pub fn name_extname(path: &str) -> (&str, &str) {
203    let path = strip_last_sep(path);
204    let sep_pos = find_last_sep(path);
205
206    let path = match sep_pos {
207        Some(idx) => &path[idx + 1..],
208        None => path,
209    };
210    if path.starts_with('.') {
211        return (path, "");
212    }
213    match path.rfind('.') {
214        Some(idx) => path.split_at(idx),
215        None => (path, ""),
216    }
217}
218
219fn strip_last_sep(path: &str) -> &str {
220    if ends_with_sep(path) {
221        &path[..path.len() - 1]
222    } else {
223        path
224    }
225}
226
227pub fn basename(path: String, suffix: Opt<String>) -> String {
228    #[cfg(windows)]
229    {
230        if path.is_empty() || path == MAIN_SEPARATOR_STR || path == FORWARD_SLASH_STR {
231            return String::from("");
232        }
233    }
234    #[cfg(not(windows))]
235    {
236        if path.is_empty() || path == MAIN_SEPARATOR_STR {
237            return String::from("");
238        }
239    }
240
241    let (base, ext) = name_extname(&path);
242    let mut name = [base, ext].concat();
243    if let Some(suffix) = suffix.0 {
244        if let Some(location) = name.rfind(&suffix) {
245            name.truncate(location);
246            return name;
247        }
248    }
249    name
250}
251
252fn extname(path: String) -> String {
253    let (_, ext) = name_extname(&path);
254    ext.to_string()
255}
256
257fn format(obj: Object) -> String {
258    let dir: String = obj.get("dir").unwrap_or_default();
259    let root: String = obj.get("root").unwrap_or_default();
260    let base: String = obj.get("base").unwrap_or_default();
261    let name: String = obj.get("name").unwrap_or_default();
262    let ext: String = obj.get("ext").unwrap_or_default();
263
264    let mut path = String::new();
265    if !dir.is_empty() {
266        path.push_str(&dir);
267        if !ends_with_sep(&dir) {
268            path.push(MAIN_SEPARATOR);
269        }
270    } else if !root.is_empty() {
271        path.push_str(&root);
272        if !ends_with_sep(&root) {
273            path.push(MAIN_SEPARATOR);
274        }
275    }
276    if !base.is_empty() {
277        path.push_str(&base);
278    } else {
279        path.push_str(&name);
280        if !ext.is_empty() {
281            if !ext.starts_with('.') {
282                path.push('.');
283            }
284            path.push_str(&ext);
285        }
286    }
287    path
288}
289
290fn parse(ctx: Ctx, path_str: String) -> Result<Object> {
291    let obj = Object::new(ctx)?;
292    let path = Path::new(&path_str);
293    let parent = path
294        .parent()
295        .map(|p| p.to_str().unwrap())
296        .unwrap_or_default();
297    let filename = path
298        .file_name()
299        .map(|n| n.to_str().unwrap())
300        .unwrap_or_default();
301
302    let (name, extension) = name_extname(filename);
303
304    let root = path
305        .components()
306        .next()
307        .and_then(|c| match c {
308            Component::Prefix(prefix) => prefix.as_os_str().to_str(),
309            Component::RootDir => c.as_os_str().to_str(),
310            _ => Some(""),
311        })
312        .unwrap_or_default();
313
314    obj.set("root", root)?;
315    obj.set("dir", parent)?;
316    obj.set("base", [name, extension].concat())?;
317    obj.set("ext", extension)?;
318    obj.set("name", name)?;
319
320    Ok(obj)
321}
322
323fn join(parts: Rest<String>) -> String {
324    join_path(parts.0.iter())
325}
326
327pub fn join_path<S, I>(parts: I) -> String
328where
329    S: AsRef<str>,
330    I: IntoIterator<Item = S>,
331{
332    join_path_with_separator(parts, false)
333}
334
335pub fn join_path_with_separator<S, I>(parts: I, force_posix_sep: bool) -> String
336where
337    S: AsRef<str>,
338    I: IntoIterator<Item = S>,
339{
340    //fine because we're either moving or storing references
341    let parts_vec: Vec<S> = parts.into_iter().collect();
342    //add one slash plus drive letter
343    //max is probably parts+size
344    let likely_max_size = parts_vec
345        .iter()
346        .map(|p| p.as_ref().len() + 1)
347        .sum::<usize>()
348        + 10;
349    let result = String::with_capacity(likely_max_size);
350    join_resolve_path(parts_vec, false, result, PathBuf::new(), force_posix_sep)
351}
352
353pub fn resolve_path<S, I>(parts: I) -> Result<String>
354where
355    S: AsRef<str>,
356    I: IntoIterator<Item = S>,
357{
358    resolve_path_with_separator(parts, false)
359}
360
361pub fn resolve_path_with_separator<S, I>(parts: I, force_posix_sep: bool) -> Result<String>
362where
363    S: AsRef<str>,
364    I: IntoIterator<Item = S>,
365{
366    let cwd = std::env::current_dir()?;
367
368    let mut result = cwd.clone().into_os_string().into_string().unwrap();
369    //add MAIN_SEPARATOR if we're not on already MAIN_SEPARATOR
370    if !result.ends_with(MAIN_SEPARATOR) {
371        result.push(MAIN_SEPARATOR);
372    }
373    #[cfg(windows)]
374    {
375        if force_posix_sep {
376            result = result.replace(MAIN_SEPARATOR, FORWARD_SLASH_STR);
377        }
378    }
379    Ok(join_resolve_path(parts, true, result, cwd, force_posix_sep))
380}
381
382pub fn relative<F, T>(from: F, to: T) -> Result<String>
383where
384    F: AsRef<str>,
385    T: AsRef<str>,
386{
387    let from_ref = from.as_ref();
388    let to_ref = to.as_ref();
389    if from_ref == to_ref {
390        return Ok("".into());
391    }
392
393    let mut abs_from = None;
394
395    if !is_absolute(from_ref) {
396        abs_from = Some(
397            std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + from_ref,
398        );
399    }
400
401    let mut abs_to = None;
402
403    if !is_absolute(to_ref) {
404        abs_to = Some(
405            std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + to_ref,
406        );
407    }
408
409    let from_ref = abs_from.as_deref().unwrap_or(from_ref);
410    let to_ref = abs_to.as_deref().unwrap_or(to_ref);
411
412    let mut from_index = 0;
413    let mut to_index = 0;
414    // skip common prefix
415    while from_index < from_ref.len() && to_index < to_ref.len() {
416        let from_next = find_next_separator(&from_ref[from_index..])
417            .unwrap_or(from_ref.len() - from_index)
418            + from_index;
419        let to_next =
420            find_next_separator(&to_ref[to_index..]).unwrap_or(to_ref.len() - to_index) + to_index;
421        if from_ref[from_index..from_next] != to_ref[to_index..to_next] {
422            break;
423        }
424        from_index = from_next + 1; //move past the separator
425        to_index = to_next + 1; //move past the separator
426    }
427    let mut relative = String::new();
428    // add ".." for each remaining component in 'from'
429    while from_index < from_ref.len() {
430        let from_next = find_next_separator(&from_ref[from_index..])
431            .unwrap_or(from_ref.len() - from_index)
432            + from_index;
433        if !relative.is_empty() {
434            relative.push(MAIN_SEPARATOR);
435        }
436        relative.push_str("..");
437        from_index = from_next + 1; // Move past the separator
438    }
439    // add the remaining components from 'to'
440    while to_index < to_ref.len() {
441        let to_next =
442            find_next_separator(&to_ref[to_index..]).unwrap_or(to_ref.len() - to_index) + to_index;
443        if !relative.is_empty() {
444            relative.push(MAIN_SEPARATOR);
445        }
446        let component = &to_ref[to_index..to_next];
447        if component != "." {
448            relative.push_str(component);
449        }
450        to_index = to_next + 1; // Move past the separator
451    }
452    Ok(if relative.is_empty() {
453        ".".into()
454    } else {
455        relative
456    })
457}
458
459fn join_resolve_path<S, I>(
460    parts: I,
461    resolve: bool,
462    mut result: String,
463    cwd: PathBuf,
464    force_posix_sep: bool,
465) -> String
466where
467    S: AsRef<str>,
468    I: IntoIterator<Item = S>,
469{
470    let (sep, sep_str) = if force_posix_sep {
471        ('/', "/")
472    } else {
473        (MAIN_SEPARATOR, MAIN_SEPARATOR_STR)
474    };
475
476    let mut resolve_cow: Cow<str>;
477    let mut empty = true;
478    let mut prefix_len = 0;
479
480    let mut index_stack = Vec::with_capacity(16);
481
482    // Remove the trailing sep: /a/b// -> /a/b
483    if ends_with_sep(&result) && result.len() > 1 {
484        result.truncate(result.len() - 1);
485    }
486
487    for part in parts {
488        let mut part_ref: &str = part.as_ref();
489        let mut start = 0;
490        if resolve {
491            if cfg!(not(windows)) {
492                if part_ref.starts_with(MAIN_SEPARATOR) {
493                    empty = false;
494                    result = MAIN_SEPARATOR.into();
495                    start = 1;
496                }
497            } else {
498                let starts_with_sep = starts_with_sep(part_ref);
499                if starts_with_sep {
500                    let (prefix, _) = get_path_prefix(&cwd);
501                    prefix_len = prefix.len();
502                    result = prefix;
503                    empty = false;
504                    result.push(sep);
505                } else {
506                    let path_buf: PathBuf = PathBuf::from(part_ref);
507                    if path_buf.is_absolute() {
508                        empty = false;
509                        let (prefix, mut components) = get_path_prefix(&path_buf);
510                        if !prefix.is_empty() {
511                            components.next(); //consume prefix
512                        }
513                        prefix_len = prefix.len();
514                        result = prefix;
515                        result.push(sep);
516                        resolve_cow = components
517                            .map(|comp| comp.as_os_str().to_str().unwrap_or_default()) // Convert each component to &str
518                            .collect::<Vec<&str>>() // Collect into a vector of &str
519                            .join(sep_str)
520                            .into();
521                        part_ref = resolve_cow.as_ref();
522                    }
523                }
524            }
525        } else if starts_with_sep(part_ref) && empty {
526            empty = false;
527            result.push(sep);
528            start = 1;
529        }
530
531        while start < part_ref.len() {
532            let end = find_next_separator(&part_ref[start..]).map_or(part_ref.len(), |i| i + start);
533            match &part_ref[start..end] {
534                ".." => {
535                    if let Some(last_index) = index_stack.pop() {
536                        result.truncate(last_index);
537                    } else if empty {
538                        if let Some(last_index) = find_last_sep(&result) {
539                            result.truncate(last_index);
540                        }
541                    }
542                },
543                "" | "." => {
544                    //ignore
545                },
546                sub_part => {
547                    let len = result.len();
548                    if !result.ends_with(sep) && !result.is_empty() {
549                        result.push(sep);
550                    }
551                    result.push_str(sub_part);
552                    result.push(sep);
553                    index_stack.push(len);
554                },
555            }
556            start = end + 1;
557        }
558    }
559
560    if result.len() > prefix_len + 1 && ends_with_sep(&result) {
561        result.truncate(result.len() - 1);
562    }
563
564    result
565}
566
567pub fn resolve(path: Rest<String>) -> Result<String> {
568    resolve_path(path.iter())
569}
570
571fn get_path_prefix(cwd: &Path) -> (String, std::iter::Peekable<std::path::Components<'_>>) {
572    let mut components = cwd.components().peekable();
573
574    let prefix = if let Some(Component::Prefix(prefix)) = components.peek() {
575        prefix.as_os_str().to_str().unwrap().to_string()
576    } else {
577        "".into()
578    };
579
580    (prefix, components)
581}
582
583pub fn normalize<P: AsRef<str>>(path: P) -> String {
584    join_path([path].iter())
585}
586
587#[allow(dead_code)] //used by windows
588fn starts_with_sep(path: &str) -> bool {
589    matches!(path.as_bytes().first().unwrap_or(&0), b'/' | b'\\')
590}
591
592#[cfg(windows)]
593pub fn ends_with_sep(path: &str) -> bool {
594    matches!(path.as_bytes().last().unwrap_or(&0), b'/' | b'\\')
595}
596
597#[cfg(not(windows))]
598pub fn ends_with_sep(path: &str) -> bool {
599    path.ends_with(MAIN_SEPARATOR)
600}
601
602#[cfg(windows)]
603pub fn is_absolute(path: &str) -> bool {
604    starts_with_sep(path) || PathBuf::from(path).is_absolute()
605}
606
607#[cfg(not(windows))]
608pub fn is_absolute(path: &str) -> bool {
609    path.starts_with(MAIN_SEPARATOR)
610}
611
612impl ModuleDef for PathModule {
613    fn declare(declare: &Declarations) -> Result<()> {
614        declare.declare("basename")?;
615        declare.declare("dirname")?;
616        declare.declare("extname")?;
617        declare.declare("format")?;
618        declare.declare("parse")?;
619        declare.declare("join")?;
620        declare.declare("resolve")?;
621        declare.declare("relative")?;
622        declare.declare("normalize")?;
623        declare.declare("isAbsolute")?;
624        declare.declare("delimiter")?;
625        declare.declare("sep")?;
626
627        declare.declare("default")?;
628        Ok(())
629    }
630
631    fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
632        export_default(ctx, exports, |default| {
633            default.set("dirname", Func::from(dirname::<String>))?;
634            default.set("basename", Func::from(basename))?;
635            default.set("extname", Func::from(extname))?;
636            default.set("format", Func::from(format))?;
637            default.set("parse", Func::from(parse))?;
638            default.set("join", Func::from(join))?;
639            default.set("relative", Func::from(relative::<String, String>))?;
640            default.set("resolve", Func::from(resolve))?;
641            default.set("normalize", Func::from(normalize::<String>))?;
642            default.set("isAbsolute", Func::from(|s: String| is_absolute(&s)))?;
643            default.prop("delimiter", DELIMITER.to_string())?;
644            default.prop("sep", MAIN_SEPARATOR.to_string())?;
645            Ok(())
646        })
647    }
648}
649
650impl From<PathModule> for ModuleInfo<PathModule> {
651    fn from(val: PathModule) -> Self {
652        ModuleInfo {
653            name: "path",
654            module: val,
655        }
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use std::{env::set_current_dir, sync::Mutex};
662
663    static THREAD_LOCK: Lazy<Mutex<()>> = Lazy::new(Mutex::default);
664
665    use once_cell::sync::Lazy;
666
667    use super::*;
668
669    #[test]
670    fn test_relative() {
671        let _shared = THREAD_LOCK.lock().unwrap();
672        let cwd = std::env::current_dir().expect("unable to get current working directory");
673        set_current_dir("/").expect("unable to set working directory to /");
674
675        assert_eq!(
676            relative("a/b/c", "b/c").unwrap(),
677            "../../../b/c".replace('/', MAIN_SEPARATOR_STR)
678        );
679        assert_eq!(
680            relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb").unwrap(),
681            "../../impl/bbb".replace('/', MAIN_SEPARATOR_STR)
682        );
683        assert_eq!(
684            relative("/a/b/c", "/a/d").unwrap(),
685            "../../d".replace('/', MAIN_SEPARATOR_STR)
686        );
687        assert_eq!(relative("/a/b/c", "/a/b/c/d").unwrap(), "d");
688        assert_eq!(relative("/a/b/c", "/a/b/c").unwrap(), "");
689
690        assert_eq!(
691            relative("a/b", "a/b/c/d").unwrap(),
692            "c/d".replace('/', MAIN_SEPARATOR_STR)
693        );
694        assert_eq!(
695            relative("a/b/c", "b/c").unwrap(),
696            "../../../b/c".replace('/', MAIN_SEPARATOR_STR)
697        );
698
699        set_current_dir(cwd).expect("unable to set working directory back");
700    }
701
702    #[test]
703    fn test_dirname() {
704        assert_eq!(dirname("/usr/local/bin".to_string()), "/usr/local");
705        assert_eq!(dirname("/usr/local/".to_string()), "/usr");
706        assert_eq!(dirname("usr/local/bin".to_string()), "usr/local");
707        assert_eq!(dirname("/".to_string()), "/");
708        assert_eq!(dirname("".to_string()), ".");
709    }
710
711    #[test]
712    fn test_basename() {
713        assert_eq!(basename("/usr/local/bin".to_string(), Opt(None)), "bin");
714        assert_eq!(
715            basename("/usr/local/bin.txt".to_string(), Opt(None)),
716            "bin.txt"
717        );
718        assert_eq!(
719            basename(
720                "/usr/local/bin.txt".to_string(),
721                Opt(Some(".txt".to_string()))
722            ),
723            "bin"
724        );
725        assert_eq!(basename("".to_string(), Opt(None)), "");
726        assert_eq!(basename("/".to_string(), Opt(None)), "");
727    }
728
729    #[test]
730    fn test_extname() {
731        assert_eq!(extname("/usr/local/bin.txt".to_string()), ".txt");
732        assert_eq!(extname("/usr/local/bin".to_string()), "");
733        assert_eq!(extname("file.tar.gz".to_string()), ".gz");
734        assert_eq!(extname(".bashrc".to_string()), "");
735        assert_eq!(extname("".to_string()), "");
736    }
737
738    #[test]
739    fn test_join() {
740        // Standard cases
741        assert_eq!(
742            join_path(["/usr", "local", "bin"].iter()),
743            "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR)
744        );
745        assert_eq!(
746            join_path(["/usr", "/local", "bin"].iter()),
747            "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR)
748        );
749        assert_eq!(
750            join_path(["usr", "local", "bin"].iter()),
751            "usr/local/bin".replace('/', MAIN_SEPARATOR_STR)
752        );
753        assert_eq!(join_path(["", "bin"].iter()), "bin");
754
755        // Complex cases
756        assert_eq!(
757            join_path(["/usr", "..", "local", "bin"].iter()),
758            "/local/bin".replace('/', MAIN_SEPARATOR_STR)
759        ); // Parent dir
760        assert_eq!(
761            join_path([".", "usr", "local"]),
762            "usr/local".replace('/', MAIN_SEPARATOR_STR)
763        ); // Current dir
764        assert_eq!(
765            join_path(["/usr", ".", "bin"].iter()),
766            "/usr/bin".replace('/', MAIN_SEPARATOR_STR)
767        ); // Current dir in middle
768        assert_eq!(
769            join_path(["usr", "local", "bin", ".."].iter()),
770            "usr/local".replace('/', MAIN_SEPARATOR_STR)
771        ); // Ending with parent dir
772        assert_eq!(
773            join_path(["/usr", "local", "", "bin"].iter()),
774            "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR)
775        ); // Empty component in path
776        assert_eq!(
777            join_path(["/usr", "local", ".hidden"].iter()),
778            "/usr/local/.hidden".replace('/', MAIN_SEPARATOR_STR)
779        ); // Hidden file
780    }
781
782    #[test]
783    fn test_resolve_path() {
784        let _shared = THREAD_LOCK.lock().unwrap();
785        let prefix = if cfg!(windows) {
786            if let Some(Component::Prefix(prefix)) =
787                std::env::current_dir().unwrap().components().next()
788            {
789                prefix.as_os_str().to_str().unwrap().to_string()
790            } else {
791                "".into()
792            }
793        } else {
794            "".into()
795        };
796
797        assert_eq!(
798            resolve_path(["", "foo/bar"].iter()).unwrap(),
799            std::env::current_dir()
800                .unwrap()
801                .join("foo/bar".replace('/', MAIN_SEPARATOR_STR))
802                .to_string_lossy()
803                .to_string()
804        );
805
806        // Standard cases
807        assert_eq!(
808            resolve_path(["/"].iter()).unwrap(),
809            prefix.clone() + MAIN_SEPARATOR_STR
810        );
811
812        // Standard cases
813        assert_eq!(
814            resolve_path(["/foo/bar", "../baz"].iter()).unwrap(),
815            prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR)
816        );
817        assert_eq!(
818            resolve_path(["/foo/bar", "./baz"].iter()).unwrap(),
819            prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR)
820        );
821        assert_eq!(
822            resolve_path(["foo/bar", "/baz"].iter()).unwrap(),
823            prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR)
824        );
825
826        // Complex cases
827        assert_eq!(
828            resolve_path(["/foo", "bar", ".", "baz"].iter()).unwrap(),
829            prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR)
830        ); // Current dir in middle
831        assert_eq!(
832            resolve_path(["/foo", "bar", "..", "baz"].iter()).unwrap(),
833            prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR)
834        ); // Parent dir in middle
835        assert_eq!(
836            resolve_path(["/foo", "bar", "../..", "baz"].iter()).unwrap(),
837            prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR)
838        ); // Double parent dir
839        assert_eq!(
840            resolve_path(["/foo", "bar", ".hidden"].iter()).unwrap(),
841            prefix.clone() + &"/foo/bar/.hidden".replace('/', MAIN_SEPARATOR_STR)
842        ); // Hidden file
843        assert_eq!(
844            resolve_path(["/foo", ".", "bar", "."].iter()).unwrap(),
845            prefix.clone() + &"/foo/bar".replace('/', MAIN_SEPARATOR_STR)
846        ); // Multiple current dirs
847        assert_eq!(
848            resolve_path(["/foo", "..", "..", "bar"].iter()).unwrap(),
849            prefix.clone() + &"/bar".replace('/', MAIN_SEPARATOR_STR)
850        ); // Multiple parent dirs
851        assert_eq!(
852            resolve_path(["/foo/bar", "/..", "baz"].iter()).unwrap(),
853            prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR)
854        ); // Parent dir with absolute path
855
856        assert_eq!(
857            resolve_path(["../foo"].iter()).unwrap(),
858            std::env::current_dir()
859                .unwrap()
860                .parent()
861                .unwrap()
862                .join("foo".replace('/', MAIN_SEPARATOR_STR))
863                .to_string_lossy()
864                .to_string()
865        ); // Start with ..
866
867        assert_eq!(
868            resolve_path(["../".repeat(32)].iter()).unwrap(),
869            prefix.clone()
870        ); // Many ..
871    }
872
873    #[test]
874    fn test_normalize() {
875        assert_eq!(
876            normalize("/foo//bar//baz"),
877            "/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR)
878        );
879        assert_eq!(
880            normalize("/foo/./bar/../baz"),
881            "/foo/baz".replace('/', MAIN_SEPARATOR_STR)
882        );
883        assert_eq!(
884            normalize("foo/bar/"),
885            "foo/bar".replace('/', MAIN_SEPARATOR_STR)
886        );
887        assert_eq!(normalize("./foo"), "foo");
888    }
889
890    #[test]
891    fn test_is_absolute() {
892        assert!(is_absolute("/usr/local/bin"));
893        assert!(!is_absolute("usr/local/bin"));
894        #[cfg(windows)]
895        assert!(is_absolute("C:\\Program Files")); // for Windows systems
896        assert!(!is_absolute("./local/bin"));
897    }
898
899    #[test]
900    fn test_replace_backslash() {
901        assert_eq!(replace_backslash("C:\\Program Files"), "C:/Program Files");
902        assert_eq!(replace_backslash("/usr/local/bin"), "/usr/local/bin");
903        assert_eq!(replace_backslash("C:\\Users\\User\\"), "C:/Users/User/");
904    }
905}