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}