1use super::arg_str;
4use crate::host::with_host;
5use fusevm::Value;
6use indexmap::IndexMap;
7
8pub const METHODS: &[&str] = &[
9 "join",
10 "resolve",
11 "normalize",
12 "basename",
13 "dirname",
14 "extname",
15 "isAbsolute",
16 "relative",
17 "parse",
18 "format",
19 "matchesGlob",
20 "toNamespacedPath",
21];
22
23pub fn constant(name: &str) -> Option<Value> {
25 match name {
26 "sep" => Some(with_host(|h| h.new_str("/"))),
27 "delimiter" => Some(with_host(|h| h.new_str(":"))),
28 _ => None,
29 }
30}
31
32pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
33 let parts: Vec<String> = (0..args.len()).map(|i| arg_str(args, i)).collect();
34 let s = |v: String| Ok(with_host(|h| h.new_str(v)));
35 Some(match method {
36 "join" => s(join(&parts)),
37 "resolve" => s(resolve(&parts)),
38 "normalize" => s(normalize(&first(&parts))),
39 "basename" => s(basename(&first(&parts), parts.get(1).map(|x| x.as_str()))),
40 "dirname" => s(dirname(&first(&parts))),
41 "extname" => s(extname(&first(&parts))),
42 "isAbsolute" => Ok(Value::Bool(first(&parts).starts_with('/'))),
43 "relative" => s(relative(
44 &first(&parts),
45 parts.get(1).cloned().unwrap_or_default().as_str(),
46 )),
47 "parse" => Ok(parse(&first(&parts))),
48 "format" => s(format(args.first())),
49 "matchesGlob" => Ok(Value::Bool(matches_glob(
50 &first(&parts),
51 parts.get(1).map(|x| x.as_str()).unwrap_or(""),
52 ))),
53 "toNamespacedPath" => s(first(&parts)),
55 _ => return None,
56 })
57}
58
59fn first(parts: &[String]) -> String {
60 parts.first().cloned().unwrap_or_default()
61}
62
63fn join(parts: &[String]) -> String {
64 let joined: Vec<&str> = parts
65 .iter()
66 .map(|s| s.as_str())
67 .filter(|s| !s.is_empty())
68 .collect();
69 if joined.is_empty() {
70 return ".".into();
71 }
72 normalize(&joined.join("/"))
73}
74
75fn normalize(p: &str) -> String {
78 if p.is_empty() {
79 return ".".into();
80 }
81 let is_abs = p.starts_with('/');
82 let trailing = p.ends_with('/') && p.len() > 1;
83 let mut out: Vec<&str> = Vec::new();
84 for seg in p.split('/') {
85 match seg {
86 "" | "." => {}
87 ".." => {
88 if let Some(&last) = out.last() {
89 if last != ".." {
90 out.pop();
91 continue;
92 }
93 }
94 if !is_abs {
95 out.push("..");
96 }
97 }
98 s => out.push(s),
99 }
100 }
101 let mut body = out.join("/");
102 if body.is_empty() {
103 return if is_abs { "/".into() } else { ".".into() };
104 }
105 if is_abs {
106 body.insert(0, '/');
107 }
108 if trailing {
109 body.push('/');
110 }
111 body
112}
113
114fn basename(p: &str, ext: Option<&str>) -> String {
115 let base = p
116 .trim_end_matches('/')
117 .rsplit('/')
118 .next()
119 .unwrap_or("")
120 .to_string();
121 match ext {
122 Some(e) if base.ends_with(e) && base != *e => base[..base.len() - e.len()].to_string(),
123 _ => base,
124 }
125}
126
127fn dirname(p: &str) -> String {
128 let trimmed = p.trim_end_matches('/');
129 match trimmed.rfind('/') {
130 Some(0) => "/".into(),
131 Some(i) => trimmed[..i].to_string(),
132 None => ".".into(),
133 }
134}
135
136fn extname(p: &str) -> String {
137 let base = basename(p, None);
138 match base.rfind('.') {
139 Some(i) if i > 0 => base[i..].to_string(),
140 _ => String::new(),
141 }
142}
143
144fn resolve(parts: &[String]) -> String {
145 let mut resolved = String::new();
146 let mut is_abs = false;
147 for p in parts.iter().rev() {
148 if p.is_empty() {
149 continue;
150 }
151 resolved = if resolved.is_empty() {
152 p.clone()
153 } else {
154 format!("{p}/{resolved}")
155 };
156 if p.starts_with('/') {
157 is_abs = true;
158 break;
159 }
160 }
161 if !is_abs {
162 let cwd = std::env::current_dir()
163 .map(|d| d.to_string_lossy().into_owned())
164 .unwrap_or_else(|_| "/".into());
165 resolved = if resolved.is_empty() {
166 cwd
167 } else {
168 format!("{cwd}/{resolved}")
169 };
170 }
171 let n = normalize(&resolved);
172 if n.len() > 1 {
174 n.trim_end_matches('/').to_string()
175 } else {
176 n
177 }
178}
179
180fn relative(from: &str, to: &str) -> String {
181 let from = resolve(&[from.to_string()]);
182 let to = resolve(&[to.to_string()]);
183 let fs: Vec<&str> = from.split('/').filter(|s| !s.is_empty()).collect();
184 let ts: Vec<&str> = to.split('/').filter(|s| !s.is_empty()).collect();
185 let common = fs.iter().zip(ts.iter()).take_while(|(a, b)| a == b).count();
186 let mut out: Vec<String> = vec!["..".into(); fs.len() - common];
187 out.extend(ts[common..].iter().map(|s| s.to_string()));
188 if out.is_empty() {
189 String::new()
190 } else {
191 out.join("/")
192 }
193}
194
195fn parse(p: &str) -> Value {
196 let root = if p.starts_with('/') { "/" } else { "" };
197 let dir = dirname(p);
198 let base = basename(p, None);
199 let ext = extname(p);
200 let name = base.strip_suffix(&ext).unwrap_or(&base).to_string();
201 with_host(|h| {
202 let mut m = IndexMap::new();
203 m.insert("root".into(), h.new_str(root));
204 m.insert("dir".into(), h.new_str(dir));
205 m.insert("base".into(), h.new_str(base));
206 m.insert("ext".into(), h.new_str(ext));
207 m.insert("name".into(), h.new_str(name));
208 h.new_object(m)
209 })
210}
211
212fn format(obj: Option<&Value>) -> String {
213 let Some(obj) = obj else { return String::new() };
214 let get = |k: &str| {
215 with_host(|h| match h.get(obj) {
216 Some(crate::host::JsObj::Object(p)) => {
217 p.get(k).map(|v| h.str_of(v)).unwrap_or_default()
218 }
219 _ => String::new(),
220 })
221 };
222 let dir = get("dir");
223 let root = get("root");
224 let base = if !get("base").is_empty() {
225 get("base")
226 } else {
227 format!("{}{}", get("name"), get("ext"))
228 };
229 let d = if !dir.is_empty() { dir } else { root };
230 if d.is_empty() {
231 base
232 } else if d.ends_with('/') {
233 format!("{d}{base}")
234 } else {
235 format!("{d}/{base}")
236 }
237}
238
239fn matches_glob(path: &str, pattern: &str) -> bool {
243 let text: Vec<char> = path.chars().collect();
244 expand_braces(pattern)
245 .iter()
246 .any(|pat| glob_match(&text, &pat.chars().collect::<Vec<char>>()))
247}
248
249fn expand_braces(pattern: &str) -> Vec<String> {
251 let chars: Vec<char> = pattern.chars().collect();
252 for (i, &c) in chars.iter().enumerate() {
253 if c != '{' {
254 continue;
255 }
256 let mut depth = 1;
257 let mut commas: Vec<usize> = Vec::new();
258 let mut close = None;
259 for (j, &cj) in chars.iter().enumerate().skip(i + 1) {
260 match cj {
261 '{' => depth += 1,
262 '}' => {
263 depth -= 1;
264 if depth == 0 {
265 close = Some(j);
266 break;
267 }
268 }
269 ',' if depth == 1 => commas.push(j),
270 _ => {}
271 }
272 }
273 let (Some(close), false) = (close, commas.is_empty()) else {
274 continue;
275 };
276 let prefix: String = chars[..i].iter().collect();
277 let suffix: String = chars[close + 1..].iter().collect();
278 let mut bounds = vec![i];
279 bounds.extend(&commas);
280 bounds.push(close);
281 let mut out = Vec::new();
282 for w in bounds.windows(2) {
283 let alt: String = chars[w[0] + 1..w[1]].iter().collect();
284 out.extend(expand_braces(&format!("{prefix}{alt}{suffix}")));
285 }
286 return out;
287 }
288 vec![pattern.to_string()]
289}
290
291fn glob_match(t: &[char], p: &[char]) -> bool {
293 if p.is_empty() {
294 return t.is_empty();
295 }
296 match p[0] {
297 '*' => {
298 let double = p.len() >= 2 && p[1] == '*';
299 let rest = {
300 let mut k = 0;
301 while k < p.len() && p[k] == '*' {
302 k += 1;
303 }
304 &p[k..]
305 };
306 if rest.is_empty() {
307 return double || !t.contains(&'/');
308 }
309 let mut ti = 0;
310 loop {
311 if glob_match(&t[ti..], rest) {
312 return true;
313 }
314 if ti >= t.len() {
315 return false;
316 }
317 if !double && t[ti] == '/' {
318 return false;
319 }
320 ti += 1;
321 }
322 }
323 '?' => !t.is_empty() && t[0] != '/' && glob_match(&t[1..], &p[1..]),
324 '[' => match match_class(t.first().copied(), p) {
325 Some((matched, plen)) => matched && glob_match(&t[1..], &p[plen..]),
326 None => !t.is_empty() && t[0] == '[' && glob_match(&t[1..], &p[1..]),
328 },
329 c => !t.is_empty() && t[0] == c && glob_match(&t[1..], &p[1..]),
330 }
331}
332
333fn match_class(ch: Option<char>, p: &[char]) -> Option<(bool, usize)> {
336 let mut i = 1;
337 let mut negate = false;
338 if matches!(p.get(i), Some('!') | Some('^')) {
339 negate = true;
340 i += 1;
341 }
342 let start = i;
343 let mut matched = false;
344 while i < p.len() && (p[i] != ']' || i == start) {
345 if i + 2 < p.len() && p[i + 1] == '-' && p[i + 2] != ']' {
346 if let Some(c) = ch {
347 if p[i] <= c && c <= p[i + 2] {
348 matched = true;
349 }
350 }
351 i += 3;
352 } else {
353 if ch == Some(p[i]) {
354 matched = true;
355 }
356 i += 1;
357 }
358 }
359 if i >= p.len() {
360 return None;
361 }
362 let ok = matches!(ch, Some(c) if c != '/') && (matched ^ negate);
364 Some((ok, i + 1))
365}