1use std::sync::Arc;
17
18use rquickjs::function::{Func, Opt, Rest};
19use rquickjs::{Ctx, JsLifetime, Object};
20
21#[derive(Clone)]
30pub struct Cwd(pub Arc<str>);
31
32#[allow(unsafe_code)]
35unsafe impl JsLifetime<'_> for Cwd {
36 type Changed<'to> = Cwd;
37}
38
39pub fn set_cwd(ctx: &Ctx<'_>, cwd: &str) {
41 let _ = ctx.store_userdata(Cwd(Arc::from(cwd)));
42}
43
44fn cwd(ctx: &Ctx<'_>) -> Arc<str> {
48 if let Some(c) = ctx.userdata::<Cwd>() {
49 return Arc::clone(&c.0);
50 }
51 let from_js: rquickjs::Result<String> = (|| {
52 let process: Object<'_> = ctx.globals().get("process")?;
53 let cwd_fn: rquickjs::Function<'_> = process.get("cwd")?;
54 cwd_fn.call(())
55 })();
56 Arc::from(from_js.unwrap_or_else(|_| "/".to_string()))
57}
58
59fn normalize_into<'a>(parts: impl Iterator<Item = &'a str>, absolute: bool, trailing: bool, out: &mut String) {
66 let root = out.len();
67 if absolute {
68 out.push('/');
69 }
70 let body = out.len();
71 let mut starts: Vec<usize> = Vec::new();
74 for seg in parts {
75 match seg {
76 "" | "." => {},
77 ".." => {
78 if let Some(start) = starts.pop() {
79 out.truncate(start);
80 } else if !absolute {
81 if out.len() > body {
84 out.push('/');
85 }
86 out.push_str("..");
87 }
88 },
89 s => {
90 let start = out.len();
91 if out.len() > body {
92 out.push('/');
93 }
94 starts.push(start);
95 out.push_str(s);
96 },
97 }
98 }
99 if out.len() == body {
100 if !absolute {
101 out.truncate(root);
102 out.push('.');
103 }
104 return;
105 }
106 if trailing {
107 out.push('/');
108 }
109}
110
111fn normalize_str(path: &str) -> String {
112 let mut out = String::with_capacity(path.len() + 1);
113 normalize_into(
114 path.split('/'),
115 path.starts_with('/'),
116 path.len() > 1 && path.ends_with('/'),
117 &mut out,
118 );
119 out
120}
121
122fn join_segments(segments: &[String]) -> String {
123 let Some(first) = segments.iter().find(|s| !s.is_empty()) else {
124 return ".".to_string();
125 };
126 let last = segments.iter().rev().find(|s| !s.is_empty()).unwrap_or(first);
127 let capacity = segments.iter().map(|s| s.len() + 1).sum::<usize>() + 1;
128 let mut out = String::with_capacity(capacity);
129 normalize_into(
130 segments.iter().filter(|s| !s.is_empty()).flat_map(|s| s.split('/')),
131 first.starts_with('/'),
132 last.len() > 1 && last.ends_with('/'),
133 &mut out,
134 );
135 out
136}
137
138fn dirname_str(path: &str) -> String {
139 let trimmed = path.trim_end_matches('/');
140 match trimmed.rfind('/') {
141 Some(0) => "/".to_string(),
142 Some(i) => trimmed[..i].to_string(),
143 None => {
144 if path.starts_with('/') {
145 "/".to_string()
146 } else {
147 ".".to_string()
148 }
149 },
150 }
151}
152
153fn basename_of(path: &str) -> &str {
154 let trimmed = path.trim_end_matches('/');
155 trimmed.rsplit('/').next().unwrap_or(trimmed)
156}
157
158fn basename_str(path: &str, ext: Option<&str>) -> String {
159 let base = basename_of(path);
160 match ext {
161 Some(e) if base.len() > e.len() && base.ends_with(e) => base[..base.len() - e.len()].to_string(),
162 _ => base.to_string(),
163 }
164}
165
166fn extname_of(base: &str) -> &str {
169 match base.rfind('.') {
170 Some(i) if i > 0 => &base[i..],
171 _ => "",
172 }
173}
174
175fn extname_str(path: &str) -> String {
176 extname_of(basename_of(path)).to_string()
177}
178
179fn resolve_segments(cwd: &str, segments: &[String]) -> String {
184 let mut parts: Vec<&str> = Vec::new();
185 let mut absolute = false;
186 for seg in segments.iter().rev() {
187 if seg.is_empty() {
188 continue;
189 }
190 parts.push(seg.as_str());
191 if seg.starts_with('/') {
192 absolute = true;
193 break;
194 }
195 }
196 if !absolute {
197 parts.push(cwd);
198 absolute = cwd.starts_with('/');
199 }
200 parts.reverse();
201 let capacity = parts.iter().map(|s| s.len() + 1).sum::<usize>() + 1;
202 let mut out = String::with_capacity(capacity);
203 normalize_into(parts.iter().flat_map(|s| s.split('/')), absolute, false, &mut out);
205 out
206}
207
208fn relative_str(from: &str, to: &str) -> String {
209 let f = normalize_str(from);
210 let t = normalize_str(to);
211 let fp: Vec<&str> = f.split('/').filter(|s| !s.is_empty()).collect();
212 let tp: Vec<&str> = t.split('/').filter(|s| !s.is_empty()).collect();
213 let common = fp.iter().zip(tp.iter()).take_while(|(a, b)| a == b).count();
214 let mut out: Vec<&str> = vec![".."; fp.len() - common];
215 out.extend(&tp[common..]);
216 out.join("/")
217}
218
219fn parse_fn<'js>(ctx: Ctx<'js>, path: String) -> rquickjs::Result<Object<'js>> {
222 parse_object(&ctx, &path)
223}
224
225fn parse_object<'js>(ctx: &Ctx<'js>, path: &str) -> rquickjs::Result<Object<'js>> {
228 let o = Object::new(ctx.clone())?;
229 let root = if path.starts_with('/') { "/" } else { "" };
230 let base = basename_of(path);
231 let ext = extname_of(base);
232 let name = &base[..base.len() - ext.len()];
233 let dir = {
234 let trimmed = path.trim_end_matches('/');
235 match trimmed.rfind('/') {
236 Some(0) => "/",
237 Some(i) => &trimmed[..i],
238 None => "",
239 }
240 };
241 o.set("root", root)?;
242 o.set("dir", dir)?;
243 o.set("base", base)?;
244 o.set("ext", ext)?;
245 o.set("name", name)?;
246 Ok(o)
247}
248
249fn format_str(root: &str, dir: &str, base: &str, name: &str, ext: &str) -> String {
252 let base = if base.is_empty() {
253 let mut b = String::with_capacity(name.len() + ext.len() + 1);
254 b.push_str(name);
255 if !ext.is_empty() && !ext.starts_with('.') {
256 b.push('.');
257 }
258 b.push_str(ext);
259 b
260 } else {
261 base.to_string()
262 };
263 if dir.is_empty() {
264 return format!("{root}{base}");
265 }
266 if dir == "/" {
267 return format!("/{base}");
268 }
269 format!("{dir}/{base}")
270}
271
272pub fn path_object<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
275 let o = Object::new(ctx.clone())?;
276 o.set("sep", "/")?;
277 o.set("delimiter", ":")?;
278 o.set("join", Func::from(|segs: Rest<String>| join_segments(&segs.0)))?;
279 o.set(
280 "resolve",
281 Func::from(|ctx: Ctx<'_>, segs: Rest<String>| -> String { resolve_segments(&cwd(&ctx), &segs.0) }),
282 )?;
283 o.set("normalize", Func::from(|p: String| normalize_str(&p)))?;
284 o.set("dirname", Func::from(|p: String| dirname_str(&p)))?;
285 o.set(
286 "basename",
287 Func::from(|p: String, ext: Opt<String>| basename_str(&p, ext.0.as_deref())),
288 )?;
289 o.set("extname", Func::from(|p: String| extname_str(&p)))?;
290 o.set(
291 "relative",
292 Func::from(|ctx: Ctx<'_>, from: String, to: String| -> String {
293 let cwd = cwd(&ctx);
294 relative_str(&resolve_segments(&cwd, &[from]), &resolve_segments(&cwd, &[to]))
295 }),
296 )?;
297 o.set("isAbsolute", Func::from(|p: String| p.starts_with('/')))?;
298 o.set("parse", Func::from(parse_fn))?;
299 o.set(
300 "format",
301 Func::from(|bag: Object<'_>| -> rquickjs::Result<String> {
302 let field = |k: &str| -> String { bag.get::<_, Option<String>>(k).ok().flatten().unwrap_or_default() };
303 Ok(format_str(
304 &field("root"),
305 &field("dir"),
306 &field("base"),
307 &field("name"),
308 &field("ext"),
309 ))
310 }),
311 )?;
312 o.set("toNamespacedPath", Func::from(|p: String| p))?;
315 o.set("posix", o.clone())?;
318 Ok(o)
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn normalize_matches_node() {
327 for (input, want) in [
328 ("/a/b/../c", "/a/c"),
329 ("/foo/bar//baz/asdf/quux/..", "/foo/bar/baz/asdf"),
330 ("a/b/..", "a"),
331 ("a/..", "."),
332 ("", "."),
333 ("/", "/"),
334 ("/..", "/"),
335 ("../..", "../.."),
336 ("./a/", "a/"),
337 ("/a/b/", "/a/b/"),
338 ("//a", "/a"),
339 ("../a/../b", "../b"),
340 ] {
341 assert_eq!(normalize_str(input), want, "normalize({input:?})");
342 }
343 }
344
345 #[test]
346 fn join_matches_node() {
347 let j = |parts: &[&str]| join_segments(&parts.iter().map(|s| (*s).to_string()).collect::<Vec<_>>());
348 assert_eq!(j(&["/foo", "bar", "baz/asdf", "quux", ".."]), "/foo/bar/baz/asdf");
349 assert_eq!(j(&["a", "", "b"]), "a/b");
350 assert_eq!(j(&[]), ".");
351 assert_eq!(j(&["", ""]), ".");
352 assert_eq!(j(&["a/", "b"]), "a/b");
353 assert_eq!(j(&["/"]), "/");
354 assert_eq!(j(&["a", "b/"]), "a/b/");
355 }
356
357 #[test]
358 fn resolve_matches_node() {
359 let r = |parts: &[&str]| resolve_segments("/base", &parts.iter().map(|s| (*s).to_string()).collect::<Vec<_>>());
360 assert_eq!(r(&["/foo/bar", "./baz"]), "/foo/bar/baz");
361 assert_eq!(r(&["/foo/bar", "/tmp/file/"]), "/tmp/file");
362 assert_eq!(r(&["a", "b"]), "/base/a/b");
363 assert_eq!(r(&[]), "/base");
364 assert_eq!(r(&["/"]), "/");
365 assert_eq!(r(&["..", ".."]), "/");
366 }
367
368 #[test]
369 fn parse_and_format_round_trip() {
370 assert_eq!(extname_of(basename_of("/home/user/file.txt")), ".txt");
371 assert_eq!(extname_of(basename_of("/home/.gitignore")), "");
372 assert_eq!(basename_of("/home/user/file.txt"), "file.txt");
373 assert_eq!(format_str("/", "/home/user", "file.txt", "", ""), "/home/user/file.txt");
374 assert_eq!(format_str("/", "", "", "file", ".txt"), "/file.txt");
375 assert_eq!(format_str("", "/", "index.js", "", ""), "/index.js");
376 }
377
378 #[test]
379 fn relative_matches_node() {
380 assert_eq!(relative_str("/a/b/c", "/a/b/c/d"), "d");
381 assert_eq!(relative_str("/a/b/c", "/a/x"), "../../x");
382 assert_eq!(relative_str("/a", "/a"), "");
383 }
384}