1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! Path Utilities
//!
//! Code adapted from the following libraries
//! * [path-absolutize](https://docs.rs/path-absolutize)
//! * [normalize_path](https://docs.rs/normalize-path)
use std::path::{Component, Path, PathBuf};
pub const SLASH_START: &[char; 2] = &['/', '\\'];
/// Extension trait to add path normalization to std's [`Path`].
pub trait PathUtil {
/// Normalize this path without performing I/O.
///
/// All redundant separator and up-level references are collapsed.
///
/// However, this does not resolve links.
fn normalize(&self) -> PathBuf;
/// Normalize with subpath assuming this path is normalized without performing I/O.
///
/// All redundant separator and up-level references are collapsed.
///
/// However, this does not resolve links.
fn normalize_with<P: AsRef<Path>>(&self, subpath: P) -> PathBuf;
/// Defined in ESM PACKAGE_TARGET_RESOLVE
/// If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants
fn is_invalid_exports_target(&self) -> bool;
}
impl PathUtil for Path {
// https://github.com/parcel-bundler/parcel/blob/e0b99c2a42e9109a9ecbd6f537844a1b33e7faf5/packages/utils/node-resolver-rs/src/path.rs#L7
fn normalize(&self) -> PathBuf {
let mut components = self.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek() {
let buf = PathBuf::from(c.as_os_str());
components.next();
buf
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!("Path {:?}", self),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
// https://github.com/parcel-bundler/parcel/blob/e0b99c2a42e9109a9ecbd6f537844a1b33e7faf5/packages/utils/node-resolver-rs/src/path.rs#L37
fn normalize_with<B: AsRef<Self>>(&self, subpath: B) -> PathBuf {
let subpath = subpath.as_ref();
let mut components = subpath.components();
let Some(head) = components.next() else { return subpath.to_path_buf() };
if matches!(head, Component::Prefix(..) | Component::RootDir) {
return subpath.to_path_buf();
}
let mut ret = self.to_path_buf();
for component in std::iter::once(head).chain(components) {
match component {
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
Component::Prefix(..) | Component::RootDir => {
unreachable!("Path {:?} Subpath {:?}", self, subpath)
}
}
}
ret
}
fn is_invalid_exports_target(&self) -> bool {
self.components().enumerate().any(|(index, c)| match c {
Component::ParentDir => true,
Component::CurDir => index > 0,
Component::Normal(c) => c.eq_ignore_ascii_case("node_modules"),
_ => false,
})
}
}
// https://github.com/webpack/enhanced-resolve/blob/main/test/path.test.js
#[test]
fn is_invalid_exports_target() {
let test_cases = [
"../a.js",
"../",
"./a/b/../../../c.js",
"./a/b/../../../",
"./../../c.js",
"./../../",
"./a/../b/../../c.js",
"./a/../b/../../",
"./././../",
];
for case in test_cases {
assert!(Path::new(case).is_invalid_exports_target(), "{case}");
}
assert!(!Path::new("C:").is_invalid_exports_target());
assert!(!Path::new("/").is_invalid_exports_target());
}
#[test]
fn normalize() {
assert_eq!(Path::new("/foo/.././foo/").normalize(), Path::new("/foo"));
assert_eq!(Path::new("C://").normalize(), Path::new("C://"));
assert_eq!(Path::new("C:").normalize(), Path::new("C:"));
assert_eq!(Path::new(r"\\server\share").normalize(), Path::new(r"\\server\share"));
}