1use super::arg_str;
17use crate::host::with_host;
18use fusevm::Value;
19use indexmap::IndexMap;
20
21pub const METHODS: &[&str] = &[
22 "join",
23 "resolve",
24 "normalize",
25 "basename",
26 "dirname",
27 "extname",
28 "isAbsolute",
29 "relative",
30 "parse",
31 "format",
32 "matchesGlob",
33 "toNamespacedPath",
34 "_makeLong",
36];
37
38#[derive(Clone, Copy, PartialEq, Eq)]
40pub enum Flavor {
41 Posix,
42 Win32,
43}
44
45impl Flavor {
46 fn is_sep(self, c: char) -> bool {
48 c == '/' || (self == Flavor::Win32 && c == '\\')
49 }
50
51 fn sep(self) -> char {
52 match self {
53 Flavor::Posix => '/',
54 Flavor::Win32 => '\\',
55 }
56 }
57
58 fn delimiter(self) -> &'static str {
59 match self {
60 Flavor::Posix => ":",
61 Flavor::Win32 => ";",
62 }
63 }
64}
65
66pub fn constant(flavor: Flavor, name: &str) -> Option<Value> {
68 match name {
69 "sep" => Some(with_host(|h| h.new_str(flavor.sep().to_string()))),
70 "delimiter" => Some(with_host(|h| h.new_str(flavor.delimiter()))),
71 _ => None,
72 }
73}
74
75pub fn call(flavor: Flavor, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
76 let parts: Vec<String> = (0..args.len()).map(|i| arg_str(args, i)).collect();
77 let s = |v: String| Ok(with_host(|h| h.new_str(v)));
78 let one = |i: usize| chars(parts.get(i).map(String::as_str).unwrap_or(""));
79 Some(match method {
80 "join" => s(join(flavor, &parts)),
81 "resolve" => s(resolve(flavor, &parts)),
82 "normalize" => s(normalize(flavor, &one(0))),
83 "basename" => s(basename(
84 flavor,
85 &one(0),
86 parts.get(1).map(|x| chars(x)).as_deref(),
89 )),
90 "dirname" => s(dirname(flavor, &one(0))),
91 "extname" => s(extname(flavor, &one(0))),
92 "isAbsolute" => Ok(Value::Bool(is_absolute(flavor, &one(0)))),
93 "relative" => s(relative(flavor, &one(0), &one(1))),
94 "parse" => Ok(parse(flavor, &one(0))),
95 "format" => s(format(flavor, args.first())),
96 "matchesGlob" => Ok(Value::Bool(matches_glob(
97 flavor,
98 parts.first().map(String::as_str).unwrap_or(""),
99 parts.get(1).map(|x| x.as_str()).unwrap_or(""),
100 ))),
101 "toNamespacedPath" | "_makeLong" => s(to_namespaced_path(
102 flavor,
103 parts.first().map(String::as_str).unwrap_or(""),
104 )),
105 _ => return None,
106 })
107}
108
109fn chars(s: &str) -> Vec<char> {
110 s.chars().collect()
111}
112
113fn str_of(c: &[char]) -> String {
114 c.iter().collect()
115}
116
117fn is_device_root(c: char) -> bool {
119 c.is_ascii_alphabetic()
120}
121
122const WINDOWS_RESERVED_NAMES: &[&str] = &[
124 "CON",
125 "PRN",
126 "AUX",
127 "NUL",
128 "COM1",
129 "COM2",
130 "COM3",
131 "COM4",
132 "COM5",
133 "COM6",
134 "COM7",
135 "COM8",
136 "COM9",
137 "LPT1",
138 "LPT2",
139 "LPT3",
140 "LPT4",
141 "LPT5",
142 "LPT6",
143 "LPT7",
144 "LPT8",
145 "LPT9",
146 "COM\u{b9}",
147 "COM\u{b2}",
148 "COM\u{b3}",
149 "LPT\u{b9}",
150 "LPT\u{b2}",
151 "LPT\u{b3}",
152];
153
154fn is_reserved_name(p: &[char], colon_index: isize) -> bool {
160 let end = if colon_index < 0 {
161 (p.len() as isize + colon_index).max(0) as usize
162 } else {
163 (colon_index as usize).min(p.len())
164 };
165 let device: String = p[..end].iter().collect::<String>().to_uppercase();
166 WINDOWS_RESERVED_NAMES.contains(&device.as_str())
167}
168
169fn index_of(p: &[char], ch: char, from: usize) -> isize {
171 p.iter()
172 .skip(from)
173 .position(|&c| c == ch)
174 .map(|i| (i + from) as isize)
175 .unwrap_or(-1)
176}
177
178fn normalize_string(
181 path: &[char],
182 allow_above_root: bool,
183 flavor: Flavor,
184 separator: char,
185) -> String {
186 let mut res: Vec<char> = Vec::new();
187 let mut last_segment_length: isize = 0;
188 let mut last_slash: isize = -1;
189 let mut dots: isize = 0;
190 let mut code: char = '\0';
191 let len = path.len() as isize;
192 let mut i: isize = 0;
193 while i <= len {
194 if i < len {
195 code = path[i as usize];
196 } else if flavor.is_sep(code) {
197 break;
198 } else {
199 code = '/';
200 }
201
202 if flavor.is_sep(code) {
203 if last_slash == i - 1 || dots == 1 {
204 } else if dots == 2 {
206 let rl = res.len() as isize;
207 if rl < 2
208 || last_segment_length != 2
209 || res[res.len() - 1] != '.'
210 || res[res.len() - 2] != '.'
211 {
212 if rl > 2 {
213 let last_slash_index = rl - last_segment_length - 1;
214 if last_slash_index == -1 {
215 res.clear();
216 last_segment_length = 0;
217 } else {
218 res.truncate(last_slash_index as usize);
219 let li = res
220 .iter()
221 .rposition(|&c| c == separator)
222 .map(|p| p as isize)
223 .unwrap_or(-1);
224 last_segment_length = res.len() as isize - 1 - li;
225 }
226 last_slash = i;
227 dots = 0;
228 i += 1;
229 continue;
230 } else if rl != 0 {
231 res.clear();
232 last_segment_length = 0;
233 last_slash = i;
234 dots = 0;
235 i += 1;
236 continue;
237 }
238 }
239 if allow_above_root {
240 if !res.is_empty() {
241 res.push(separator);
242 }
243 res.push('.');
244 res.push('.');
245 last_segment_length = 2;
246 }
247 } else {
248 if !res.is_empty() {
249 res.push(separator);
250 }
251 res.extend_from_slice(&path[(last_slash + 1) as usize..i as usize]);
252 last_segment_length = i - last_slash - 1;
253 }
254 last_slash = i;
255 dots = 0;
256 } else if code == '.' && dots != -1 {
257 dots += 1;
258 } else {
259 dots = -1;
260 }
261 i += 1;
262 }
263 res.into_iter().collect()
264}
265
266fn cwd() -> String {
267 std::env::current_dir()
268 .map(|d| d.to_string_lossy().into_owned())
269 .unwrap_or_else(|_| "/".into())
270}
271
272pub(crate) fn resolve_one(p: &str) -> String {
281 resolve_posix(&[p.to_string()])
282}
283
284fn resolve(flavor: Flavor, args: &[String]) -> String {
285 match flavor {
286 Flavor::Posix => resolve_posix(args),
287 Flavor::Win32 => resolve_win32(args),
288 }
289}
290
291fn resolve_posix(args: &[String]) -> String {
292 if args.is_empty() || (args.len() == 1 && (args[0].is_empty() || args[0] == ".")) {
293 let c = cwd();
294 if c.starts_with('/') {
295 return c;
296 }
297 }
298 let mut resolved = String::new();
299 let mut absolute = false;
300 for p in args.iter().rev() {
301 if absolute {
302 break;
303 }
304 if p.is_empty() {
305 continue;
306 }
307 resolved = std::format!("{p}/{resolved}");
308 absolute = p.starts_with('/');
309 }
310 if !absolute {
311 let c = cwd();
312 resolved = std::format!("{c}/{resolved}");
313 absolute = c.starts_with('/');
314 }
315 let out = normalize_string(&chars(&resolved), !absolute, Flavor::Posix, '/');
316 if absolute {
317 std::format!("/{out}")
318 } else if out.is_empty() {
319 ".".into()
320 } else {
321 out
322 }
323}
324
325fn resolve_win32(args: &[String]) -> String {
326 let f = Flavor::Win32;
327 let mut resolved_device = String::new();
328 let mut resolved_tail = String::new();
329 let mut resolved_absolute = false;
330
331 let mut i = args.len() as isize - 1;
332 while i >= -1 {
333 let path: Vec<char> = if i >= 0 {
334 let p = &args[i as usize];
335 if p.is_empty() {
336 i -= 1;
337 continue;
338 }
339 chars(p)
340 } else if resolved_device.is_empty() {
341 let c = cwd();
342 if args.is_empty()
345 || (args.len() == 1 && (args[0].is_empty() || args[0] == ".") && c.starts_with('/'))
346 {
347 return c.replace('/', "\\");
348 }
349 chars(&c)
350 } else {
351 let c = cwd();
358 let cc = chars(&c);
359 let drive_mismatch = str_of(&cc[..cc.len().min(2)]).to_lowercase()
360 != resolved_device.to_lowercase()
361 && cc.get(2) == Some(&'\\');
362 if drive_mismatch {
363 chars(&std::format!("{resolved_device}\\"))
364 } else {
365 cc
366 }
367 };
368
369 let len = path.len();
370 let mut root_end: usize = 0;
371 let mut device = String::new();
372 let mut is_absolute = false;
373 let code = *path.first().unwrap_or(&'\0');
374
375 if len == 1 {
376 if f.is_sep(code) {
377 root_end = 1;
378 is_absolute = true;
379 }
380 } else if f.is_sep(code) {
381 is_absolute = true;
382 if f.is_sep(path[1]) {
383 let mut j = 2usize;
384 let mut last = j;
385 while j < len && !f.is_sep(path[j]) {
386 j += 1;
387 }
388 if j < len && j != last {
389 let first_part = str_of(&path[last..j]);
390 last = j;
391 while j < len && f.is_sep(path[j]) {
392 j += 1;
393 }
394 if j < len && j != last {
395 last = j;
396 while j < len && !f.is_sep(path[j]) {
397 j += 1;
398 }
399 if j == len || j != last {
400 if first_part != "." && first_part != "?" {
401 device =
402 std::format!("\\\\{first_part}\\{}", str_of(&path[last..j]));
403 root_end = j;
404 } else {
405 device = std::format!("\\\\{first_part}");
406 root_end = 4;
407 }
408 }
409 }
410 }
411 } else {
412 root_end = 1;
413 }
414 } else if is_device_root(code) && path[1] == ':' {
415 device = str_of(&path[..2]);
416 root_end = 2;
417 if len > 2 && f.is_sep(path[2]) {
418 is_absolute = true;
419 root_end = 3;
420 }
421 }
422
423 if !device.is_empty() {
424 if !resolved_device.is_empty() {
425 if device.to_lowercase() != resolved_device.to_lowercase() {
426 i -= 1;
427 continue;
428 }
429 } else {
430 resolved_device = device;
431 }
432 }
433
434 if resolved_absolute {
435 if !resolved_device.is_empty() {
436 break;
437 }
438 } else {
439 resolved_tail = std::format!("{}\\{resolved_tail}", str_of(&path[root_end.min(len)..]));
440 resolved_absolute = is_absolute;
441 if is_absolute && !resolved_device.is_empty() {
442 break;
443 }
444 }
445 i -= 1;
446 }
447
448 let tail = normalize_string(&chars(&resolved_tail), !resolved_absolute, f, '\\');
449 if resolved_absolute {
450 std::format!("{resolved_device}{}{tail}", '\\')
451 } else {
452 let joined = std::format!("{resolved_device}{tail}");
453 if joined.is_empty() {
454 ".".into()
455 } else {
456 joined
457 }
458 }
459}
460
461fn normalize(flavor: Flavor, path: &[char]) -> String {
466 match flavor {
467 Flavor::Posix => normalize_posix(path),
468 Flavor::Win32 => normalize_win32(path),
469 }
470}
471
472fn normalize_posix(path: &[char]) -> String {
473 if path.is_empty() {
474 return ".".into();
475 }
476 let is_absolute = path[0] == '/';
477 let trailing = path[path.len() - 1] == '/';
478 let out = normalize_string(path, !is_absolute, Flavor::Posix, '/');
479 if out.is_empty() {
480 return if is_absolute {
481 "/".into()
482 } else if trailing {
483 "./".into()
484 } else {
485 ".".into()
486 };
487 }
488 let out = if trailing {
489 std::format!("{out}/")
490 } else {
491 out
492 };
493 if is_absolute {
494 std::format!("/{out}")
495 } else {
496 out
497 }
498}
499
500fn normalize_win32(path: &[char]) -> String {
501 let f = Flavor::Win32;
502 let len = path.len();
503 if len == 0 {
504 return ".".into();
505 }
506 let mut root_end: usize = 0;
507 let mut device: Option<String> = None;
508 let mut is_absolute = false;
509 let code = path[0];
510
511 if len == 1 {
512 return if code == '/' {
513 "\\".into()
514 } else {
515 str_of(path)
516 };
517 }
518 if f.is_sep(code) {
519 is_absolute = true;
520 if f.is_sep(path[1]) {
521 let mut j = 2usize;
522 let mut last = j;
523 while j < len && !f.is_sep(path[j]) {
524 j += 1;
525 }
526 if j < len && j != last {
527 let first_part = str_of(&path[last..j]);
528 last = j;
529 while j < len && f.is_sep(path[j]) {
530 j += 1;
531 }
532 if j < len && j != last {
533 last = j;
534 while j < len && !f.is_sep(path[j]) {
535 j += 1;
536 }
537 if j == len || j != last {
538 if first_part == "." || first_part == "?" {
539 device = Some(std::format!("\\\\{first_part}"));
540 root_end = 4;
541 let colon_index = index_of(path, ':', 0);
542 let end = (colon_index + 1).max(0) as usize;
543 let possible: Vec<char> = if end >= 4 && end <= len {
544 path[4..end].to_vec()
545 } else {
546 Vec::new()
547 };
548 if is_reserved_name(&possible, possible.len() as isize - 1) {
549 device = Some(std::format!("\\\\?\\{}", str_of(&possible)));
550 root_end = 4 + possible.len();
551 }
552 } else if j == len {
553 return std::format!("\\\\{first_part}\\{}\\", str_of(&path[last..]));
554 } else {
555 device =
556 Some(std::format!("\\\\{first_part}\\{}", str_of(&path[last..j])));
557 root_end = j;
558 }
559 }
560 }
561 }
562 } else {
563 root_end = 1;
564 }
565 } else {
566 let colon_index = index_of(path, ':', 0);
567 if colon_index > 0 {
568 if is_device_root(code) && colon_index == 1 {
569 device = Some(str_of(&path[..2]));
570 root_end = 2;
571 if len > 2 && f.is_sep(path[2]) {
572 is_absolute = true;
573 root_end = 3;
574 }
575 } else if is_reserved_name(path, colon_index) {
576 device = Some(str_of(&path[..(colon_index + 1) as usize]));
577 root_end = (colon_index + 1) as usize;
578 }
579 }
580 }
581
582 let mut tail = if root_end < len {
583 normalize_string(&path[root_end..], !is_absolute, f, '\\')
584 } else {
585 String::new()
586 };
587 if tail.is_empty() && !is_absolute {
588 tail = ".".into();
589 }
590 if !tail.is_empty() && f.is_sep(path[len - 1]) {
591 tail.push('\\');
592 }
593 if !is_absolute && device.is_none() && path.contains(&':') {
594 let tc = chars(&tail);
597 if tc.len() >= 2 && is_device_root(tc[0]) && tc[1] == ':' {
598 return std::format!(".\\{tail}");
599 }
600 let mut index = index_of(path, ':', 0);
601 while index != -1 {
602 if index == len as isize - 1 || f.is_sep(path[(index + 1) as usize]) {
603 return std::format!(".\\{tail}");
604 }
605 index = index_of(path, ':', (index + 1) as usize);
606 }
607 }
608 if is_reserved_name(path, index_of(path, ':', 0)) {
609 return std::format!(".\\{}{tail}", device.unwrap_or_default());
610 }
611 match device {
612 None => {
613 if is_absolute {
614 std::format!("\\{tail}")
615 } else {
616 tail
617 }
618 }
619 Some(d) => {
620 if is_absolute {
621 std::format!("{d}\\{tail}")
622 } else {
623 std::format!("{d}{tail}")
624 }
625 }
626 }
627}
628
629fn is_absolute(flavor: Flavor, path: &[char]) -> bool {
634 let len = path.len();
635 if len == 0 {
636 return false;
637 }
638 match flavor {
639 Flavor::Posix => path[0] == '/',
640 Flavor::Win32 => {
641 flavor.is_sep(path[0])
642 || (len > 2 && is_device_root(path[0]) && path[1] == ':' && flavor.is_sep(path[2]))
643 }
644 }
645}
646
647fn join(flavor: Flavor, args: &[String]) -> String {
648 let parts: Vec<&String> = args.iter().filter(|a| !a.is_empty()).collect();
649 if parts.is_empty() {
650 return ".".into();
651 }
652 match flavor {
653 Flavor::Posix => {
654 let joined = parts
655 .iter()
656 .map(|s| s.as_str())
657 .collect::<Vec<_>>()
658 .join("/");
659 normalize_posix(&chars(&joined))
660 }
661 Flavor::Win32 => join_win32(&parts),
662 }
663}
664
665fn join_win32(parts: &[&String]) -> String {
666 let f = Flavor::Win32;
667 let first_part = chars(parts[0]);
668 let mut joined: String = parts
669 .iter()
670 .map(|s| s.as_str())
671 .collect::<Vec<_>>()
672 .join("\\");
673
674 let mut needs_replace = true;
677 let mut slash_count = 0usize;
678 if f.is_sep(*first_part.first().unwrap_or(&'\0')) {
679 slash_count += 1;
680 let first_len = first_part.len();
681 if first_len > 1 && f.is_sep(first_part[1]) {
682 slash_count += 1;
683 if first_len > 2 {
684 if f.is_sep(first_part[2]) {
685 slash_count += 1;
686 } else {
687 needs_replace = false;
688 }
689 }
690 }
691 }
692 if needs_replace {
693 let jc = chars(&joined);
694 while slash_count < jc.len() && f.is_sep(jc[slash_count]) {
695 slash_count += 1;
696 }
697 if slash_count >= 2 {
698 joined = std::format!("\\{}", str_of(&jc[slash_count..]));
699 }
700 }
701
702 let jc = chars(&joined);
704 let mut segments: Vec<String> = Vec::new();
705 let mut part = String::new();
706 let mut i = 0usize;
707 while i < jc.len() {
708 if jc[i] == '\\' {
709 if !part.is_empty() {
710 segments.push(std::mem::take(&mut part));
711 }
712 part.clear();
713 while i + 1 < jc.len() && jc[i + 1] == '\\' {
714 i += 1;
715 }
716 } else {
717 part.push(jc[i]);
718 }
719 i += 1;
720 }
721 if !part.is_empty() {
722 segments.push(part);
723 }
724 if segments.iter().any(|p| {
725 let pc = chars(p);
726 let ci = index_of(&pc, ':', 0);
727 ci != -1 && is_reserved_name(&pc, ci)
728 }) {
729 return joined.replace('/', "\\");
730 }
731
732 normalize_win32(&chars(&joined))
733}
734
735fn relative(flavor: Flavor, from: &[char], to: &[char]) -> String {
740 match flavor {
741 Flavor::Posix => relative_posix(from, to),
742 Flavor::Win32 => relative_win32(from, to),
743 }
744}
745
746fn relative_posix(from_in: &[char], to_in: &[char]) -> String {
747 if from_in == to_in {
748 return String::new();
749 }
750 let from = chars(&resolve_posix(&[str_of(from_in)]));
751 let to = chars(&resolve_posix(&[str_of(to_in)]));
752 if from == to {
753 return String::new();
754 }
755
756 let from_start = 1isize;
757 let from_end = from.len() as isize;
758 let from_len = from_end - from_start;
759 let to_start = 1isize;
760 let to_len = to.len() as isize - to_start;
761
762 let length = from_len.min(to_len);
763 let mut last_common_sep: isize = -1;
764 let mut i = 0isize;
765 while i < length {
766 let fc = from[(from_start + i) as usize];
767 if fc != to[(to_start + i) as usize] {
768 break;
769 } else if fc == '/' {
770 last_common_sep = i;
771 }
772 i += 1;
773 }
774 if i == length {
775 if to_len > length {
776 if to[(to_start + i) as usize] == '/' {
777 return str_of(&to[(to_start + i + 1) as usize..]);
778 }
779 if i == 0 {
780 return str_of(&to[(to_start + i) as usize..]);
781 }
782 } else if from_len > length {
783 if from[(from_start + i) as usize] == '/' {
784 last_common_sep = i;
785 } else if i == 0 {
786 last_common_sep = 0;
787 }
788 }
789 }
790
791 let mut out = String::new();
792 let mut k = from_start + last_common_sep + 1;
793 while k <= from_end {
794 if k == from_end || from[k as usize] == '/' {
795 out.push_str(if out.is_empty() { ".." } else { "/.." });
796 }
797 k += 1;
798 }
799 std::format!(
800 "{out}{}",
801 str_of(&to[(to_start + last_common_sep) as usize..])
802 )
803}
804
805fn relative_win32(from_in: &[char], to_in: &[char]) -> String {
806 if from_in == to_in {
807 return String::new();
808 }
809 let from_orig = resolve_win32(&[str_of(from_in)]);
810 let to_orig = resolve_win32(&[str_of(to_in)]);
811 if from_orig == to_orig {
812 return String::new();
813 }
814 let from_lc = from_orig.to_lowercase();
815 let to_lc = to_orig.to_lowercase();
816 if from_lc == to_lc {
817 return String::new();
818 }
819
820 let from_orig_c = chars(&from_orig);
821 let to_orig_c = chars(&to_orig);
822 let from = chars(&from_lc);
823 let to = chars(&to_lc);
824
825 if from_orig_c.len() != from.len() || to_orig_c.len() != to.len() {
828 let mut from_split: Vec<String> = from_orig.split('\\').map(str::to_string).collect();
829 let mut to_split: Vec<String> = to_orig.split('\\').map(str::to_string).collect();
830 if from_split.last().is_some_and(String::is_empty) {
831 from_split.pop();
832 }
833 if to_split.last().is_some_and(String::is_empty) {
834 to_split.pop();
835 }
836 let from_len = from_split.len();
837 let to_len = to_split.len();
838 let length = from_len.min(to_len);
839 let mut i = 0usize;
840 while i < length {
841 if from_split[i].to_lowercase() != to_split[i].to_lowercase() {
842 break;
843 }
844 i += 1;
845 }
846 if i == 0 {
847 return to_orig;
848 } else if i == length {
849 if to_len > length {
850 return to_split[i..].join("\\");
851 }
852 if from_len > length {
853 return "..\\".repeat(from_len - 1 - i) + "..";
854 }
855 return String::new();
856 }
857 return "..\\".repeat(from_len - i) + &to_split[i..].join("\\");
858 }
859
860 let mut from_start = 0isize;
861 while (from_start as usize) < from.len() && from[from_start as usize] == '\\' {
862 from_start += 1;
863 }
864 let mut from_end = from.len() as isize;
865 while from_end - 1 > from_start && from[(from_end - 1) as usize] == '\\' {
866 from_end -= 1;
867 }
868 let from_len = from_end - from_start;
869
870 let mut to_start = 0isize;
871 while (to_start as usize) < to.len() && to[to_start as usize] == '\\' {
872 to_start += 1;
873 }
874 let mut to_end = to.len() as isize;
875 while to_end - 1 > to_start && to[(to_end - 1) as usize] == '\\' {
876 to_end -= 1;
877 }
878 let to_len = to_end - to_start;
879
880 let length = from_len.min(to_len);
881 let mut last_common_sep: isize = -1;
882 let mut i = 0isize;
883 while i < length {
884 let fc = from[(from_start + i) as usize];
885 if fc != to[(to_start + i) as usize] {
886 break;
887 } else if fc == '\\' {
888 last_common_sep = i;
889 }
890 i += 1;
891 }
892
893 if i != length {
894 if last_common_sep == -1 {
895 return to_orig;
896 }
897 } else {
898 if to_len > length {
899 if to[(to_start + i) as usize] == '\\' {
900 return str_of(&to_orig_c[(to_start + i + 1) as usize..]);
901 }
902 if i == 2 {
903 return str_of(&to_orig_c[(to_start + i) as usize..]);
904 }
905 }
906 if from_len > length {
907 if from[(from_start + i) as usize] == '\\' {
908 last_common_sep = i;
909 } else if i == 2 {
910 last_common_sep = 3;
911 }
912 }
913 if last_common_sep == -1 {
914 last_common_sep = 0;
915 }
916 }
917
918 let mut out = String::new();
919 let mut k = from_start + last_common_sep + 1;
920 while k <= from_end {
921 if k == from_end || from[k as usize] == '\\' {
922 out.push_str(if out.is_empty() { ".." } else { "\\.." });
923 }
924 k += 1;
925 }
926
927 to_start += last_common_sep;
928 if !out.is_empty() {
929 return std::format!(
930 "{out}{}",
931 str_of(&to_orig_c[to_start as usize..to_end as usize])
932 );
933 }
934 if to_orig_c.get(to_start as usize) == Some(&'\\') {
935 to_start += 1;
936 }
937 str_of(&to_orig_c[to_start as usize..to_end as usize])
938}
939
940fn to_namespaced_path(flavor: Flavor, path: &str) -> String {
945 if flavor == Flavor::Posix || path.is_empty() {
946 return path.to_string();
947 }
948 let resolved = resolve_win32(&[path.to_string()]);
949 let rc = chars(&resolved);
950 if rc.len() <= 2 {
951 return path.to_string();
952 }
953 if rc[0] == '\\' {
954 if rc[1] == '\\' && rc[2] != '?' && rc[2] != '.' {
955 return std::format!("\\\\?\\UNC\\{}", str_of(&rc[2..]));
956 }
957 } else if is_device_root(rc[0]) && rc[1] == ':' && rc[2] == '\\' {
958 return std::format!("\\\\?\\{resolved}");
959 }
960 resolved
961}
962
963fn dirname(flavor: Flavor, path: &[char]) -> String {
968 match flavor {
969 Flavor::Posix => dirname_posix(path),
970 Flavor::Win32 => dirname_win32(path),
971 }
972}
973
974fn dirname_posix(path: &[char]) -> String {
975 if path.is_empty() {
976 return ".".into();
977 }
978 let has_root = path[0] == '/';
979 let mut end: isize = -1;
980 let mut matched_slash = true;
981 let mut i = path.len() as isize - 1;
982 while i >= 1 {
983 if path[i as usize] == '/' {
984 if !matched_slash {
985 end = i;
986 break;
987 }
988 } else {
989 matched_slash = false;
990 }
991 i -= 1;
992 }
993 if end == -1 {
994 return if has_root { "/".into() } else { ".".into() };
995 }
996 if has_root && end == 1 {
997 return "//".into();
998 }
999 str_of(&path[..end as usize])
1000}
1001
1002fn dirname_win32(path: &[char]) -> String {
1003 let f = Flavor::Win32;
1004 let len = path.len();
1005 if len == 0 {
1006 return ".".into();
1007 }
1008 let mut root_end: isize = -1;
1009 let mut offset: usize = 0;
1010 let code = path[0];
1011
1012 if len == 1 {
1013 return if f.is_sep(code) {
1014 str_of(path)
1015 } else {
1016 ".".into()
1017 };
1018 }
1019
1020 if f.is_sep(code) {
1021 root_end = 1;
1022 offset = 1;
1023 if f.is_sep(path[1]) {
1024 let mut j = 2usize;
1025 let mut last = j;
1026 while j < len && !f.is_sep(path[j]) {
1027 j += 1;
1028 }
1029 if j < len && j != last {
1030 last = j;
1031 while j < len && f.is_sep(path[j]) {
1032 j += 1;
1033 }
1034 if j < len && j != last {
1035 last = j;
1036 while j < len && !f.is_sep(path[j]) {
1037 j += 1;
1038 }
1039 if j == len {
1040 return str_of(path);
1041 }
1042 if j != last {
1043 root_end = j as isize + 1;
1044 offset = j + 1;
1045 }
1046 }
1047 }
1048 }
1049 } else if is_device_root(code) && path[1] == ':' {
1050 root_end = if len > 2 && f.is_sep(path[2]) { 3 } else { 2 };
1051 offset = root_end as usize;
1052 }
1053
1054 let mut end: isize = -1;
1055 let mut matched_slash = true;
1056 let mut i = len as isize - 1;
1057 while i >= offset as isize {
1058 if f.is_sep(path[i as usize]) {
1059 if !matched_slash {
1060 end = i;
1061 break;
1062 }
1063 } else {
1064 matched_slash = false;
1065 }
1066 i -= 1;
1067 }
1068
1069 if end == -1 {
1070 if root_end == -1 {
1071 return ".".into();
1072 }
1073 end = root_end;
1074 }
1075 str_of(&path[..end as usize])
1076}
1077
1078fn basename(flavor: Flavor, path: &[char], suffix: Option<&[char]>) -> String {
1079 let mut start: isize = 0;
1080 let mut end: isize = -1;
1081 let mut matched_slash = true;
1082
1083 if flavor == Flavor::Win32 && path.len() >= 2 && is_device_root(path[0]) && path[1] == ':' {
1085 start = 2;
1086 }
1087
1088 if let Some(sfx) = suffix.filter(|s| !s.is_empty() && s.len() <= path.len()) {
1089 if sfx == path {
1090 return String::new();
1091 }
1092 let mut ext_idx: isize = sfx.len() as isize - 1;
1093 let mut first_non_slash_end: isize = -1;
1094 let mut i = path.len() as isize - 1;
1095 while i >= start {
1096 let code = path[i as usize];
1097 if flavor.is_sep(code) {
1098 if !matched_slash {
1099 start = i + 1;
1100 break;
1101 }
1102 } else {
1103 if first_non_slash_end == -1 {
1104 matched_slash = false;
1105 first_non_slash_end = i + 1;
1106 }
1107 if ext_idx >= 0 {
1108 if code == sfx[ext_idx as usize] {
1109 ext_idx -= 1;
1110 if ext_idx == -1 {
1111 end = i;
1112 }
1113 } else {
1114 ext_idx = -1;
1115 end = first_non_slash_end;
1116 }
1117 }
1118 }
1119 i -= 1;
1120 }
1121 if start == end {
1122 end = first_non_slash_end;
1123 } else if end == -1 {
1124 end = path.len() as isize;
1125 }
1126 return str_of(&path[start.max(0) as usize..end.max(0) as usize]);
1127 }
1128
1129 let mut i = path.len() as isize - 1;
1130 while i >= start {
1131 if flavor.is_sep(path[i as usize]) {
1132 if !matched_slash {
1133 start = i + 1;
1134 break;
1135 }
1136 } else if end == -1 {
1137 matched_slash = false;
1138 end = i + 1;
1139 }
1140 i -= 1;
1141 }
1142 if end == -1 {
1143 return String::new();
1144 }
1145 str_of(&path[start as usize..end as usize])
1146}
1147
1148fn extname(flavor: Flavor, path: &[char]) -> String {
1149 let mut start: isize = 0;
1150 let mut start_dot: isize = -1;
1151 let mut start_part: isize = 0;
1152 let mut end: isize = -1;
1153 let mut matched_slash = true;
1154 let mut pre_dot_state: isize = 0;
1155
1156 if flavor == Flavor::Win32 && path.len() >= 2 && path[1] == ':' && is_device_root(path[0]) {
1157 start = 2;
1158 start_part = 2;
1159 }
1160
1161 let mut i = path.len() as isize - 1;
1162 while i >= start {
1163 let code = path[i as usize];
1164 if flavor.is_sep(code) {
1165 if !matched_slash {
1166 start_part = i + 1;
1167 break;
1168 }
1169 i -= 1;
1170 continue;
1171 }
1172 if end == -1 {
1173 matched_slash = false;
1174 end = i + 1;
1175 }
1176 if code == '.' {
1177 if start_dot == -1 {
1178 start_dot = i;
1179 } else if pre_dot_state != 1 {
1180 pre_dot_state = 1;
1181 }
1182 } else if start_dot != -1 {
1183 pre_dot_state = -1;
1184 }
1185 i -= 1;
1186 }
1187
1188 if start_dot == -1
1189 || end == -1
1190 || pre_dot_state == 0
1191 || (pre_dot_state == 1 && start_dot == end - 1 && start_dot == start_part + 1)
1192 {
1193 return String::new();
1194 }
1195 str_of(&path[start_dot as usize..end as usize])
1196}
1197
1198fn new_parsed(root: &str, dir: &str, base: &str, ext: &str, name: &str) -> Value {
1203 with_host(|h| {
1204 let mut m = IndexMap::new();
1205 m.insert("root".into(), h.new_str(root));
1206 m.insert("dir".into(), h.new_str(dir));
1207 m.insert("base".into(), h.new_str(base));
1208 m.insert("ext".into(), h.new_str(ext));
1209 m.insert("name".into(), h.new_str(name));
1210 h.new_object(m)
1211 })
1212}
1213
1214fn parse(flavor: Flavor, path: &[char]) -> Value {
1215 let (root, dir, base, ext, name) = match flavor {
1216 Flavor::Posix => parse_posix(path),
1217 Flavor::Win32 => parse_win32(path),
1218 };
1219 new_parsed(&root, &dir, &base, &ext, &name)
1220}
1221
1222type Parsed = (String, String, String, String, String);
1223
1224fn parse_posix(path: &[char]) -> Parsed {
1225 let mut root = String::new();
1226 let mut dir = String::new();
1227 let mut base = String::new();
1228 let mut ext = String::new();
1229 let mut name = String::new();
1230 if path.is_empty() {
1231 return (root, dir, base, ext, name);
1232 }
1233 let is_abs = path[0] == '/';
1234 let scan_start: isize = if is_abs {
1235 root = "/".into();
1236 1
1237 } else {
1238 0
1239 };
1240
1241 let mut start_dot: isize = -1;
1242 let mut start_part: isize = 0;
1243 let mut end: isize = -1;
1244 let mut matched_slash = true;
1245 let mut pre_dot_state: isize = 0;
1246 let mut i = path.len() as isize - 1;
1247 while i >= scan_start {
1248 let code = path[i as usize];
1249 if code == '/' {
1250 if !matched_slash {
1251 start_part = i + 1;
1252 break;
1253 }
1254 i -= 1;
1255 continue;
1256 }
1257 if end == -1 {
1258 matched_slash = false;
1259 end = i + 1;
1260 }
1261 if code == '.' {
1262 if start_dot == -1 {
1263 start_dot = i;
1264 } else if pre_dot_state != 1 {
1265 pre_dot_state = 1;
1266 }
1267 } else if start_dot != -1 {
1268 pre_dot_state = -1;
1269 }
1270 i -= 1;
1271 }
1272
1273 if end != -1 {
1274 let s = if start_part == 0 && is_abs {
1275 1
1276 } else {
1277 start_part
1278 };
1279 if start_dot == -1
1280 || pre_dot_state == 0
1281 || (pre_dot_state == 1 && start_dot == end - 1 && start_dot == start_part + 1)
1282 {
1283 base = str_of(&path[s as usize..end as usize]);
1284 name = base.clone();
1285 } else {
1286 name = str_of(&path[s as usize..start_dot as usize]);
1287 base = str_of(&path[s as usize..end as usize]);
1288 ext = str_of(&path[start_dot as usize..end as usize]);
1289 }
1290 }
1291
1292 if start_part > 0 {
1293 dir = str_of(&path[..(start_part - 1) as usize]);
1294 } else if is_abs {
1295 dir = "/".into();
1296 }
1297 (root, dir, base, ext, name)
1298}
1299
1300fn parse_win32(path: &[char]) -> Parsed {
1301 let f = Flavor::Win32;
1302 let mut root = String::new();
1303 let mut dir = String::new();
1304 let mut base = String::new();
1305 let mut ext = String::new();
1306 let mut name = String::new();
1307 let len = path.len();
1308 if len == 0 {
1309 return (root, dir, base, ext, name);
1310 }
1311
1312 let mut root_end: usize = 0;
1313 let code = path[0];
1314
1315 if len == 1 {
1316 if f.is_sep(code) {
1317 root = str_of(path);
1318 dir = root.clone();
1319 return (root, dir, base, ext, name);
1320 }
1321 base = str_of(path);
1322 name = base.clone();
1323 return (root, dir, base, ext, name);
1324 }
1325
1326 if f.is_sep(code) {
1327 root_end = 1;
1328 if f.is_sep(path[1]) {
1329 let mut j = 2usize;
1330 let mut last = j;
1331 while j < len && !f.is_sep(path[j]) {
1332 j += 1;
1333 }
1334 if j < len && j != last {
1335 last = j;
1336 while j < len && f.is_sep(path[j]) {
1337 j += 1;
1338 }
1339 if j < len && j != last {
1340 last = j;
1341 while j < len && !f.is_sep(path[j]) {
1342 j += 1;
1343 }
1344 if j == len {
1345 root_end = j;
1346 } else if j != last {
1347 root_end = j + 1;
1348 }
1349 }
1350 }
1351 }
1352 } else if is_device_root(code) && path[1] == ':' {
1353 if len <= 2 {
1354 root = str_of(path);
1355 dir = root.clone();
1356 return (root, dir, base, ext, name);
1357 }
1358 root_end = 2;
1359 if f.is_sep(path[2]) {
1360 if len == 3 {
1361 root = str_of(path);
1362 dir = root.clone();
1363 return (root, dir, base, ext, name);
1364 }
1365 root_end = 3;
1366 }
1367 }
1368 if root_end > 0 {
1369 root = str_of(&path[..root_end]);
1370 }
1371
1372 let mut start_dot: isize = -1;
1373 let mut start_part: isize = root_end as isize;
1374 let mut end: isize = -1;
1375 let mut matched_slash = true;
1376 let mut pre_dot_state: isize = 0;
1377 let mut i = len as isize - 1;
1378 while i >= root_end as isize {
1379 let c = path[i as usize];
1380 if f.is_sep(c) {
1381 if !matched_slash {
1382 start_part = i + 1;
1383 break;
1384 }
1385 i -= 1;
1386 continue;
1387 }
1388 if end == -1 {
1389 matched_slash = false;
1390 end = i + 1;
1391 }
1392 if c == '.' {
1393 if start_dot == -1 {
1394 start_dot = i;
1395 } else if pre_dot_state != 1 {
1396 pre_dot_state = 1;
1397 }
1398 } else if start_dot != -1 {
1399 pre_dot_state = -1;
1400 }
1401 i -= 1;
1402 }
1403
1404 if end != -1 {
1405 if start_dot == -1
1406 || pre_dot_state == 0
1407 || (pre_dot_state == 1 && start_dot == end - 1 && start_dot == start_part + 1)
1408 {
1409 base = str_of(&path[start_part as usize..end as usize]);
1410 name = base.clone();
1411 } else {
1412 name = str_of(&path[start_part as usize..start_dot as usize]);
1413 base = str_of(&path[start_part as usize..end as usize]);
1414 ext = str_of(&path[start_dot as usize..end as usize]);
1415 }
1416 }
1417
1418 if start_part > 0 && start_part != root_end as isize {
1419 dir = str_of(&path[..(start_part - 1) as usize]);
1420 } else {
1421 dir = root.clone();
1422 }
1423 (root, dir, base, ext, name)
1424}
1425
1426fn format(flavor: Flavor, obj: Option<&Value>) -> String {
1428 let Some(obj) = obj else { return String::new() };
1429 let get = |k: &str| {
1430 with_host(|h| match h.get(obj) {
1431 Some(crate::host::JsObj::Object(p)) => match p.get(k) {
1432 Some(Value::Undef) | None => String::new(),
1433 Some(v) => h.str_of(v),
1434 },
1435 _ => String::new(),
1436 })
1437 };
1438 let root = get("root");
1439 let dir_raw = get("dir");
1440 let base_raw = get("base");
1441 let base = if !base_raw.is_empty() {
1442 base_raw
1443 } else {
1444 let ext = get("ext");
1445 let ext = if ext.is_empty() {
1446 String::new()
1447 } else if ext.starts_with('.') {
1448 ext
1449 } else {
1450 std::format!(".{ext}")
1451 };
1452 std::format!("{}{ext}", get("name"))
1453 };
1454 let dir = if !dir_raw.is_empty() {
1455 dir_raw
1456 } else {
1457 root.clone()
1458 };
1459 if dir.is_empty() {
1460 return base;
1461 }
1462 if dir == root {
1463 std::format!("{dir}{base}")
1464 } else {
1465 std::format!("{dir}{}{base}", flavor.sep())
1466 }
1467}
1468
1469fn matches_glob(flavor: Flavor, path: &str, pattern: &str) -> bool {
1475 let (path, pattern) = match flavor {
1476 Flavor::Posix => (path.to_string(), pattern.to_string()),
1477 Flavor::Win32 => (path.replace('\\', "/"), pattern.replace('\\', "/")),
1478 };
1479 let text: Vec<char> = path.chars().collect();
1480 expand_braces(&pattern)
1481 .iter()
1482 .any(|pat| glob_match(&text, &pat.chars().collect::<Vec<char>>()))
1483}
1484
1485fn expand_braces(pattern: &str) -> Vec<String> {
1487 let chars: Vec<char> = pattern.chars().collect();
1488 for (i, &c) in chars.iter().enumerate() {
1489 if c != '{' {
1490 continue;
1491 }
1492 let mut depth = 1;
1493 let mut commas: Vec<usize> = Vec::new();
1494 let mut close = None;
1495 for (j, &cj) in chars.iter().enumerate().skip(i + 1) {
1496 match cj {
1497 '{' => depth += 1,
1498 '}' => {
1499 depth -= 1;
1500 if depth == 0 {
1501 close = Some(j);
1502 break;
1503 }
1504 }
1505 ',' if depth == 1 => commas.push(j),
1506 _ => {}
1507 }
1508 }
1509 let (Some(close), false) = (close, commas.is_empty()) else {
1510 continue;
1511 };
1512 let prefix: String = chars[..i].iter().collect();
1513 let suffix: String = chars[close + 1..].iter().collect();
1514 let mut bounds = vec![i];
1515 bounds.extend(&commas);
1516 bounds.push(close);
1517 let mut out = Vec::new();
1518 for w in bounds.windows(2) {
1519 let alt: String = chars[w[0] + 1..w[1]].iter().collect();
1520 out.extend(expand_braces(&std::format!("{prefix}{alt}{suffix}")));
1521 }
1522 return out;
1523 }
1524 vec![pattern.to_string()]
1525}
1526
1527fn glob_match(t: &[char], p: &[char]) -> bool {
1529 if p.is_empty() {
1530 return t.is_empty();
1531 }
1532 match p[0] {
1533 '*' => {
1534 let double = p.len() >= 2 && p[1] == '*';
1535 let rest = {
1536 let mut k = 0;
1537 while k < p.len() && p[k] == '*' {
1538 k += 1;
1539 }
1540 &p[k..]
1541 };
1542 if rest.is_empty() {
1543 return double || !t.contains(&'/');
1544 }
1545 let mut ti = 0;
1546 loop {
1547 if glob_match(&t[ti..], rest) {
1548 return true;
1549 }
1550 if ti >= t.len() {
1551 return false;
1552 }
1553 if !double && t[ti] == '/' {
1554 return false;
1555 }
1556 ti += 1;
1557 }
1558 }
1559 '?' => !t.is_empty() && t[0] != '/' && glob_match(&t[1..], &p[1..]),
1560 '[' => match match_class(t.first().copied(), p) {
1561 Some((matched, plen)) => matched && glob_match(&t[1..], &p[plen..]),
1562 None => !t.is_empty() && t[0] == '[' && glob_match(&t[1..], &p[1..]),
1564 },
1565 c => !t.is_empty() && t[0] == c && glob_match(&t[1..], &p[1..]),
1566 }
1567}
1568
1569fn match_class(ch: Option<char>, p: &[char]) -> Option<(bool, usize)> {
1572 let mut i = 1;
1573 let mut negate = false;
1574 if matches!(p.get(i), Some('!') | Some('^')) {
1575 negate = true;
1576 i += 1;
1577 }
1578 let start = i;
1579 let mut matched = false;
1580 while i < p.len() && (p[i] != ']' || i == start) {
1581 if i + 2 < p.len() && p[i + 1] == '-' && p[i + 2] != ']' {
1582 if let Some(c) = ch {
1583 if p[i] <= c && c <= p[i + 2] {
1584 matched = true;
1585 }
1586 }
1587 i += 3;
1588 } else {
1589 if ch == Some(p[i]) {
1590 matched = true;
1591 }
1592 i += 1;
1593 }
1594 }
1595 if i >= p.len() {
1596 return None;
1597 }
1598 let ok = matches!(ch, Some(c) if c != '/') && (matched ^ negate);
1600 Some((ok, i + 1))
1601}