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