nodejs_path/path/
posix.rs

1use std::{borrow::Cow, ops::Add, path::PathBuf};
2
3use crate::Parsed;
4
5use super::shared::{
6    format_inner, is_posix_path_separator, normalize_string, CHAR_DOT, CHAR_FORWARD_SLASH,
7};
8
9/// Provides the platform-specific path segment separator:
10/// - `\` on Windows
11/// - `/` on POSIX
12#[allow(non_upper_case_globals)]
13pub const sep: char = '/';
14/// Provides the platform-specific path delimiter:
15/// - `;` for Windows
16/// - `:` for POSIX
17#[allow(non_upper_case_globals)]
18pub const delimiter: char = ':';
19
20///
21/// ```rust
22/// assert_eq!(&nodejs_path::basename_impl("/foo/bar/baz/asdf/quux.html"), "quux.html");
23/// ```
24#[inline]
25pub fn basename_impl(path: &str) -> String {
26    basename_impl_without_ext(path, "")
27}
28
29/// ```rust
30/// assert_eq!(&nodejs_path::basename_impl_without_ext("/foo/bar/baz/asdf/quux.html", ".html"), "quux");
31///
32/// assert_eq!(&nodejs_path::basename_impl_without_ext("/foo/bar/baz/asdf/quux.HTML", ".html"), "quux.HTML");
33///
34/// assert_eq!(&nodejs_path::basename_impl_without_ext("aaa/bbb", "bbb"), "bbb");
35/// ```
36pub fn basename_impl_without_ext(path: &str, ext: &str) -> String {
37    let mut start = 0;
38    let mut end = -1;
39    let mut matched_slash = true;
40
41    let path = path.chars().collect::<Vec<char>>();
42    let ext = ext.chars().collect::<Vec<char>>();
43
44    if ext.len() > 0 && ext.len() <= path.len() {
45        if ext == path {
46            return "".to_owned();
47        }
48        let mut ext_idx = ext.len() as i32 - 1;
49        let mut first_non_slash_end = -1;
50        let mut i = path.len() as i32 - 1;
51        while i >= 0 {
52            let code = path.get(i as usize).unwrap();
53
54            if code == &CHAR_FORWARD_SLASH {
55                // If we reached a path separator that was not part of a set of path
56                // separators at the end of the string, stop now
57                if !matched_slash {
58                    start = i + 1;
59                    break;
60                }
61            } else {
62                if first_non_slash_end == -1 {
63                    // We saw the first non-path separator, remember this index in case
64                    // we need it if the extension ends up not matching
65                    matched_slash = false;
66                    first_non_slash_end = i + 1;
67                }
68                if ext_idx >= 0 {
69                    // Try to match the explicit extension
70                    if code == ext.get(ext_idx as usize).unwrap() {
71                        ext_idx -= 1;
72                        if ext_idx == -1 {
73                            // We matched the extension, so mark this as the end of our path
74                            // component
75                            end = i;
76                        }
77                    } else {
78                        // Extension does not match, so our result is the entire path
79                        // component
80                        ext_idx = -1;
81                        end = first_non_slash_end;
82                    }
83                }
84            }
85
86            i -= 1;
87        }
88
89        if start == end {
90            end = first_non_slash_end
91        } else if end == -1 {
92            end = path.len() as i32
93        }
94
95        return path[start as usize..end as usize].iter().collect();
96    }
97
98    let mut i = path.len() as i32 - 1;
99    while i >= 0 {
100        if path.get(i as usize).unwrap() == &CHAR_FORWARD_SLASH {
101            // If we reached a path separator that was not part of a set of path
102            // separators at the end of the string, stop now
103            if !matched_slash {
104                start = i + 1;
105                break;
106            }
107        } else if end == -1 {
108            // We saw the first non-path separator, mark this as the end of our
109            // path component
110            matched_slash = false;
111            end = i + 1;
112        }
113
114        i -= 1;
115    }
116
117    if end == -1 {
118        return "".to_owned();
119    }
120
121    return path[start as usize..end as usize].iter().collect();
122}
123
124/// Returns the last portion of a path, similar to the Unix basename command. Trailing directory separators are ignored.
125/// ```rust
126/// assert_eq!(&nodejs_path::basename!("/foo/bar/baz/asdf/quux.html"), "quux.html");
127///
128/// assert_eq!(&nodejs_path::basename!("/foo/bar/baz/asdf/quux.html", ".html"), "quux");
129///
130/// assert_eq!(&nodejs_path::basename!("/foo/bar/baz/asdf/quux.HTML", ".html"), "quux.HTML");
131/// ```
132
133#[macro_export]
134macro_rules! basename {
135    (  $x:expr  ) => {{
136        $crate::posix::basename_impl($x)
137    }};
138    (  $x:expr, $y:expr  ) => {{
139        $crate::posix::basename_impl_without_ext($x, $y)
140    }};
141}
142pub use basename;
143
144/// Returns the directory name of a path, similar to the Unix dirname command. Trailing directory separators are ignored,
145/// ```rust
146/// assert_eq!(&nodejs_path::dirname("/foo/bar/baz/asdf/quux"), "/foo/bar/baz/asdf");
147/// ```
148pub fn dirname(path: &str) -> String {
149    if path.len() == 0 {
150        ".".to_owned()
151    } else {
152        let path = path.chars().collect::<Vec<char>>();
153        let has_root = path
154            .iter()
155            .next()
156            .map(|c| c == &CHAR_FORWARD_SLASH)
157            .unwrap_or(false);
158        let mut end = -1;
159        let mut matched_slash = true;
160
161        let mut i = path.len() as i32 - 1;
162        while i >= 1 {
163            if path
164                .get(i as usize)
165                .map(|c| c == &CHAR_FORWARD_SLASH)
166                .unwrap_or(false)
167            {
168                if !matched_slash {
169                    end = i;
170                    break;
171                }
172            } else {
173                // We saw the first non-path separator
174                matched_slash = false;
175            }
176
177            i -= 1;
178        }
179
180        if end == -1 {
181            if has_root {
182                "/".to_owned()
183            } else {
184                ".".to_owned()
185            }
186        } else if has_root && end == 1 {
187            "//".to_owned()
188        } else {
189            path[0..end as usize].iter().collect()
190        }
191    }
192}
193/// Returns the extension of the path, from the last occurrence of the . (period) character to end of string in the last portion of the path. If there is no . in the last portion of the path, or if there are no . characters other than the first character of the basename of path, an empty string is returned.
194/// ```rust
195/// assert_eq!(&nodejs_path::extname("index.html"), ".html");
196///
197/// assert_eq!(&nodejs_path::extname("index.coffee.md"), ".md");
198///
199/// assert_eq!(&nodejs_path::extname("index."), ".");
200///
201/// assert_eq!(&nodejs_path::extname("index"), "");
202///
203/// assert_eq!(&nodejs_path::extname(".index.md"), ".md");
204/// ```
205pub fn extname(path: &str) -> String {
206    parse(path).ext
207}
208
209/// Returns a path string from an object. This is the opposite of nodejs_path::parse().
210
211pub fn format(path_object: Parsed) -> String {
212    format_inner("/", path_object)
213}
214
215/// The method determines if path is an absolute path. If the given path is a zero-length string, false will be returned.
216/// #Example
217/// ```rust
218/// assert_eq!(nodejs_path::posix::is_absolute("/foo/bar"), true);
219/// assert_eq!(nodejs_path::posix::is_absolute("/baz/.."), true);
220/// assert_eq!(nodejs_path::posix::is_absolute("qux/"), false);
221/// assert_eq!(nodejs_path::posix::is_absolute("."), false);  
222/// assert_eq!(nodejs_path::posix::is_absolute(""), false);  
223/// ```
224pub fn is_absolute(path: &str) -> bool {
225    path.bytes().next().map(|c| c == b'/').unwrap_or(false)
226}
227
228/// The method joins all given path segments together using the platform-specific separator as a delimiter, then normalizes the resulting path.
229///
230/// Zero-length path segments are ignored. If the joined path string is a zero-length string then '.' will be returned, representing the current working directory.
231/// ```rust
232/// assert_eq!(nodejs_path::posix::join!("/foo", "bar", "baz/asdf", "quux", ".."), "/foo/bar/baz/asdf");
233/// ```
234#[macro_export]
235macro_rules! join {
236    ( $( $x:expr ),* ) => {
237      {
238        $crate::posix::join_impl(&[
239          $(
240            $x,
241          )*
242        ])
243      }
244    };
245  }
246pub use join;
247
248pub fn join_impl(args: &[&str]) -> String {
249    if args.len() == 0 {
250        ".".to_owned()
251    } else {
252        // let length =
253        let joined = args
254            .iter()
255            .filter_map(|&arg| {
256                if arg.is_empty() {
257                    None
258                } else {
259                    Some(Cow::Borrowed(arg))
260                }
261            })
262            .reduce(|mut pre, cur| {
263                pre = pre.add("/");
264                pre = pre.add(cur);
265                pre
266            });
267        match joined {
268            Some(joined) => normalize(&joined),
269            None => ".".to_string(),
270        }
271    }
272}
273
274/// The path.normalize() method normalizes the given path, resolving '..' and '.' segments.
275///
276/// When multiple, sequential path segment separation characters are found (e.g. / on POSIX and either \ or / on Windows), they are replaced by a single instance of the platform-specific path segment /// separator (/ on POSIX and \ on Windows). Trailing separators are preserved.
277///
278/// If the path is a zero-length string, '.' is returned, representing the current working directory.
279///
280/// ```rust
281/// assert_eq!(nodejs_path::posix::normalize("/foo/bar//baz/asdf/quux/.."), "/foo/bar/baz/asdf");
282/// ```
283pub fn normalize(path: &str) -> String {
284    if path.len() == 0 {
285        return ".".to_owned();
286    } else {
287        let is_absolute = is_absolute(path);
288        let trailing_separator = path
289            .chars()
290            .last()
291            .map(|c| c == CHAR_FORWARD_SLASH)
292            .unwrap_or(false);
293        let mut consecutive_dd = 0;
294        // let mut path = normalize_string(path, !is_absolute, &'/', &is_posix_path_separator);
295        let mut path_stack = vec![];
296        path.split("/")
297            .filter(|seg| !seg.is_empty())
298            .for_each(|seg| {
299                match seg {
300                    "." => {}
301                    ".." => {
302                        // path_stack.pop();
303                        if consecutive_dd == path_stack.len() {
304                            path_stack.push(seg);
305                            consecutive_dd += 1;
306                        } else {
307                            path_stack.pop();
308                        }
309                    }
310                    other => {
311                        path_stack.push(other);
312                    }
313                }
314            });
315        let mut normalized_path = if is_absolute {
316            // if is absolute path, whatever how many times .. used, it is just the same as /
317            path_stack
318                .iter()
319                .position(|&str| str != "..")
320                .map(|item| path_stack[item..].join("/"))
321                .unwrap_or("".to_string())
322        } else {
323            path_stack.join("/")
324        };
325        if is_absolute {
326            normalized_path = "/".to_string() + &normalized_path;
327        }
328
329        if normalized_path.is_empty() {
330            normalized_path.push('.');
331        }
332
333        if trailing_separator && normalized_path != "/" {
334            normalized_path.push('/');
335        }
336
337        normalized_path
338    }
339}
340
341/// # Example
342/// ```rust
343/// assert_eq!(nodejs_path::parse("/home/user/dir/file.txt"), nodejs_path::Parsed{
344///   root: "/".to_string(),
345///   dir: "/home/user/dir".to_string(),
346///   base: "file.txt".to_string(),
347///   ext: ".txt".to_string(),
348///   name: "file".to_string(),
349/// })
350/// ```
351///
352/// ```plain
353/// ┌─────────────────────┬────────────┐
354/// │          dir        │    base    │
355/// ├──────┬              ├──────┬─────┤
356/// │ root │              │ name │ ext │
357/// "  /    home/user/dir / file  .txt "
358/// └──────┴──────────────┴──────┴─────┘
359/// (All spaces in the "" line should be ignored. They are purely for formatting.)
360/// ```
361pub fn parse(path: &str) -> Parsed {
362    let path = path.chars().collect::<Vec<char>>();
363    let mut ret = Parsed::default();
364    if path.len() == 0 {
365        ret
366    } else {
367        let is_absolute = path.get(0).map(|c| c == &CHAR_FORWARD_SLASH).unwrap();
368
369        let start;
370        if is_absolute {
371            ret.root = "/".to_owned();
372            start = 1;
373        } else {
374            start = 0;
375        }
376        let mut start_dot = -1;
377        let mut start_part = 0;
378        let mut end = -1;
379        let mut matched_slash = true;
380        let mut i = (path.len() - 1) as i32;
381
382        // Track the state of characters (if any) we see before our first dot and
383        // after any path separator we find
384        let mut pre_dot_state = 0;
385
386        // Get non-dir info
387        while i >= start {
388            let code = *path.get(i as usize).unwrap();
389            if code == CHAR_FORWARD_SLASH {
390                // If we reached a path separator that was not part of a set of path
391                // separators at the end of the string, stop now
392                if !matched_slash {
393                    start_part = i + 1;
394                    // i -= 1;
395                    break;
396                }
397                i -= 1;
398                continue;
399            }
400            if end == -1 {
401                // We saw the first non-path separator, mark this as the end of our
402                // extension
403                matched_slash = false;
404                end = i + 1;
405            }
406            if code == CHAR_DOT {
407                // If this is our first dot, mark it as the start of our extension
408                if start_dot == -1 {
409                    start_dot = i;
410                } else if pre_dot_state != 1 {
411                    pre_dot_state = 1;
412                }
413            } else if start_dot != -1 {
414                // We saw a non-dot and non-path separator before our dot, so we should
415                // have a good chance at having a non-empty extension
416                pre_dot_state = -1;
417            }
418
419            i -= 1;
420        }
421
422        if end != -1 {
423            let start = if start_part == 0 && is_absolute {
424                1
425            } else {
426                start_part
427            };
428            if start_dot == -1 ||
429                // We saw a non-dot character immediately before the dot
430                pre_dot_state == 0 ||
431                // The (right-most) trimmed path component is exactly '..'
432                (pre_dot_state == 1 &&
433                start_dot == end - 1 &&
434                start_dot == start_part + 1)
435            {
436                ret.base = path[start as usize..end as usize].iter().collect();
437                ret.name = ret.base.clone();
438            } else {
439                ret.name = path[start as usize..start_dot as usize].iter().collect();
440                ret.base = path[start as usize..end as usize].iter().collect();
441                ret.ext = path[start_dot as usize..end as usize].iter().collect();
442            }
443        }
444
445        if start_part > 0 {
446            ret.dir = path[0..(start_part - 1) as usize].iter().collect();
447        } else if is_absolute {
448            ret.dir = "/".to_owned();
449        }
450
451        ret
452    }
453}
454
455///
456/// method returns the relative path from from to to based on the current working directory. If from and to each resolve to the same path (after calling resolve() on each), a zero-length string is returned.
457/// ```rust
458/// assert_eq!(nodejs_path::posix::relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb"), "../../impl/bbb");
459/// ```
460pub fn relative(from: &str, to: &str) -> String {
461    if from == to {
462        "".to_owned()
463    } else {
464        let from = resolve!(&from).chars().collect::<Vec<char>>();
465        let to = resolve!(&to).chars().collect::<Vec<char>>();
466
467        if from == to {
468            "".to_owned()
469        } else {
470            let from_start = 1;
471            let from_end = from.len() as i32;
472            let from_len = from_end - from_start;
473            let to_start = 1;
474            let to_len = to.len() as i32 - to_start;
475
476            // Compare paths to find the longest common path from root
477            let length = if from_len < to_len { from_len } else { to_len };
478
479            let mut last_common_sep = -1;
480            let mut i = 0;
481
482            while i < length {
483                let from_code = from.get((from_start + i) as usize).unwrap();
484                if from_code != to.get((to_start + i) as usize).unwrap() {
485                    break;
486                } else if from_code == &CHAR_FORWARD_SLASH {
487                    last_common_sep = i;
488                }
489                i += 1;
490            }
491
492            if i == length {
493                if to_len > length {
494                    if to.get((to_start + i) as usize).unwrap() == &CHAR_FORWARD_SLASH {
495                        // We get here if `from` is the exact base path for `to`.
496                        // For example: from='/foo/bar'; to='/foo/bar/baz'
497                        return to[(to_start + i + 1) as usize..to.len()].iter().collect();
498                        // return StringPrototypeSlice(to, toStart + i + 1);
499                    }
500                    if i == 0 {
501                        // We get here if `from` is the root
502                        // For example: from='/'; to='/foo'
503                        return to[(to_start + i) as usize..to.len()].iter().collect();
504                    }
505                } else if from_len > length {
506                    if from.get((from_start + i) as usize).unwrap() == &CHAR_FORWARD_SLASH {
507                        // We get here if `to` is the exact base path for `from`.
508                        // For example: from='/foo/bar/baz'; to='/foo/bar'
509                        last_common_sep = i;
510                    } else if i == 0 {
511                        // We get here if `to` is the root.
512                        // For example: from='/foo/bar'; to='/'
513                        last_common_sep = 0;
514                    }
515                }
516            }
517
518            let mut out = "".to_owned();
519            // Generate the relative path based on the path difference between `to`
520            // and `from`.
521            let mut i = from_start + last_common_sep + 1;
522            while i <= from_end {
523                if i == from_end || from.get(i as usize).unwrap() == &CHAR_FORWARD_SLASH {
524                    if out.len() == 0 {
525                        out.push_str("..")
526                    } else {
527                        out.push_str("/..")
528                    }
529                    // out += out.length === 0 ? '..' : '/..';
530                }
531                i += 1;
532            }
533
534            // Lastly, append the rest of the destination (`to`) path that comes after
535            // the common path parts.
536            format!(
537                "{}{}",
538                &out,
539                &to[(to_start + last_common_sep) as usize..to.len()]
540                    .iter()
541                    .collect::<String>()
542            )
543            // return `${out}${StringPrototypeSlice(to, toStart + lastCommonSep)}`;
544        }
545    }
546}
547
548pub fn resolve_impl(args: &[&str]) -> String {
549    let mut resolved_path = "".to_owned();
550    let mut resolved_absolute = false;
551
552    let mut i = args.len() as i32 - 1;
553
554    while i >= -1 && !resolved_absolute {
555        let path = if i >= 0 {
556            args.get(i.clone() as usize).unwrap().to_string()
557        } else {
558            cwd().to_owned()
559        };
560
561        // Skip empty entries
562        if path.len() == 0 {
563            i -= 1;
564            continue;
565        }
566
567        resolved_path = format!("{}/{}", path, resolved_path);
568        resolved_absolute = path
569            .chars()
570            .next()
571            .map(|c| c == CHAR_FORWARD_SLASH)
572            .unwrap_or(false);
573
574        i -= 1;
575    }
576
577    // At this point the path should be resolved to a full absolute path, but
578    // handle relative paths to be safe (might happen when process.cwd() fails)
579
580    // Normalize the path
581    resolved_path = normalize_string(
582        &resolved_path,
583        !resolved_absolute,
584        &sep,
585        &is_posix_path_separator,
586    );
587
588    if resolved_absolute {
589        "/".to_owned() + &resolved_path
590    } else {
591        if !resolved_path.is_empty() {
592            resolved_path
593        } else {
594            ".".to_owned()
595        }
596    }
597}
598
599/// Resolves a sequence of paths or path segments into an absolute path.
600///
601/// ```rust
602/// assert_eq!(&nodejs_path::resolve!("/foo/bar", "./baz"), "/foo/bar/baz");
603///
604/// assert_eq!(&nodejs_path::resolve!("/foo/bar", "/tmp/file/"), "/tmp/file");
605///
606/// assert_eq!(&nodejs_path::resolve!("/home/myself/node", "wwwroot", "static_files/png/", "../gif/image.gif"), "/home/myself/node/wwwroot/static_files/gif/image.gif");
607///
608/// assert_eq!(nodejs_path::resolve!("."), std::env::current_dir().unwrap().to_str().unwrap().to_owned());
609///
610/// assert_eq!(nodejs_path::resolve!(), std::env::current_dir().unwrap().to_str().unwrap().to_owned());
611/// ```
612#[macro_export]
613macro_rules! resolve {
614    ( $( $x:expr ),* ) => {
615      {
616        $crate::posix::resolve_impl(&[
617          $(
618            $x,
619          )*
620        ])
621      }
622    };
623  }
624pub use resolve;
625
626pub fn to_namespaced_path() {}
627
628use once_cell::sync::Lazy;
629
630pub(crate) static POSIX_CWD: Lazy<String> = Lazy::new(|| {
631    let mut cwd = std::env::current_dir()
632        .unwrap_or(PathBuf::from(""))
633        .to_string_lossy()
634        .to_string();
635    if cfg!(target_os = "windows") {
636        // Converts Windows' backslash path separators to POSIX forward slashes
637        // and truncates any drive indicator
638        // const regexp = /\\/g;
639        // return () => {
640        //   const cwd = StringPrototypeReplace(process.cwd(), regexp, '/');
641        //   return StringPrototypeSlice(cwd, StringPrototypeIndexOf(cwd, '/'));
642        // };
643        // FIXME:
644        cwd = cwd
645            .chars()
646            .map(|c| if c == '\\' { '/' } else { c })
647            .take_while(|c| c != &'/')
648            .collect()
649    };
650
651    // We're already on POSIX, no need for any transformations
652    cwd
653});
654
655/// Get current working directory. Just like `process.cwd()`
656pub fn cwd() -> &'static str {
657    &POSIX_CWD
658}