authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-11-24 15:27:24-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-11-24 15:27:24-08:00
log53e615b920a5c8de19df515c40edc70c82aee392
treec47df5b06b926dd85173ed09eb5a332e1bb1f673
parent32dc46aae56623bff9b1fc792d49913f9295be7b
parent822f41242438faeaaf9846e3ebed454b59525ba7
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #25993 from squeek502/windows-paths

Teach `std.fs.path` about the wonderful world of Windows paths

17 files changed, 1699 insertions(+), 776 deletions(-)

lib/compiler/resinator/compile.zig+1-1
......@@ -2914,7 +2914,7 @@ fn validateSearchPath(path: []const u8) error{BadPathName}!void {
29142914 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
29152915 // Those path types are something of an unavoidable way to
29162916 // still hit unreachable during the openDir call.
2917 var component_iterator = try std.fs.path.componentIterator(path);
2917 var component_iterator = std.fs.path.componentIterator(path);
29182918 while (component_iterator.next()) |component| {
29192919 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
29202920 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;
lib/std/Build/Cache.zig+1-3
......@@ -104,9 +104,7 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath {
104104fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {
105105 const relative = try fs.path.relative(allocator, prefix, path);
106106 errdefer allocator.free(relative);
107 var component_iterator = fs.path.NativeComponentIterator.init(relative) catch {
108 return error.NotASubPath;
109 };
107 var component_iterator = fs.path.NativeComponentIterator.init(relative);
110108 if (component_iterator.root() != null) {
111109 return error.NotASubPath;
112110 }
lib/std/Build/Watch/FsEvents.zig+1-1
......@@ -167,7 +167,7 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)
167167 }.lessThan);
168168 need_dirs.clearRetainingCapacity();
169169 for (old_dirs) |dir_path| {
170 var it: std.fs.path.ComponentIterator(.posix, u8) = try .init(dir_path);
170 var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path);
171171 while (it.next()) |component| {
172172 if (need_dirs.contains(component.path)) {
173173 // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added
lib/std/Io/Dir.zig+1-1
......@@ -318,7 +318,7 @@ pub const MakePathStatus = enum { existed, created };
318318/// Same as `makePath` except returns whether the path already existed or was
319319/// successfully created.
320320pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus {
321 var it = try std.fs.path.componentIterator(sub_path);
321 var it = std.fs.path.componentIterator(sub_path);
322322 var status: MakePathStatus = .existed;
323323 var component = it.last() orelse return error.BadPathName;
324324 while (true) {
lib/std/Io/Threaded.zig+1-1
......@@ -1210,7 +1210,7 @@ fn dirMakeOpenPathWindows(
12101210 w.SYNCHRONIZE | w.FILE_TRAVERSE |
12111211 (if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0));
12121212
1213 var it = try std.fs.path.componentIterator(sub_path);
1213 var it = std.fs.path.componentIterator(sub_path);
12141214 // If there are no components in the path, then create a dummy component with the full path.
12151215 var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
12161216 .name = "",
lib/std/fs/path.zig+1066-496
......@@ -20,10 +20,7 @@ const testing = std.testing;
2020const mem = std.mem;
2121const ascii = std.ascii;
2222const Allocator = mem.Allocator;
23const math = std.math;
2423const windows = std.os.windows;
25const os = std.os;
26const fs = std.fs;
2724const process = std.process;
2825const native_os = builtin.target.os.tag;
2926
......@@ -60,11 +57,12 @@ pub const PathType = enum {
6057 posix,
6158
6259 /// Returns true if `c` is a valid path separator for the `path_type`.
60 /// If `T` is `u16`, `c` is assumed to be little-endian.
6361 pub inline fn isSep(comptime path_type: PathType, comptime T: type, c: T) bool {
6462 return switch (path_type) {
65 .windows => c == '/' or c == '\\',
66 .posix => c == '/',
67 .uefi => c == '\\',
63 .windows => c == mem.nativeToLittle(T, '/') or c == mem.nativeToLittle(T, '\\'),
64 .posix => c == mem.nativeToLittle(T, '/'),
65 .uefi => c == mem.nativeToLittle(T, '\\'),
6866 };
6967 }
7068};
......@@ -221,7 +219,7 @@ test join {
221219 try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);
222220 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
223221 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
224 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b\\", "c" }, "c:\\a\\b\\c", zero);
222 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b\\", "\\c" }, "c:\\a\\b\\c", zero);
225223
226224 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero);
227225 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero);
......@@ -283,26 +281,16 @@ pub fn isAbsolute(path: []const u8) bool {
283281}
284282
285283fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {
286 if (path.len < 1)
287 return false;
288
289 if (path[0] == '/')
290 return true;
291
292 if (path[0] == '\\')
293 return true;
294
295 if (path.len < 3)
296 return false;
297
298 if (path[1] == ':') {
299 if (path[2] == '/')
300 return true;
301 if (path[2] == '\\')
302 return true;
303 }
304
305 return false;
284 return switch (windows.getWin32PathType(T, path)) {
285 // Unambiguously absolute
286 .drive_absolute, .unc_absolute, .local_device, .root_local_device => true,
287 // Unambiguously relative
288 .relative => false,
289 // Ambiguous, more absolute than relative
290 .rooted => true,
291 // Ambiguous, more relative than absolute
292 .drive_relative => false,
293 };
306294}
307295
308296pub fn isAbsoluteWindows(path: []const u8) bool {
......@@ -347,6 +335,9 @@ test isAbsoluteWindows {
347335 try testIsAbsoluteWindows("C:\\Users\\", true);
348336 try testIsAbsoluteWindows("C:cwd/another", false);
349337 try testIsAbsoluteWindows("C:cwd\\another", false);
338 try testIsAbsoluteWindows("λ:\\", true);
339 try testIsAbsoluteWindows("λ:", false);
340 try testIsAbsoluteWindows("\u{10000}:\\", false);
350341 try testIsAbsoluteWindows("directory/directory", false);
351342 try testIsAbsoluteWindows("directory\\directory", false);
352343 try testIsAbsoluteWindows("/usr/local", true);
......@@ -362,12 +353,17 @@ test isAbsolutePosix {
362353
363354fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {
364355 try testing.expectEqual(expected_result, isAbsoluteWindows(path));
356 const path_w = try std.unicode.wtf8ToWtf16LeAllocZ(std.testing.allocator, path);
357 defer std.testing.allocator.free(path_w);
358 try testing.expectEqual(expected_result, isAbsoluteWindowsW(path_w));
359 try testing.expectEqual(expected_result, isAbsoluteWindowsWtf16(path_w));
365360}
366361
367362fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {
368363 try testing.expectEqual(expected_result, isAbsolutePosix(path));
369364}
370365
366/// Deprecated; see `WindowsPath2`
371367pub const WindowsPath = struct {
372368 is_abs: bool,
373369 kind: Kind,
......@@ -380,6 +376,7 @@ pub const WindowsPath = struct {
380376 };
381377};
382378
379/// Deprecated; see `parsePathWindows`
383380pub fn windowsParsePath(path: []const u8) WindowsPath {
384381 if (path.len >= 2 and path[1] == ':') {
385382 return WindowsPath{
......@@ -402,26 +399,18 @@ pub fn windowsParsePath(path: []const u8) WindowsPath {
402399 .disk_designator = &[_]u8{},
403400 .is_abs = false,
404401 };
405 if (path.len < "//a/b".len) {
406 return relative_path;
407 }
408
409 inline for ("/\\") |this_sep| {
410 const two_sep = [_]u8{ this_sep, this_sep };
411 if (mem.startsWith(u8, path, &two_sep)) {
412 if (path[2] == this_sep) {
413 return relative_path;
414 }
415402
416 var it = mem.tokenizeAny(u8, path, "/\\");
417 _ = (it.next() orelse return relative_path);
418 _ = (it.next() orelse return relative_path);
419 return WindowsPath{
420 .is_abs = isAbsoluteWindows(path),
421 .kind = WindowsPath.Kind.NetworkShare,
422 .disk_designator = path[0..it.index],
423 };
424 }
403 if (path.len >= 2 and PathType.windows.isSep(u8, path[0]) and PathType.windows.isSep(u8, path[1])) {
404 const root_end = root_end: {
405 var server_end = mem.indexOfAnyPos(u8, path, 2, "/\\") orelse break :root_end path.len;
406 while (server_end < path.len and PathType.windows.isSep(u8, path[server_end])) server_end += 1;
407 break :root_end mem.indexOfAnyPos(u8, path, server_end, "/\\") orelse path.len;
408 };
409 return WindowsPath{
410 .is_abs = true,
411 .kind = WindowsPath.Kind.NetworkShare,
412 .disk_designator = path[0..root_end],
413 };
425414 }
426415 return relative_path;
427416}
......@@ -446,10 +435,22 @@ test windowsParsePath {
446435 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));
447436 }
448437 {
449 const parsed = windowsParsePath("\\\\a\\");
450 try testing.expect(!parsed.is_abs);
451 try testing.expect(parsed.kind == WindowsPath.Kind.None);
452 try testing.expect(mem.eql(u8, parsed.disk_designator, ""));
438 const parsed = windowsParsePath("\\/a\\");
439 try testing.expect(parsed.is_abs);
440 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
441 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\/a\\"));
442 }
443 {
444 const parsed = windowsParsePath("\\\\a\\\\b");
445 try testing.expect(parsed.is_abs);
446 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
447 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
448 }
449 {
450 const parsed = windowsParsePath("\\\\a\\\\b\\c");
451 try testing.expect(parsed.is_abs);
452 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
453 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
453454 }
454455 {
455456 const parsed = windowsParsePath("/usr/local");
......@@ -465,6 +466,229 @@ test windowsParsePath {
465466 }
466467}
467468
469/// On Windows, this calls `parsePathWindows` and on POSIX it calls `parsePathPosix`.
470///
471/// Returns a platform-specific struct with two fields: `root` and `kind`.
472/// The `root` will be a slice of `path` (`/` for POSIX absolute paths, and things
473/// like `C:\`, `\\server\share\`, etc for Windows paths).
474/// If the path is of kind `.relative`, then `root` will be zero-length.
475pub fn parsePath(path: []const u8) switch (native_os) {
476 .windows => WindowsPath2(u8),
477 else => PosixPath,
478} {
479 switch (native_os) {
480 .windows => return parsePathWindows(u8, path),
481 else => return parsePathPosix(path),
482 }
483}
484
485const PosixPath = struct {
486 kind: enum { relative, absolute },
487 root: []const u8,
488};
489
490pub fn parsePathPosix(path: []const u8) PosixPath {
491 const abs = isAbsolutePosix(path);
492 return .{
493 .kind = if (abs) .absolute else .relative,
494 .root = if (abs) path[0..1] else path[0..0],
495 };
496}
497
498test parsePathPosix {
499 {
500 const parsed = parsePathPosix("a/b");
501 try testing.expectEqual(.relative, parsed.kind);
502 try testing.expectEqualStrings("", parsed.root);
503 }
504 {
505 const parsed = parsePathPosix("/a/b");
506 try testing.expectEqual(.absolute, parsed.kind);
507 try testing.expectEqualStrings("/", parsed.root);
508 }
509 {
510 const parsed = parsePathPosix("///a/b");
511 try testing.expectEqual(.absolute, parsed.kind);
512 try testing.expectEqualStrings("/", parsed.root);
513 }
514}
515
516pub fn WindowsPath2(comptime T: type) type {
517 return struct {
518 kind: windows.Win32PathType,
519 root: []const T,
520 };
521}
522
523pub fn parsePathWindows(comptime T: type, path: []const T) WindowsPath2(T) {
524 const kind = windows.getWin32PathType(T, path);
525 const root = root: switch (kind) {
526 .drive_absolute, .drive_relative => {
527 const drive_letter_len = getDriveLetter(T, path).len;
528 break :root path[0 .. drive_letter_len + @as(usize, if (kind == .drive_absolute) 2 else 1)];
529 },
530 .relative => path[0..0],
531 .local_device => path[0..4],
532 .root_local_device => path,
533 .rooted => path[0..1],
534 .unc_absolute => {
535 const unc = parseUNC(T, path);
536 // There may be any number of path separators between the server and the share,
537 // so take that into account by using pointer math to get the difference.
538 var root_len = 2 + (unc.share.ptr - unc.server.ptr) + unc.share.len;
539 if (unc.sep_after_share) root_len += 1;
540 break :root path[0..root_len];
541 },
542 };
543 return .{
544 .kind = kind,
545 .root = root,
546 };
547}
548
549test parsePathWindows {
550 {
551 const path = "//a/b";
552 const parsed = parsePathWindows(u8, path);
553 try testing.expectEqual(.unc_absolute, parsed.kind);
554 try testing.expectEqualStrings("//a/b", parsed.root);
555 try testWindowsParsePathHarmony(path);
556 }
557 {
558 const path = "\\\\a\\b";
559 const parsed = parsePathWindows(u8, path);
560 try testing.expectEqual(.unc_absolute, parsed.kind);
561 try testing.expectEqualStrings("\\\\a\\b", parsed.root);
562 try testWindowsParsePathHarmony(path);
563 }
564 {
565 const path = "\\/a/b/c";
566 const parsed = parsePathWindows(u8, path);
567 try testing.expectEqual(.unc_absolute, parsed.kind);
568 try testing.expectEqualStrings("\\/a/b/", parsed.root);
569 try testWindowsParsePathHarmony(path);
570 }
571 {
572 const path = "\\\\a\\";
573 const parsed = parsePathWindows(u8, path);
574 try testing.expectEqual(.unc_absolute, parsed.kind);
575 try testing.expectEqualStrings("\\\\a\\", parsed.root);
576 try testWindowsParsePathHarmony(path);
577 }
578 {
579 const path = "\\\\a\\b\\";
580 const parsed = parsePathWindows(u8, path);
581 try testing.expectEqual(.unc_absolute, parsed.kind);
582 try testing.expectEqualStrings("\\\\a\\b\\", parsed.root);
583 try testWindowsParsePathHarmony(path);
584 }
585 {
586 const path = "\\\\a\\/b\\/";
587 const parsed = parsePathWindows(u8, path);
588 try testing.expectEqual(.unc_absolute, parsed.kind);
589 try testing.expectEqualStrings("\\\\a\\/b\\", parsed.root);
590 try testWindowsParsePathHarmony(path);
591 }
592 {
593 const path = "\\\\кириллица\\ελληνικά\\português";
594 const parsed = parsePathWindows(u8, path);
595 try testing.expectEqual(.unc_absolute, parsed.kind);
596 try testing.expectEqualStrings("\\\\кириллица\\ελληνικά\\", parsed.root);
597 try testWindowsParsePathHarmony(path);
598 }
599 {
600 const path = "/usr/local";
601 const parsed = parsePathWindows(u8, path);
602 try testing.expectEqual(.rooted, parsed.kind);
603 try testing.expectEqualStrings("/", parsed.root);
604 try testWindowsParsePathHarmony(path);
605 }
606 {
607 const path = "\\\\.";
608 const parsed = parsePathWindows(u8, path);
609 try testing.expectEqual(.root_local_device, parsed.kind);
610 try testing.expectEqualStrings("\\\\.", parsed.root);
611 try testWindowsParsePathHarmony(path);
612 }
613 {
614 const path = "\\\\.\\a";
615 const parsed = parsePathWindows(u8, path);
616 try testing.expectEqual(.local_device, parsed.kind);
617 try testing.expectEqualStrings("\\\\.\\", parsed.root);
618 try testWindowsParsePathHarmony(path);
619 }
620 {
621 const path = "c:../";
622 const parsed = parsePathWindows(u8, path);
623 try testing.expectEqual(.drive_relative, parsed.kind);
624 try testing.expectEqualStrings("c:", parsed.root);
625 try testWindowsParsePathHarmony(path);
626 }
627 {
628 const path = "C:\\../";
629 const parsed = parsePathWindows(u8, path);
630 try testing.expectEqual(.drive_absolute, parsed.kind);
631 try testing.expectEqualStrings("C:\\", parsed.root);
632 try testWindowsParsePathHarmony(path);
633 }
634 {
635 // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
636 const path = "€:\\";
637 const parsed = parsePathWindows(u8, path);
638 try testing.expectEqual(.drive_absolute, parsed.kind);
639 try testing.expectEqualStrings("€:\\", parsed.root);
640 try testWindowsParsePathHarmony(path);
641 }
642 {
643 const path = "€:";
644 const parsed = parsePathWindows(u8, path);
645 try testing.expectEqual(.drive_relative, parsed.kind);
646 try testing.expectEqualStrings("€:", parsed.root);
647 try testWindowsParsePathHarmony(path);
648 }
649 {
650 // But code points that are encoded as two WTF-16 code units are not
651 const path = "\u{10000}:\\";
652 const parsed = parsePathWindows(u8, path);
653 try testing.expectEqual(.relative, parsed.kind);
654 try testing.expectEqualStrings("", parsed.root);
655 try testWindowsParsePathHarmony(path);
656 }
657 {
658 const path = "\u{10000}:";
659 const parsed = parsePathWindows(u8, path);
660 try testing.expectEqual(.relative, parsed.kind);
661 try testing.expectEqualStrings("", parsed.root);
662 try testWindowsParsePathHarmony(path);
663 }
664 {
665 // Paths are assumed to be in the Win32 namespace, so while this is
666 // likely a NT namespace path, it's treated as a rooted path.
667 const path = "\\??\\foo";
668 const parsed = parsePathWindows(u8, path);
669 try testing.expectEqual(.rooted, parsed.kind);
670 try testing.expectEqualStrings("\\", parsed.root);
671 try testWindowsParsePathHarmony(path);
672 }
673}
674
675fn testWindowsParsePathHarmony(wtf8: []const u8) !void {
676 var wtf16_buf: [256]u16 = undefined;
677 const wtf16_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf, wtf8);
678 const wtf16 = wtf16_buf[0..wtf16_len];
679
680 const wtf8_parsed = parsePathWindows(u8, wtf8);
681 const wtf16_parsed = parsePathWindows(u16, wtf16);
682
683 var wtf8_buf: [256]u8 = undefined;
684 const wtf16_root_as_wtf8_len = std.unicode.wtf16LeToWtf8(&wtf8_buf, wtf16_parsed.root);
685 const wtf16_root_as_wtf8 = wtf8_buf[0..wtf16_root_as_wtf8_len];
686
687 try std.testing.expectEqual(wtf8_parsed.kind, wtf16_parsed.kind);
688 try std.testing.expectEqualStrings(wtf8_parsed.root, wtf16_root_as_wtf8);
689}
690
691/// Deprecated; use `parsePath`
468692pub fn diskDesignator(path: []const u8) []const u8 {
469693 if (native_os == .windows) {
470694 return diskDesignatorWindows(path);
......@@ -473,41 +697,172 @@ pub fn diskDesignator(path: []const u8) []const u8 {
473697 }
474698}
475699
700/// Deprecated; use `parsePathWindows`
476701pub fn diskDesignatorWindows(path: []const u8) []const u8 {
477702 return windowsParsePath(path).disk_designator;
478703}
479704
480fn networkShareServersEql(ns1: []const u8, ns2: []const u8) bool {
481 const sep1 = ns1[0];
482 const sep2 = ns2[0];
705fn WindowsUNC(comptime T: type) type {
706 return struct {
707 server: []const T,
708 sep_after_server: bool,
709 share: []const T,
710 sep_after_share: bool,
711 };
712}
483713
484 var it1 = mem.tokenizeScalar(u8, ns1, sep1);
485 var it2 = mem.tokenizeScalar(u8, ns2, sep2);
714/// Asserts that `path` starts with two path separators
715fn parseUNC(comptime T: type, path: []const T) WindowsUNC(T) {
716 assert(path.len >= 2 and PathType.windows.isSep(T, path[0]) and PathType.windows.isSep(T, path[1]));
717 const any_sep = switch (T) {
718 u8 => "/\\",
719 u16 => std.unicode.wtf8ToWtf16LeStringLiteral("/\\"),
720 else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) are supported"),
721 };
722 // For the server, the first path separator after the initial two is always
723 // the terminator of the server name, even if that means the server name is
724 // zero-length.
725 const server_end = mem.indexOfAnyPos(T, path, 2, any_sep) orelse return .{
726 .server = path[2..path.len],
727 .sep_after_server = false,
728 .share = path[path.len..path.len],
729 .sep_after_share = false,
730 };
731 // For the share, there can be any number of path separators between the server
732 // and the share, so we want to skip over all of them instead of just looking for
733 // the first one.
734 var it = std.mem.tokenizeAny(T, path[server_end + 1 ..], any_sep);
735 const share = it.next() orelse return .{
736 .server = path[2..server_end],
737 .sep_after_server = true,
738 .share = path[server_end + 1 .. server_end + 1],
739 .sep_after_share = false,
740 };
741 return .{
742 .server = path[2..server_end],
743 .sep_after_server = true,
744 .share = share,
745 .sep_after_share = it.index != it.buffer.len,
746 };
747}
486748
487 return windows.eqlIgnoreCaseWtf8(it1.next().?, it2.next().?);
749test parseUNC {
750 {
751 const unc = parseUNC(u8, "//");
752 try std.testing.expectEqualStrings("", unc.server);
753 try std.testing.expect(!unc.sep_after_server);
754 try std.testing.expectEqualStrings("", unc.share);
755 try std.testing.expect(!unc.sep_after_share);
756 }
757 {
758 const unc = parseUNC(u8, "\\\\s");
759 try std.testing.expectEqualStrings("s", unc.server);
760 try std.testing.expect(!unc.sep_after_server);
761 try std.testing.expectEqualStrings("", unc.share);
762 try std.testing.expect(!unc.sep_after_share);
763 }
764 {
765 const unc = parseUNC(u8, "\\\\s/");
766 try std.testing.expectEqualStrings("s", unc.server);
767 try std.testing.expect(unc.sep_after_server);
768 try std.testing.expectEqualStrings("", unc.share);
769 try std.testing.expect(!unc.sep_after_share);
770 }
771 {
772 const unc = parseUNC(u8, "\\/server\\share");
773 try std.testing.expectEqualStrings("server", unc.server);
774 try std.testing.expect(unc.sep_after_server);
775 try std.testing.expectEqualStrings("share", unc.share);
776 try std.testing.expect(!unc.sep_after_share);
777 }
778 {
779 const unc = parseUNC(u8, "/\\server\\share/");
780 try std.testing.expectEqualStrings("server", unc.server);
781 try std.testing.expect(unc.sep_after_server);
782 try std.testing.expectEqualStrings("share", unc.share);
783 try std.testing.expect(unc.sep_after_share);
784 }
785 {
786 const unc = parseUNC(u8, "\\\\server/\\share\\/");
787 try std.testing.expectEqualStrings("server", unc.server);
788 try std.testing.expect(unc.sep_after_server);
789 try std.testing.expectEqualStrings("share", unc.share);
790 try std.testing.expect(unc.sep_after_share);
791 }
792 {
793 const unc = parseUNC(u8, "\\\\server\\/\\\\");
794 try std.testing.expectEqualStrings("server", unc.server);
795 try std.testing.expect(unc.sep_after_server);
796 try std.testing.expectEqualStrings("", unc.share);
797 try std.testing.expect(!unc.sep_after_share);
798 }
488799}
489800
490fn compareDiskDesignators(kind: WindowsPath.Kind, p1: []const u8, p2: []const u8) bool {
801const DiskDesignatorKind = enum { drive, unc };
802
803/// `p1` and `p2` are both assumed to be the `kind` provided.
804fn compareDiskDesignators(comptime T: type, kind: DiskDesignatorKind, p1: []const T, p2: []const T) bool {
805 const eql = switch (T) {
806 u8 => windows.eqlIgnoreCaseWtf8,
807 u16 => windows.eqlIgnoreCaseWtf16,
808 else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) is supported"),
809 };
491810 switch (kind) {
492 WindowsPath.Kind.None => {
493 assert(p1.len == 0);
494 assert(p2.len == 0);
495 return true;
496 },
497 WindowsPath.Kind.Drive => {
498 return ascii.toUpper(p1[0]) == ascii.toUpper(p2[0]);
811 .drive => {
812 const drive_letter1 = getDriveLetter(T, p1);
813 const drive_letter2 = getDriveLetter(T, p2);
814
815 return eql(drive_letter1, drive_letter2);
499816 },
500 WindowsPath.Kind.NetworkShare => {
501 var it1 = mem.tokenizeAny(u8, p1, "/\\");
502 var it2 = mem.tokenizeAny(u8, p2, "/\\");
817 .unc => {
818 var unc1 = parseUNC(T, p1);
819 var unc2 = parseUNC(T, p2);
503820
504 return windows.eqlIgnoreCaseWtf8(it1.next().?, it2.next().?) and windows.eqlIgnoreCaseWtf8(it1.next().?, it2.next().?);
821 return eql(unc1.server, unc2.server) and
822 eql(unc1.share, unc2.share);
505823 },
506824 }
507825}
508826
827/// `path` is assumed to be drive-relative or drive-absolute.
828fn getDriveLetter(comptime T: type, path: []const T) []const T {
829 const len: usize = switch (T) {
830 // getWin32PathType will only return .drive_absolute/.drive_relative when there is
831 // (1) a valid code point, and (2) a code point < U+10000, so we only need to
832 // get the length determined by the first byte.
833 u8 => std.unicode.utf8ByteSequenceLength(path[0]) catch unreachable,
834 u16 => 1,
835 else => @compileError("unsupported type: " ++ @typeName(T)),
836 };
837 return path[0..len];
838}
839
840test compareDiskDesignators {
841 try testCompareDiskDesignators(true, .drive, "c:", "C:\\");
842 try testCompareDiskDesignators(true, .drive, "C:\\", "C:");
843 try testCompareDiskDesignators(false, .drive, "C:\\", "D:\\");
844 // Case-insensitivity technically applies to non-ASCII drive letters
845 try testCompareDiskDesignators(true, .drive, "λ:\\", "Λ:");
846
847 try testCompareDiskDesignators(true, .unc, "\\\\server", "//server//");
848 try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share");
849 try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share\\\\foo");
850 try testCompareDiskDesignators(false, .unc, "\\\\server\\sharefoo", "/\\server/share\\foo");
851 try testCompareDiskDesignators(false, .unc, "\\\\serverfoo\\\\share", "//server/share");
852 try testCompareDiskDesignators(false, .unc, "\\\\server\\", "//server/share");
853}
854
855fn testCompareDiskDesignators(expected_result: bool, kind: DiskDesignatorKind, p1: []const u8, p2: []const u8) !void {
856 var wtf16_buf1: [256]u16 = undefined;
857 const w1_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf1, p1);
858 var wtf16_buf2: [256]u16 = undefined;
859 const w2_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf2, p2);
860 try std.testing.expectEqual(expected_result, compareDiskDesignators(u8, kind, p1, p2));
861 try std.testing.expectEqual(expected_result, compareDiskDesignators(u16, kind, wtf16_buf1[0..w1_len], wtf16_buf2[0..w2_len]));
862}
863
509864/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
510pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
865pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
511866 if (native_os == .windows) {
512867 return resolveWindows(allocator, paths);
513868 } else {
......@@ -516,184 +871,232 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
516871}
517872
518873/// This function is like a series of `cd` statements executed one after another.
519/// It resolves "." and "..", but will not convert relative path to absolute path, use std.fs.Dir.realpath instead.
520/// The result does not have a trailing path separator.
521/// Each drive has its own current working directory.
874/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
875/// an absolute path, use std.fs.Dir.realpath instead.
876/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.
522877/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
878///
879/// The result will not have a trailing path separator, except for the following scenarios:
880/// - The resolved path is drive-absolute with no components (e.g. `C:\`).
881/// - The resolved path is a UNC path with only a server name, and the input path contained a trailing separator
882/// (e.g. `\\server\`).
883/// - The resolved path is a UNC path with no components after the share name, and the input path contained a
884/// trailing separator (e.g. `\\server\share\`).
885///
886/// Each drive has its own current working directory, which is only resolved via the paths provided.
887/// In the scenario that the resolved path contains a drive-relative path that can't be resolved using the paths alone,
888/// the result will be a drive-relative path.
889/// Similarly, in the scenario that the resolved path contains a rooted path that can't be resolved using the paths alone,
890/// the result will be a rooted path.
891///
523892/// Note: all usage of this function should be audited due to the existence of symlinks.
524893/// Without performing actual syscalls, resolving `..` could be incorrect.
525894/// This API may break in the future: https://github.com/ziglang/zig/issues/13613
526pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {
527 assert(paths.len > 0);
528
529 // determine which disk designator we will result with, if any
530 var result_drive_buf = "_:".*;
531 var disk_designator: []const u8 = "";
532 var drive_kind = WindowsPath.Kind.None;
533 var have_abs_path = false;
534 var first_index: usize = 0;
535 for (paths, 0..) |p, i| {
536 const parsed = windowsParsePath(p);
537 if (parsed.is_abs) {
538 have_abs_path = true;
539 first_index = i;
895pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
896 // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
897 // (we use `* 3` because stackFallback uses 1 usize as a length)
898 var bit_set_allocator_state = std.heap.stackFallback(@sizeOf(usize) * 3, allocator);
899 const bit_set_allocator = bit_set_allocator_state.get();
900 var relevant_paths = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(bit_set_allocator, paths.len);
901 defer relevant_paths.deinit(bit_set_allocator);
902
903 // Iterate the paths backwards, marking the relevant paths along the way.
904 // This also allows us to break from the loop whenever any earlier paths are known to be irrelevant.
905 var first_path_i: usize = paths.len;
906 const effective_root_path: WindowsPath2(u8) = root: {
907 var last_effective_root_path: WindowsPath2(u8) = .{ .kind = .relative, .root = "" };
908 var last_rooted_path_i: ?usize = null;
909 var last_drive_relative_path_i: usize = undefined;
910 while (first_path_i > 0) {
911 first_path_i -= 1;
912 const parsed = parsePathWindows(u8, paths[first_path_i]);
913 switch (parsed.kind) {
914 .unc_absolute, .root_local_device, .local_device => {
915 switch (last_effective_root_path.kind) {
916 .rooted => {},
917 .drive_relative => continue,
918 else => {
919 relevant_paths.set(first_path_i);
920 },
921 }
922 break :root parsed;
923 },
924 .drive_relative, .drive_absolute => {
925 switch (last_effective_root_path.kind) {
926 .drive_relative => if (!compareDiskDesignators(u8, .drive, parsed.root, last_effective_root_path.root)) {
927 continue;
928 } else if (last_rooted_path_i != null) {
929 break :root .{ .kind = .drive_absolute, .root = parsed.root };
930 },
931 .relative => last_effective_root_path = parsed,
932 .rooted => {
933 // This is the end of the line, since the rooted path will always be relative
934 // to this drive letter, and even if the current path is drive-relative, the
935 // rooted-ness makes that irrelevant.
936 //
937 // Therefore, force the kind of the effective root to be drive-absolute in order to
938 // properly resolve a rooted path against a drive-relative one, as the result should
939 // always be drive-absolute.
940 break :root .{ .kind = .drive_absolute, .root = parsed.root };
941 },
942 .drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
943 }
944 relevant_paths.set(first_path_i);
945 last_drive_relative_path_i = first_path_i;
946 if (parsed.kind == .drive_absolute) {
947 break :root parsed;
948 }
949 },
950 .relative => {
951 switch (last_effective_root_path.kind) {
952 .rooted => continue,
953 .relative => last_effective_root_path = parsed,
954 else => {},
955 }
956 relevant_paths.set(first_path_i);
957 },
958 .rooted => {
959 switch (last_effective_root_path.kind) {
960 .drive_relative => {},
961 .relative => last_effective_root_path = parsed,
962 .rooted => continue,
963 .drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
964 }
965 if (last_rooted_path_i == null) {
966 last_rooted_path_i = first_path_i;
967 relevant_paths.set(first_path_i);
968 }
969 },
970 }
540971 }
541 switch (parsed.kind) {
542 .Drive => {
543 result_drive_buf[0] = ascii.toUpper(parsed.disk_designator[0]);
544 disk_designator = result_drive_buf[0..];
545 drive_kind = WindowsPath.Kind.Drive;
546 },
547 .NetworkShare => {
548 disk_designator = parsed.disk_designator;
549 drive_kind = WindowsPath.Kind.NetworkShare;
550 },
551 .None => {},
972 // After iterating, if the pending effective root is drive-relative then that means
973 // nothing has led to forcing a drive-absolute root (a path that allows resolving the
974 // drive-specific CWD would cause an early break), so we now need to ignore all paths
975 // before the most recent drive-relative one. For example, if we're resolving
976 // { "\\rooted", "relative", "C:drive-relative" }
977 // then the `\rooted` and `relative` needs to be ignored since we can't
978 // know what the rooted path is rooted against as that'd require knowing the CWD.
979 if (last_effective_root_path.kind == .drive_relative) {
980 for (0..last_drive_relative_path_i) |i| {
981 relevant_paths.unset(i);
982 }
552983 }
553 }
984 break :root last_effective_root_path;
985 };
554986
555 // if we will result with a disk designator, loop again to determine
556 // which is the last time the disk designator is absolutely specified, if any
557 // and count up the max bytes for paths related to this disk designator
558 if (drive_kind != WindowsPath.Kind.None) {
559 have_abs_path = false;
560 first_index = 0;
561 var correct_disk_designator = false;
562
563 for (paths, 0..) |p, i| {
564 const parsed = windowsParsePath(p);
565 if (parsed.kind != WindowsPath.Kind.None) {
566 if (parsed.kind == drive_kind) {
567 correct_disk_designator = compareDiskDesignators(drive_kind, disk_designator, parsed.disk_designator);
568 } else {
569 continue;
570 }
987 var result: std.ArrayList(u8) = .empty;
988 defer result.deinit(allocator);
989
990 var want_path_sep_between_root_and_component = false;
991 switch (effective_root_path.kind) {
992 .root_local_device, .local_device => {
993 try result.ensureUnusedCapacity(allocator, 3);
994 result.appendSliceAssumeCapacity("\\\\");
995 result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
996 want_path_sep_between_root_and_component = true;
997 },
998 .drive_absolute, .drive_relative => {
999 try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);
1000 result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
1001 result.appendAssumeCapacity(':');
1002 if (effective_root_path.kind == .drive_absolute) {
1003 result.appendAssumeCapacity('\\');
5711004 }
572 if (!correct_disk_designator) {
573 continue;
1005 },
1006 .unc_absolute => {
1007 const unc = parseUNC(u8, effective_root_path.root);
1008
1009 const root_len = len: {
1010 var len: usize = 2 + unc.server.len + unc.share.len;
1011 if (unc.sep_after_server) len += 1;
1012 if (unc.sep_after_share) len += 1;
1013 break :len len;
1014 };
1015 try result.ensureUnusedCapacity(allocator, root_len);
1016 result.appendSliceAssumeCapacity("\\\\");
1017 if (unc.server.len > 0 or unc.sep_after_server) {
1018 result.appendSliceAssumeCapacity(unc.server);
1019 if (unc.sep_after_server)
1020 result.appendAssumeCapacity('\\')
1021 else
1022 want_path_sep_between_root_and_component = true;
5741023 }
575 if (parsed.is_abs) {
576 first_index = i;
577 have_abs_path = true;
1024 if (unc.share.len > 0) {
1025 result.appendSliceAssumeCapacity(unc.share);
1026 if (unc.sep_after_share)
1027 result.appendAssumeCapacity('\\')
1028 else
1029 want_path_sep_between_root_and_component = true;
5781030 }
579 }
1031 },
1032 .rooted => {
1033 try result.append(allocator, '\\');
1034 },
1035 .relative => {},
5801036 }
5811037
582 // Allocate result and fill in the disk designator.
583 var result = std.array_list.Managed(u8).init(allocator);
584 defer result.deinit();
585
586 const disk_designator_len: usize = l: {
587 if (!have_abs_path) break :l 0;
588 switch (drive_kind) {
589 .Drive => {
590 try result.appendSlice(disk_designator);
591 break :l disk_designator.len;
592 },
593 .NetworkShare => {
594 var it = mem.tokenizeAny(u8, paths[first_index], "/\\");
595 const server_name = it.next().?;
596 const other_name = it.next().?;
597
598 try result.ensureUnusedCapacity(2 + 1 + server_name.len + other_name.len);
599 result.appendSliceAssumeCapacity("\\\\");
600 result.appendSliceAssumeCapacity(server_name);
601 result.appendAssumeCapacity('\\');
602 result.appendSliceAssumeCapacity(other_name);
603
604 break :l result.items.len;
605 },
606 .None => {
607 break :l 1;
608 },
609 }
610 };
611
612 var correct_disk_designator = true;
1038 const root_len = result.items.len;
6131039 var negative_count: usize = 0;
1040 for (paths[first_path_i..], first_path_i..) |path, i| {
1041 if (!relevant_paths.isSet(i)) continue;
6141042
615 for (paths[first_index..]) |p| {
616 const parsed = windowsParsePath(p);
617
618 if (parsed.kind != .None) {
619 if (parsed.kind == drive_kind) {
620 const dd = result.items[0..disk_designator_len];
621 correct_disk_designator = compareDiskDesignators(drive_kind, dd, parsed.disk_designator);
622 } else {
623 continue;
624 }
625 }
626 if (!correct_disk_designator) {
627 continue;
628 }
629 var it = mem.tokenizeAny(u8, p[parsed.disk_designator.len..], "/\\");
1043 const parsed = parsePathWindows(u8, path);
1044 const skip_len = parsed.root.len;
1045 var it = mem.tokenizeAny(u8, path[skip_len..], "/\\");
6301046 while (it.next()) |component| {
6311047 if (mem.eql(u8, component, ".")) {
6321048 continue;
6331049 } else if (mem.eql(u8, component, "..")) {
634 if (result.items.len == 0) {
1050 if (result.items.len == 0 or (result.items.len == root_len and effective_root_path.kind == .drive_relative)) {
6351051 negative_count += 1;
6361052 continue;
6371053 }
6381054 while (true) {
639 if (result.items.len == disk_designator_len) {
1055 if (result.items.len == root_len) {
6401056 break;
6411057 }
642 const end_with_sep = switch (result.items[result.items.len - 1]) {
643 '\\', '/' => true,
644 else => false,
645 };
1058 const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
6461059 result.items.len -= 1;
647 if (end_with_sep or result.items.len == 0) break;
1060 if (end_with_sep) break;
6481061 }
649 } else if (!have_abs_path and result.items.len == 0) {
650 try result.appendSlice(component);
1062 } else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {
1063 try result.appendSlice(allocator, component);
6511064 } else {
652 try result.ensureUnusedCapacity(1 + component.len);
1065 try result.ensureUnusedCapacity(allocator, 1 + component.len);
6531066 result.appendAssumeCapacity('\\');
6541067 result.appendSliceAssumeCapacity(component);
6551068 }
6561069 }
6571070 }
6581071
659 if (disk_designator_len != 0 and result.items.len == disk_designator_len) {
660 try result.append('\\');
661 return result.toOwnedSlice();
1072 if (root_len != 0 and result.items.len == root_len and negative_count == 0) {
1073 return result.toOwnedSlice(allocator);
6621074 }
6631075
664 if (result.items.len == 0) {
1076 if (result.items.len == root_len) {
6651077 if (negative_count == 0) {
6661078 return allocator.dupe(u8, ".");
667 } else {
668 const real_result = try allocator.alloc(u8, 3 * negative_count - 1);
669 var count = negative_count - 1;
670 var i: usize = 0;
671 while (count > 0) : (count -= 1) {
672 real_result[i..][0..3].* = "..\\".*;
673 i += 3;
674 }
675 real_result[i..][0..2].* = "..".*;
676 return real_result;
6771079 }
678 }
6791080
680 if (negative_count == 0) {
681 return result.toOwnedSlice();
1081 try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);
1082 for (0..negative_count - 1) |_| {
1083 result.appendSliceAssumeCapacity("..\\");
1084 }
1085 result.appendSliceAssumeCapacity("..");
6821086 } else {
683 const real_result = try allocator.alloc(u8, 3 * negative_count + result.items.len);
684 var count = negative_count;
685 var i: usize = 0;
686 while (count > 0) : (count -= 1) {
687 real_result[i..][0..3].* = "..\\".*;
688 i += 3;
1087 const dest = try result.addManyAt(allocator, root_len, 3 * negative_count);
1088 for (0..negative_count) |i| {
1089 dest[i * 3 ..][0..3].* = "..\\".*;
6891090 }
690 @memcpy(real_result[i..][0..result.items.len], result.items);
691 return real_result;
6921091 }
1092
1093 return result.toOwnedSlice(allocator);
6931094}
6941095
6951096/// This function is like a series of `cd` statements executed one after another.
696/// It resolves "." and "..", but will not convert relative path to absolute path, use std.fs.Dir.realpath instead.
1097/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
1098/// an absolute path, use std.fs.Dir.realpath instead.
1099/// ".." components may persist in the resolved path if the resolved path is relative.
6971100/// The result does not have a trailing path separator.
6981101/// This function does not perform any syscalls. Executing this series of path
6991102/// lookups on the actual filesystem may produce different results due to
......@@ -772,10 +1175,14 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
7721175}
7731176
7741177test resolve {
1178 try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");
1179 try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");
7751180 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
7761181 try testResolveWindows(&[_][]const u8{"."}, ".");
7771182 try testResolveWindows(&[_][]const u8{""}, ".");
7781183
1184 try testResolvePosix(&[_][]const u8{ "a", "../../.." }, "../..");
1185 try testResolvePosix(&[_][]const u8{ "..", "", "../../foo" }, "../../../foo");
7791186 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
7801187 try testResolvePosix(&[_][]const u8{"."}, ".");
7811188 try testResolvePosix(&[_][]const u8{""}, ".");
......@@ -792,22 +1199,81 @@ test resolveWindows {
7921199 );
7931200
7941201 try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok");
1202 try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c\\", ".\\..\\foo" }, "C:\\a\\b\\foo");
7951203 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");
7961204 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");
7971205 try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");
7981206 try testResolveWindows(&[_][]const u8{ "c:/ignore", "c:/some/file" }, "C:\\some\\file");
799 try testResolveWindows(&[_][]const u8{ "d:/ignore", "d:some/dir//" }, "D:\\ignore\\some\\dir");
1207 // The first path "sets" the CWD, so the drive-relative path is then relative to that.
1208 try testResolveWindows(&[_][]const u8{ "d:/foo", "d:some/dir//", "D:another" }, "D:\\foo\\some\\dir\\another");
8001209 try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
8011210 try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
802 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "C:\\");
803 try testResolveWindows(&[_][]const u8{ "c:/", "//dir" }, "C:\\dir");
804 try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share\\");
805 try testResolveWindows(&[_][]const u8{ "c:/", "//server//share" }, "\\\\server\\share\\");
806 try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "C:\\some\\dir");
1211 try testResolveWindows(&[_][]const u8{ "\\\\server/share/ignore", "//server/share/bar" }, "\\\\server\\share\\bar");
1212 try testResolveWindows(&[_][]const u8{ "\\/server\\share/", "..", "relative" }, "\\\\server\\share\\relative");
1213 try testResolveWindows(&[_][]const u8{ "\\\\server\\share", "C:drive-relative" }, "C:drive-relative");
1214 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "\\\\");
1215 try testResolveWindows(&[_][]const u8{ "c:/", "//server" }, "\\\\server");
1216 try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share");
1217 try testResolveWindows(&[_][]const u8{ "c:/", "//server//share////" }, "\\\\server\\share\\");
1218 try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "\\\\\\some\\dir");
1219 try testResolveWindows(&[_][]const u8{ "c:foo", "bar" }, "C:foo\\bar");
8071220 try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js");
1221 // Drive-relative stays drive-relative if there's nothing to provide the drive-specific CWD
1222 try testResolveWindows(&[_][]const u8{ "relative", "d:foo" }, "D:foo");
1223 try testResolveWindows(&[_][]const u8{ "../..\\..", "d:foo" }, "D:foo");
1224 try testResolveWindows(&[_][]const u8{ "../..\\..", "\\rooted", "d:foo" }, "D:foo");
1225 try testResolveWindows(&[_][]const u8{ "C:\\foo", "../..\\..", "\\rooted", "d:foo" }, "D:foo");
1226 try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "d:foo" }, "D:..\\..\\foo");
1227 try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:..\\..\\foo");
1228 try testResolveWindows(&[_][]const u8{ "ignored", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:foo");
1229 // Rooted paths remain rooted if there's no absolute path available to resolve the "root"
1230 try testResolveWindows(&[_][]const u8{ "/foo", "bar" }, "\\foo\\bar");
1231 // Rooted against a UNC path
1232 try testResolveWindows(&[_][]const u8{ "//server/share/ignore", "/foo", "bar" }, "\\\\server\\share\\foo\\bar");
1233 try testResolveWindows(&[_][]const u8{ "//server/share/", "/foo" }, "\\\\server\\share\\foo");
1234 try testResolveWindows(&[_][]const u8{ "//server/share", "/foo" }, "\\\\server\\share\\foo");
1235 try testResolveWindows(&[_][]const u8{ "//server/", "/foo" }, "\\\\server\\foo");
1236 try testResolveWindows(&[_][]const u8{ "//server", "/foo" }, "\\\\server\\foo");
1237 try testResolveWindows(&[_][]const u8{ "//", "/foo" }, "\\\\foo");
1238 // Rooted against a drive-relative path
1239 try testResolveWindows(&[_][]const u8{ "C:", "/foo", "bar" }, "C:\\foo\\bar");
1240 try testResolveWindows(&[_][]const u8{ "C:\\ignore", "C:", "/foo", "bar" }, "C:\\foo\\bar");
1241 try testResolveWindows(&[_][]const u8{ "C:\\ignore", "\\foo", "C:bar" }, "C:\\foo\\bar");
1242 // Only the last rooted path is relevant
1243 try testResolveWindows(&[_][]const u8{ "\\ignore", "\\foo" }, "\\foo");
1244 try testResolveWindows(&[_][]const u8{ "c:ignore", "ignore", "\\ignore", "\\foo" }, "C:\\foo");
1245 // Rooted is only relevant to a drive-relative if there's a previous drive-* path
1246 try testResolveWindows(&[_][]const u8{ "\\ignore", "C:foo" }, "C:foo");
1247 try testResolveWindows(&[_][]const u8{ "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
1248 try testResolveWindows(&[_][]const u8{ "c:ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
1249 try testResolveWindows(&[_][]const u8{ "c:\\ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
1250 try testResolveWindows(&[_][]const u8{ "d:\\ignore", "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
1251 // Root local device paths
1252 try testResolveWindows(&[_][]const u8{"\\/."}, "\\\\.");
1253 try testResolveWindows(&[_][]const u8{ "\\/.", "C:drive-relative" }, "C:drive-relative");
1254 try testResolveWindows(&[_][]const u8{"/\\?"}, "\\\\?");
1255 try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\.", "foo" }, "\\\\.\\foo");
1256 try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "foo" }, "\\\\?\\foo");
1257 try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "//.", "ignore", "\\foo" }, "\\\\.\\foo");
1258 try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "ignore", "\\foo" }, "\\\\?\\foo");
8081259
8091260 // Keep relative paths relative.
8101261 try testResolveWindows(&[_][]const u8{"a/b"}, "a\\b");
1262 try testResolveWindows(&[_][]const u8{".."}, "..");
1263 try testResolveWindows(&[_][]const u8{"../.."}, "..\\..");
1264 try testResolveWindows(&[_][]const u8{ "C:foo", "../.." }, "C:..");
1265 try testResolveWindows(&[_][]const u8{ "d:foo", "../..\\.." }, "D:..\\..");
1266
1267 // Local device paths treat the \\.\ or \\?\ as the "root", everything afterwards is treated as a regular component.
1268 try testResolveWindows(&[_][]const u8{ "\\\\?\\C:\\foo", "../bar", "baz" }, "\\\\?\\C:\\bar\\baz");
1269 try testResolveWindows(&[_][]const u8{ "\\\\.\\C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
1270 try testResolveWindows(&[_][]const u8{ "//./C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
1271 try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", ".." }, "\\\\.");
1272 try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", "..\\.." }, "\\\\.");
1273
1274 // Paths are assumed to be Win32, so paths that are likely NT paths are treated as a rooted path.
1275 try testResolveWindows(&[_][]const u8{ "\\??\\C:\\foo", "/bar", "baz" }, "\\bar\\baz");
1276 try testResolveWindows(&[_][]const u8{ "C:\\", "\\??\\C:\\foo", "bar" }, "C:\\??\\C:\\foo\\bar");
8111277}
8121278
8131279test resolvePosix {
......@@ -855,63 +1321,18 @@ pub fn dirname(path: []const u8) ?[]const u8 {
8551321}
8561322
8571323pub fn dirnameWindows(path: []const u8) ?[]const u8 {
858 if (path.len == 0)
859 return null;
860
861 const root_slice = diskDesignatorWindows(path);
862 if (path.len == root_slice.len)
863 return null;
864
865 const have_root_slash = path.len > root_slice.len and (path[root_slice.len] == '/' or path[root_slice.len] == '\\');
866
867 var end_index: usize = path.len - 1;
868
869 while (path[end_index] == '/' or path[end_index] == '\\') {
870 if (end_index == 0)
871 return null;
872 end_index -= 1;
873 }
874
875 while (path[end_index] != '/' and path[end_index] != '\\') {
876 if (end_index == 0)
877 return null;
878 end_index -= 1;
879 }
880
881 if (have_root_slash and end_index == root_slice.len) {
882 end_index += 1;
883 }
884
885 if (end_index == 0)
886 return null;
887
888 return path[0..end_index];
1324 return dirnameInner(.windows, path);
8891325}
8901326
8911327pub fn dirnamePosix(path: []const u8) ?[]const u8 {
892 if (path.len == 0)
893 return null;
894
895 var end_index: usize = path.len - 1;
896 while (path[end_index] == '/') {
897 if (end_index == 0)
898 return null;
899 end_index -= 1;
900 }
901
902 while (path[end_index] != '/') {
903 if (end_index == 0)
904 return null;
905 end_index -= 1;
906 }
907
908 if (end_index == 0 and path[0] == '/')
909 return path[0..1];
910
911 if (end_index == 0)
912 return null;
1328 return dirnameInner(.posix, path);
1329}
9131330
914 return path[0..end_index];
1331fn dirnameInner(comptime path_type: PathType, path: []const u8) ?[]const u8 {
1332 var it = ComponentIterator(path_type, u8).init(path);
1333 _ = it.last() orelse return null;
1334 const up = it.previous() orelse return it.root();
1335 return up.path;
9151336}
9161337
9171338test dirnamePosix {
......@@ -930,11 +1351,12 @@ test dirnamePosix {
9301351
9311352test dirnameWindows {
9321353 try testDirnameWindows("c:\\", null);
1354 try testDirnameWindows("c:\\\\", null);
9331355 try testDirnameWindows("c:\\foo", "c:\\");
934 try testDirnameWindows("c:\\foo\\", "c:\\");
1356 try testDirnameWindows("c:\\\\foo\\", "c:\\");
9351357 try testDirnameWindows("c:\\foo\\bar", "c:\\foo");
9361358 try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");
937 try testDirnameWindows("c:\\foo\\bar\\baz", "c:\\foo\\bar");
1359 try testDirnameWindows("c:\\\\foo\\bar\\baz", "c:\\\\foo\\bar");
9381360 try testDirnameWindows("\\", null);
9391361 try testDirnameWindows("\\foo", "\\");
9401362 try testDirnameWindows("\\foo\\", "\\");
......@@ -942,19 +1364,30 @@ test dirnameWindows {
9421364 try testDirnameWindows("\\foo\\bar\\", "\\foo");
9431365 try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
9441366 try testDirnameWindows("c:", null);
945 try testDirnameWindows("c:foo", null);
946 try testDirnameWindows("c:foo\\", null);
1367 try testDirnameWindows("c:foo", "c:");
1368 try testDirnameWindows("c:foo\\", "c:");
9471369 try testDirnameWindows("c:foo\\bar", "c:foo");
9481370 try testDirnameWindows("c:foo\\bar\\", "c:foo");
9491371 try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
9501372 try testDirnameWindows("file:stream", null);
9511373 try testDirnameWindows("dir\\file:stream", "dir");
9521374 try testDirnameWindows("\\\\unc\\share", null);
1375 try testDirnameWindows("\\\\unc\\share\\\\", null);
9531376 try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
9541377 try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
9551378 try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
9561379 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
9571380 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");
1381 try testDirnameWindows("\\\\.", null);
1382 try testDirnameWindows("\\\\.\\", null);
1383 try testDirnameWindows("\\\\.\\device", "\\\\.\\");
1384 try testDirnameWindows("\\\\.\\device\\", "\\\\.\\");
1385 try testDirnameWindows("\\\\.\\device\\foo", "\\\\.\\device");
1386 try testDirnameWindows("\\\\?", null);
1387 try testDirnameWindows("\\\\?\\", null);
1388 try testDirnameWindows("\\\\?\\device", "\\\\?\\");
1389 try testDirnameWindows("\\\\?\\device\\", "\\\\?\\");
1390 try testDirnameWindows("\\\\?\\device\\foo", "\\\\?\\device");
9581391 try testDirnameWindows("/a/b/", "/a");
9591392 try testDirnameWindows("/a/b", "/a");
9601393 try testDirnameWindows("/a", "/");
......@@ -974,7 +1407,7 @@ fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {
9741407
9751408fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {
9761409 if (dirnameWindows(input)) |output| {
977 try testing.expect(mem.eql(u8, output, expected_output.?));
1410 try testing.expectEqualStrings(expected_output.?, output);
9781411 } else {
9791412 try testing.expect(expected_output == null);
9801413 }
......@@ -989,56 +1422,17 @@ pub fn basename(path: []const u8) []const u8 {
9891422}
9901423
9911424pub fn basenamePosix(path: []const u8) []const u8 {
992 if (path.len == 0)
993 return &[_]u8{};
994
995 var end_index: usize = path.len - 1;
996 while (path[end_index] == '/') {
997 if (end_index == 0)
998 return &[_]u8{};
999 end_index -= 1;
1000 }
1001 var start_index: usize = end_index;
1002 end_index += 1;
1003 while (path[start_index] != '/') {
1004 if (start_index == 0)
1005 return path[0..end_index];
1006 start_index -= 1;
1007 }
1008
1009 return path[start_index + 1 .. end_index];
1425 return basenameInner(.posix, path);
10101426}
10111427
10121428pub fn basenameWindows(path: []const u8) []const u8 {
1013 if (path.len == 0)
1014 return &[_]u8{};
1015
1016 var end_index: usize = path.len - 1;
1017 while (true) {
1018 const byte = path[end_index];
1019 if (byte == '/' or byte == '\\') {
1020 if (end_index == 0)
1021 return &[_]u8{};
1022 end_index -= 1;
1023 continue;
1024 }
1025 if (byte == ':' and end_index == 1) {
1026 return &[_]u8{};
1027 }
1028 break;
1029 }
1030
1031 var start_index: usize = end_index;
1032 end_index += 1;
1033 while (path[start_index] != '/' and path[start_index] != '\\' and
1034 !(path[start_index] == ':' and start_index == 1))
1035 {
1036 if (start_index == 0)
1037 return path[0..end_index];
1038 start_index -= 1;
1039 }
1429 return basenameInner(.windows, path);
1430}
10401431
1041 return path[start_index + 1 .. end_index];
1432fn basenameInner(comptime path_type: PathType, path: []const u8) []const u8 {
1433 var it = ComponentIterator(path_type, u8).init(path);
1434 const last = it.last() orelse return &[_]u8{};
1435 return last.name;
10421436}
10431437
10441438test basename {
......@@ -1053,7 +1447,9 @@ test basename {
10531447 try testBasename("/aaa/", "aaa");
10541448 try testBasename("/aaa/b", "b");
10551449 try testBasename("/a/b", "b");
1056 try testBasename("//a", "a");
1450
1451 // For Windows, this is a UNC path that only has a server name component.
1452 try testBasename("//a", if (native_os == .windows) "" else "a");
10571453
10581454 try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
10591455 try testBasenamePosix("\\basename.ext", "\\basename.ext");
......@@ -1076,6 +1472,12 @@ test basename {
10761472 try testBasenameWindows("C:basename.ext", "basename.ext");
10771473 try testBasenameWindows("C:basename.ext\\", "basename.ext");
10781474 try testBasenameWindows("C:basename.ext\\\\", "basename.ext");
1475 try testBasenameWindows("\\\\.", "");
1476 try testBasenameWindows("\\\\.\\", "");
1477 try testBasenameWindows("\\\\.\\basename.ext", "basename.ext");
1478 try testBasenameWindows("\\\\?", "");
1479 try testBasenameWindows("\\\\?\\", "");
1480 try testBasenameWindows("\\\\?\\basename.ext", "basename.ext");
10791481 try testBasenameWindows("C:foo", "foo");
10801482 try testBasenameWindows("file:stream", "file:stream");
10811483}
......@@ -1092,11 +1494,15 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
10921494 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
10931495}
10941496
1497pub const RelativeError = std.process.GetCwdAllocError;
1498
10951499/// Returns the relative path from `from` to `to`. If `from` and `to` each
10961500/// resolve to the same path (after calling `resolve` on each), a zero-length
10971501/// string is returned.
1098/// On Windows this canonicalizes the drive to a capital letter and paths to `\\`.
1099pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1502/// On Windows, the result is not guaranteed to be relative, as the paths may be
1503/// on different volumes. In that case, the result will be the canonicalized absolute
1504/// path of `to`.
1505pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) RelativeError![]u8 {
11001506 if (native_os == .windows) {
11011507 return relativeWindows(allocator, from, to);
11021508 } else {
......@@ -1105,30 +1511,53 @@ pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
11051511}
11061512
11071513pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1108 const cwd = try process.getCwdAlloc(allocator);
1109 defer allocator.free(cwd);
1110 const resolved_from = try resolveWindows(allocator, &[_][]const u8{ cwd, from });
1111 defer allocator.free(resolved_from);
1514 if (native_os != .windows) @compileError("this function relies on Windows-specific semantics");
11121515
1516 const parsed_from = parsePathWindows(u8, from);
1517 const parsed_to = parsePathWindows(u8, to);
1518
1519 const result_is_always_to = x: {
1520 if (parsed_from.kind != parsed_to.kind) {
1521 break :x false;
1522 }
1523 switch (parsed_from.kind) {
1524 .drive_relative, .drive_absolute => {
1525 break :x !compareDiskDesignators(u8, .drive, parsed_from.root, parsed_to.root);
1526 },
1527 .unc_absolute => {
1528 break :x !compareDiskDesignators(u8, .unc, parsed_from.root, parsed_to.root);
1529 },
1530 .relative, .rooted, .local_device => break :x false,
1531 .root_local_device => break :x true,
1532 }
1533 };
1534
1535 if (result_is_always_to) {
1536 return windowsResolveAgainstCwd(allocator, to, parsed_to);
1537 }
1538
1539 const resolved_from = try windowsResolveAgainstCwd(allocator, from, parsed_from);
1540 defer allocator.free(resolved_from);
11131541 var clean_up_resolved_to = true;
1114 const resolved_to = try resolveWindows(allocator, &[_][]const u8{ cwd, to });
1542 const resolved_to = try windowsResolveAgainstCwd(allocator, to, parsed_to);
11151543 defer if (clean_up_resolved_to) allocator.free(resolved_to);
11161544
1117 const parsed_from = windowsParsePath(resolved_from);
1118 const parsed_to = windowsParsePath(resolved_to);
1545 const parsed_resolved_from = parsePathWindows(u8, resolved_from);
1546 const parsed_resolved_to = parsePathWindows(u8, resolved_to);
1547
11191548 const result_is_to = x: {
1120 if (parsed_from.kind != parsed_to.kind) {
1549 if (parsed_resolved_from.kind != parsed_resolved_to.kind) {
11211550 break :x true;
1122 } else switch (parsed_from.kind) {
1123 .NetworkShare => {
1124 break :x !networkShareServersEql(parsed_to.disk_designator, parsed_from.disk_designator);
1125 },
1126 .Drive => {
1127 break :x ascii.toUpper(parsed_from.disk_designator[0]) != ascii.toUpper(parsed_to.disk_designator[0]);
1551 }
1552 switch (parsed_resolved_from.kind) {
1553 .drive_absolute, .drive_relative => {
1554 break :x !compareDiskDesignators(u8, .drive, parsed_resolved_from.root, parsed_resolved_to.root);
11281555 },
1129 .None => {
1130 break :x false;
1556 .unc_absolute => {
1557 break :x !compareDiskDesignators(u8, .unc, parsed_resolved_from.root, parsed_resolved_to.root);
11311558 },
1559 .relative, .rooted, .local_device => break :x false,
1560 .root_local_device => break :x true,
11321561 }
11331562 };
11341563
......@@ -1137,8 +1566,8 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !
11371566 return resolved_to;
11381567 }
11391568
1140 var from_it = mem.tokenizeAny(u8, resolved_from, "/\\");
1141 var to_it = mem.tokenizeAny(u8, resolved_to, "/\\");
1569 var from_it = mem.tokenizeAny(u8, resolved_from[parsed_resolved_from.root.len..], "/\\");
1570 var to_it = mem.tokenizeAny(u8, resolved_to[parsed_resolved_to.root.len..], "/\\");
11421571 while (true) {
11431572 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
11441573 const to_rest = to_it.rest();
......@@ -1170,11 +1599,101 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !
11701599
11711600 return allocator.realloc(result, result_index);
11721601 }
1173
11741602 return [_]u8{};
11751603}
11761604
1605fn windowsResolveAgainstCwd(allocator: Allocator, path: []const u8, parsed: WindowsPath2(u8)) ![]u8 {
1606 // Space for 256 WTF-16 code units; potentially 3 WTF-8 bytes per WTF-16 code unit
1607 var temp_allocator_state = std.heap.stackFallback(256 * 3, allocator);
1608 return switch (parsed.kind) {
1609 .drive_absolute,
1610 .unc_absolute,
1611 .root_local_device,
1612 .local_device,
1613 => try resolveWindows(allocator, &.{path}),
1614 .relative => blk: {
1615 const temp_allocator = temp_allocator_state.get();
1616
1617 const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1618 const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1619
1620 const wtf8_len = std.unicode.calcWtf8Len(cwd_w);
1621 const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1622 defer temp_allocator.free(wtf8_buf);
1623 assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len);
1624
1625 break :blk try resolveWindows(allocator, &.{ wtf8_buf, path });
1626 },
1627 .rooted => blk: {
1628 const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1629 const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1630 const parsed_cwd = parsePathWindows(u16, cwd_w);
1631 switch (parsed_cwd.kind) {
1632 .drive_absolute => {
1633 var drive_buf = "_:\\".*;
1634 drive_buf[0] = @truncate(cwd_w[0]);
1635 break :blk try resolveWindows(allocator, &.{ &drive_buf, path });
1636 },
1637 .unc_absolute => {
1638 const temp_allocator = temp_allocator_state.get();
1639 var root_buf = try temp_allocator.alloc(u8, parsed_cwd.root.len * 3);
1640 defer temp_allocator.free(root_buf);
1641
1642 const wtf8_len = std.unicode.wtf16LeToWtf8(root_buf, parsed_cwd.root);
1643 const root = root_buf[0..wtf8_len];
1644 break :blk try resolveWindows(allocator, &.{ root, path });
1645 },
1646 // Effectively a malformed CWD, give up and just return a normalized path
1647 else => break :blk try resolveWindows(allocator, &.{path}),
1648 }
1649 },
1650 .drive_relative => blk: {
1651 const temp_allocator = temp_allocator_state.get();
1652 const drive_cwd = drive_cwd: {
1653 const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1654 const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1655 const parsed_cwd = parsePathWindows(u16, cwd_w);
1656
1657 if (parsed_cwd.kind == .drive_absolute) {
1658 const drive_letter_w = parsed_cwd.root[0];
1659 const drive_letters_match = drive_letter_w <= 0x7F and
1660 ascii.toUpper(@intCast(drive_letter_w)) == ascii.toUpper(parsed.root[0]);
1661 if (drive_letters_match) {
1662 const wtf8_len = std.unicode.calcWtf8Len(cwd_w);
1663 const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1664 assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len);
1665 break :drive_cwd wtf8_buf[0..];
1666 }
1667
1668 // Per-drive CWD's are stored in special semi-hidden environment variables
1669 // of the format `=<drive-letter>:`, e.g. `=C:`. This type of CWD is
1670 // purely a shell concept, so there's no guarantee that it'll be set
1671 // or that it'll even be accurate.
1672 var key_buf = std.unicode.wtf8ToWtf16LeStringLiteral("=_:").*;
1673 key_buf[1] = parsed.root[0];
1674 if (std.process.getenvW(&key_buf)) |drive_cwd_w| {
1675 const wtf8_len = std.unicode.calcWtf8Len(drive_cwd_w);
1676 const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1677 assert(std.unicode.wtf16LeToWtf8(wtf8_buf, drive_cwd_w) == wtf8_len);
1678 break :drive_cwd wtf8_buf[0..];
1679 }
1680 }
1681
1682 const drive_buf = try temp_allocator.alloc(u8, 3);
1683 drive_buf[0] = parsed.root[0];
1684 drive_buf[1] = ':';
1685 drive_buf[2] = '\\';
1686 break :drive_cwd drive_buf;
1687 };
1688 defer temp_allocator.free(drive_cwd);
1689 break :blk try resolveWindows(allocator, &.{ drive_cwd, path });
1690 },
1691 };
1692}
1693
11771694pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1695 if (native_os == .windows) @compileError("this function relies on semantics that do not apply to Windows");
1696
11781697 const cwd = try process.getCwdAlloc(allocator);
11791698 defer allocator.free(cwd);
11801699 const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });
......@@ -1217,51 +1736,59 @@ pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]
12171736}
12181737
12191738test relative {
1220 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
1221 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
1222 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
1223 try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
1224 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
1225 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
1226 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
1227 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
1228 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
1229 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
1230 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
1231 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
1232 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
1233 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
1234 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
1235 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
1236 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
1237 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
1238 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
1239 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
1240 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz");
1241 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux");
1242 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
1243 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
1244
1245 try testRelativeWindows("a/b/c", "a\\b", "..");
1246 try testRelativeWindows("a/b/c", "a", "..\\..");
1247 try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");
1248
1249 try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
1250 // Unicode-aware case-insensitive path comparison
1251 try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");
1252
1253 try testRelativePosix("/var/lib", "/var", "..");
1254 try testRelativePosix("/var/lib", "/bin", "../../bin");
1255 try testRelativePosix("/var/lib", "/var/lib", "");
1256 try testRelativePosix("/var/lib", "/var/apache", "../apache");
1257 try testRelativePosix("/var/", "/var/lib", "lib");
1258 try testRelativePosix("/", "/var/lib", "var/lib");
1259 try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
1260 try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
1261 try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
1262 try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
1263 try testRelativePosix("/baz-quux", "/baz", "../baz");
1264 try testRelativePosix("/baz", "/baz-quux", "../baz-quux");
1739 if (native_os == .windows) {
1740 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
1741 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
1742 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
1743 try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
1744 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
1745 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
1746 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
1747 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
1748 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
1749 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
1750 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
1751 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
1752 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
1753 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
1754 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
1755 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
1756 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
1757 try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz");
1758 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
1759 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
1760 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
1761 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz");
1762 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux");
1763 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
1764 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
1765
1766 try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo");
1767 try testRelativeWindows("c:foo", "c:foo\\bar", "bar");
1768 try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo");
1769 try testRelativeWindows("\\foo", "\\foo\\bar", "bar");
1770
1771 try testRelativeWindows("a/b/c", "a\\b", "..");
1772 try testRelativeWindows("a/b/c", "a", "..\\..");
1773 try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");
1774
1775 try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
1776 // Unicode-aware case-insensitive path comparison
1777 try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");
1778 } else {
1779 try testRelativePosix("/var/lib", "/var", "..");
1780 try testRelativePosix("/var/lib", "/bin", "../../bin");
1781 try testRelativePosix("/var/lib", "/var/lib", "");
1782 try testRelativePosix("/var/lib", "/var/apache", "../apache");
1783 try testRelativePosix("/var/", "/var/lib", "lib");
1784 try testRelativePosix("/", "/var/lib", "var/lib");
1785 try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
1786 try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
1787 try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
1788 try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
1789 try testRelativePosix("/baz-quux", "/baz", "../baz");
1790 try testRelativePosix("/baz", "/baz-quux", "../baz-quux");
1791 }
12651792}
12661793
12671794fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
......@@ -1391,7 +1918,10 @@ test stem {
13911918pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
13921919 return struct {
13931920 path: []const T,
1394 root_end_index: usize = 0,
1921 /// Length of the root with at most one trailing path separator included (e.g. `C:/`).
1922 root_len: usize,
1923 /// Length of the root with all trailing path separators included (e.g. `C://///`).
1924 root_end_index: usize,
13951925 start_index: usize = 0,
13961926 end_index: usize = 0,
13971927
......@@ -1406,100 +1936,45 @@ pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
14061936 path: []const T,
14071937 };
14081938
1409 const InitError = switch (path_type) {
1410 .windows => error{BadPathName},
1411 else => error{},
1412 };
1413
14141939 /// After `init`, `next` will return the first component after the root
14151940 /// (there is no need to call `first` after `init`).
14161941 /// To iterate backwards (from the end of the path to the beginning), call `last`
14171942 /// after `init` and then iterate via `previous` calls.
1418 /// For Windows paths, `error.BadPathName` is returned if the `path` has an explicit
1419 /// namespace prefix (`\\.\`, `\\?\`, or `\??\`) or if it is a UNC path with more
1420 /// than two path separators at the beginning.
1421 pub fn init(path: []const T) InitError!Self {
1422 const root_end_index: usize = switch (path_type) {
1943 /// For Windows paths, paths are assumed to be in the Win32 namespace.
1944 pub fn init(path: []const T) Self {
1945 const root_len: usize = switch (path_type) {
14231946 .posix, .uefi => posix: {
14241947 // Root on UEFI and POSIX only differs by the path separator
1425 var root_end_index: usize = 0;
1426 while (true) : (root_end_index += 1) {
1427 if (root_end_index >= path.len or !path_type.isSep(T, path[root_end_index])) {
1428 break;
1429 }
1430 }
1431 break :posix root_end_index;
1948 break :posix if (path.len > 0 and path_type.isSep(T, path[0])) 1 else 0;
14321949 },
14331950 .windows => windows: {
1434 // Namespaces other than the Win32 file namespace are tricky
1435 // and basically impossible to determine a 'root' for, since it's
1436 // possible to construct an effectively arbitrarily long 'root',
1437 // e.g. `\\.\GLOBALROOT\??\UNC\localhost\C$\foo` is a
1438 // possible path that would be effectively equivalent to
1439 // `C:\foo`, and the `GLOBALROOT\??\` part can also be recursive,
1440 // so `GLOBALROOT\??\GLOBALROOT\??\...` would work for any number
1441 // of repetitions. Therefore, paths with an explicit namespace prefix
1442 // (\\.\, \??\, \\?\) are not allowed here.
1443 if (std.os.windows.getNamespacePrefix(T, path) != .none) {
1444 return error.BadPathName;
1445 }
1446 const windows_path_type = std.os.windows.getUnprefixedPathType(T, path);
1447 break :windows switch (windows_path_type) {
1448 .relative => 0,
1449 .root_local_device => path.len,
1450 .rooted => 1,
1451 .unc_absolute => unc: {
1452 var end_index: usize = 2;
1453 // Any extra separators between the first two and the server name are not allowed
1454 // and will always lead to STATUS_OBJECT_PATH_INVALID if it is attempted
1455 // to be used.
1456 if (end_index < path.len and path_type.isSep(T, path[end_index])) {
1457 return error.BadPathName;
1458 }
1459 // Server
1460 while (end_index < path.len and !path_type.isSep(T, path[end_index])) {
1461 end_index += 1;
1462 }
1463 // Slash(es) after server
1464 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1465 end_index += 1;
1466 }
1467 // Share
1468 while (end_index < path.len and !path_type.isSep(T, path[end_index])) {
1469 end_index += 1;
1470 }
1471 // Slash(es) after share
1472 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1473 end_index += 1;
1474 }
1475 break :unc end_index;
1476 },
1477 .drive_absolute => drive: {
1478 var end_index: usize = 3;
1479 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1480 end_index += 1;
1481 }
1482 break :drive end_index;
1483 },
1484 .drive_relative => 2,
1485 };
1951 break :windows parsePathWindows(T, path).root.len;
14861952 },
14871953 };
1954 // If there are repeated path separators directly after the root,
1955 // keep track of that info so that they don't have to be dealt with when
1956 // iterating components.
1957 var root_end_index = root_len;
1958 for (path[root_len..]) |c| {
1959 if (!path_type.isSep(T, c)) break;
1960 root_end_index += 1;
1961 }
14881962 return .{
14891963 .path = path,
1964 .root_len = root_len,
14901965 .root_end_index = root_end_index,
14911966 .start_index = root_end_index,
14921967 .end_index = root_end_index,
14931968 };
14941969 }
14951970
1496 /// Returns the root of the path if it is an absolute path, or null otherwise.
1971 /// Returns the root of the path if it is not a relative path, or null otherwise.
14971972 /// For POSIX paths, this will be `/`.
14981973 /// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.
14991974 /// For UEFI paths, this will be `\`.
15001975 pub fn root(self: Self) ?[]const T {
15011976 if (self.root_end_index == 0) return null;
1502 return self.path[0..self.root_end_index];
1977 return self.path[0..self.root_len];
15031978 }
15041979
15051980 /// Returns the first component (from the beginning of the path).
......@@ -1614,7 +2089,7 @@ pub const NativeComponentIterator = ComponentIterator(switch (native_os) {
16142089 else => .posix,
16152090}, u8);
16162091
1617pub fn componentIterator(path: []const u8) !NativeComponentIterator {
2092pub fn componentIterator(path: []const u8) NativeComponentIterator {
16182093 return NativeComponentIterator.init(path);
16192094}
16202095
......@@ -1622,8 +2097,9 @@ test "ComponentIterator posix" {
16222097 const PosixComponentIterator = ComponentIterator(.posix, u8);
16232098 {
16242099 const path = "a/b/c/";
1625 var it = try PosixComponentIterator.init(path);
1626 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);
2100 var it = PosixComponentIterator.init(path);
2101 try std.testing.expectEqual(0, it.root_len);
2102 try std.testing.expectEqual(0, it.root_end_index);
16272103 try std.testing.expect(null == it.root());
16282104 {
16292105 try std.testing.expect(null == it.previous());
......@@ -1669,8 +2145,9 @@ test "ComponentIterator posix" {
16692145
16702146 {
16712147 const path = "/a/b/c/";
1672 var it = try PosixComponentIterator.init(path);
1673 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);
2148 var it = PosixComponentIterator.init(path);
2149 try std.testing.expectEqual(1, it.root_len);
2150 try std.testing.expectEqual(1, it.root_end_index);
16742151 try std.testing.expectEqualStrings("/", it.root().?);
16752152 {
16762153 try std.testing.expect(null == it.previous());
......@@ -1714,10 +2191,59 @@ test "ComponentIterator posix" {
17142191 }
17152192 }
17162193
2194 {
2195 const path = "////a///b///c////";
2196 var it = PosixComponentIterator.init(path);
2197 try std.testing.expectEqual(1, it.root_len);
2198 try std.testing.expectEqual(4, it.root_end_index);
2199 try std.testing.expectEqualStrings("/", it.root().?);
2200 {
2201 try std.testing.expect(null == it.previous());
2202
2203 const first_via_next = it.next().?;
2204 try std.testing.expectEqualStrings("a", first_via_next.name);
2205 try std.testing.expectEqualStrings("////a", first_via_next.path);
2206
2207 const first = it.first().?;
2208 try std.testing.expectEqualStrings("a", first.name);
2209 try std.testing.expectEqualStrings("////a", first.path);
2210
2211 try std.testing.expect(null == it.previous());
2212
2213 const second = it.next().?;
2214 try std.testing.expectEqualStrings("b", second.name);
2215 try std.testing.expectEqualStrings("////a///b", second.path);
2216
2217 const third = it.next().?;
2218 try std.testing.expectEqualStrings("c", third.name);
2219 try std.testing.expectEqualStrings("////a///b///c", third.path);
2220
2221 try std.testing.expect(null == it.next());
2222 }
2223 {
2224 const last = it.last().?;
2225 try std.testing.expectEqualStrings("c", last.name);
2226 try std.testing.expectEqualStrings("////a///b///c", last.path);
2227
2228 try std.testing.expect(null == it.next());
2229
2230 const second_to_last = it.previous().?;
2231 try std.testing.expectEqualStrings("b", second_to_last.name);
2232 try std.testing.expectEqualStrings("////a///b", second_to_last.path);
2233
2234 const third_to_last = it.previous().?;
2235 try std.testing.expectEqualStrings("a", third_to_last.name);
2236 try std.testing.expectEqualStrings("////a", third_to_last.path);
2237
2238 try std.testing.expect(null == it.previous());
2239 }
2240 }
2241
17172242 {
17182243 const path = "/";
1719 var it = try PosixComponentIterator.init(path);
1720 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);
2244 var it = PosixComponentIterator.init(path);
2245 try std.testing.expectEqual(1, it.root_len);
2246 try std.testing.expectEqual(1, it.root_end_index);
17212247 try std.testing.expectEqualStrings("/", it.root().?);
17222248
17232249 try std.testing.expect(null == it.first());
......@@ -1733,8 +2259,9 @@ test "ComponentIterator posix" {
17332259
17342260 {
17352261 const path = "";
1736 var it = try PosixComponentIterator.init(path);
1737 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);
2262 var it = PosixComponentIterator.init(path);
2263 try std.testing.expectEqual(0, it.root_len);
2264 try std.testing.expectEqual(0, it.root_end_index);
17382265 try std.testing.expect(null == it.root());
17392266
17402267 try std.testing.expect(null == it.first());
......@@ -1753,8 +2280,9 @@ test "ComponentIterator windows" {
17532280 const WindowsComponentIterator = ComponentIterator(.windows, u8);
17542281 {
17552282 const path = "a/b\\c//";
1756 var it = try WindowsComponentIterator.init(path);
1757 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);
2283 var it = WindowsComponentIterator.init(path);
2284 try std.testing.expectEqual(0, it.root_len);
2285 try std.testing.expectEqual(0, it.root_end_index);
17582286 try std.testing.expect(null == it.root());
17592287 {
17602288 try std.testing.expect(null == it.previous());
......@@ -1800,8 +2328,9 @@ test "ComponentIterator windows" {
18002328
18012329 {
18022330 const path = "C:\\a/b/c/";
1803 var it = try WindowsComponentIterator.init(path);
1804 try std.testing.expectEqual(@as(usize, 3), it.root_end_index);
2331 var it = WindowsComponentIterator.init(path);
2332 try std.testing.expectEqual(3, it.root_len);
2333 try std.testing.expectEqual(3, it.root_end_index);
18052334 try std.testing.expectEqualStrings("C:\\", it.root().?);
18062335 {
18072336 const first = it.first().?;
......@@ -1835,10 +2364,49 @@ test "ComponentIterator windows" {
18352364 }
18362365 }
18372366
2367 {
2368 const path = "C:\\\\//a/\\/\\b///c////";
2369 var it = WindowsComponentIterator.init(path);
2370 try std.testing.expectEqual(3, it.root_len);
2371 try std.testing.expectEqual(6, it.root_end_index);
2372 try std.testing.expectEqualStrings("C:\\", it.root().?);
2373 {
2374 const first = it.first().?;
2375 try std.testing.expectEqualStrings("a", first.name);
2376 try std.testing.expectEqualStrings("C:\\\\//a", first.path);
2377
2378 const second = it.next().?;
2379 try std.testing.expectEqualStrings("b", second.name);
2380 try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second.path);
2381
2382 const third = it.next().?;
2383 try std.testing.expectEqualStrings("c", third.name);
2384 try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", third.path);
2385
2386 try std.testing.expect(null == it.next());
2387 }
2388 {
2389 const last = it.last().?;
2390 try std.testing.expectEqualStrings("c", last.name);
2391 try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", last.path);
2392
2393 const second_to_last = it.previous().?;
2394 try std.testing.expectEqualStrings("b", second_to_last.name);
2395 try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second_to_last.path);
2396
2397 const third_to_last = it.previous().?;
2398 try std.testing.expectEqualStrings("a", third_to_last.name);
2399 try std.testing.expectEqualStrings("C:\\\\//a", third_to_last.path);
2400
2401 try std.testing.expect(null == it.previous());
2402 }
2403 }
2404
18382405 {
18392406 const path = "/";
1840 var it = try WindowsComponentIterator.init(path);
1841 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);
2407 var it = WindowsComponentIterator.init(path);
2408 try std.testing.expectEqual(1, it.root_len);
2409 try std.testing.expectEqual(1, it.root_end_index);
18422410 try std.testing.expectEqualStrings("/", it.root().?);
18432411
18442412 try std.testing.expect(null == it.first());
......@@ -1854,8 +2422,9 @@ test "ComponentIterator windows" {
18542422
18552423 {
18562424 const path = "";
1857 var it = try WindowsComponentIterator.init(path);
1858 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);
2425 var it = WindowsComponentIterator.init(path);
2426 try std.testing.expectEqual(0, it.root_len);
2427 try std.testing.expectEqual(0, it.root_end_index);
18592428 try std.testing.expect(null == it.root());
18602429
18612430 try std.testing.expect(null == it.first());
......@@ -1871,17 +2440,13 @@ test "ComponentIterator windows" {
18712440}
18722441
18732442test "ComponentIterator windows WTF-16" {
1874 // TODO: Fix on big endian architectures
1875 if (builtin.cpu.arch.endian() != .little) {
1876 return error.SkipZigTest;
1877 }
1878
18792443 const WindowsComponentIterator = ComponentIterator(.windows, u16);
18802444 const L = std.unicode.utf8ToUtf16LeStringLiteral;
18812445
18822446 const path = L("C:\\a/b/c/");
1883 var it = try WindowsComponentIterator.init(path);
1884 try std.testing.expectEqual(@as(usize, 3), it.root_end_index);
2447 var it = WindowsComponentIterator.init(path);
2448 try std.testing.expectEqual(3, it.root_len);
2449 try std.testing.expectEqual(3, it.root_end_index);
18852450 try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);
18862451 {
18872452 const first = it.first().?;
......@@ -1918,55 +2483,60 @@ test "ComponentIterator windows WTF-16" {
19182483test "ComponentIterator roots" {
19192484 // UEFI
19202485 {
1921 var it = try ComponentIterator(.uefi, u8).init("\\\\a");
1922 try std.testing.expectEqualStrings("\\\\", it.root().?);
2486 var it = ComponentIterator(.uefi, u8).init("\\\\a");
2487 try std.testing.expectEqualStrings("\\", it.root().?);
19232488
1924 it = try ComponentIterator(.uefi, u8).init("//a");
2489 it = ComponentIterator(.uefi, u8).init("//a");
19252490 try std.testing.expect(null == it.root());
19262491 }
19272492 // POSIX
19282493 {
1929 var it = try ComponentIterator(.posix, u8).init("//a");
1930 try std.testing.expectEqualStrings("//", it.root().?);
2494 var it = ComponentIterator(.posix, u8).init("//a");
2495 try std.testing.expectEqualStrings("/", it.root().?);
19312496
1932 it = try ComponentIterator(.posix, u8).init("\\\\a");
2497 it = ComponentIterator(.posix, u8).init("\\\\a");
19332498 try std.testing.expect(null == it.root());
19342499 }
19352500 // Windows
19362501 {
19372502 // Drive relative
1938 var it = try ComponentIterator(.windows, u8).init("C:a");
2503 var it = ComponentIterator(.windows, u8).init("C:a");
19392504 try std.testing.expectEqualStrings("C:", it.root().?);
19402505
19412506 // Drive absolute
1942 it = try ComponentIterator(.windows, u8).init("C://a");
1943 try std.testing.expectEqualStrings("C://", it.root().?);
1944 it = try ComponentIterator(.windows, u8).init("C:\\a");
2507 it = ComponentIterator(.windows, u8).init("C:/a");
2508 try std.testing.expectEqualStrings("C:/", it.root().?);
2509 it = ComponentIterator(.windows, u8).init("C:\\a");
19452510 try std.testing.expectEqualStrings("C:\\", it.root().?);
2511 it = ComponentIterator(.windows, u8).init("C:///a");
2512 try std.testing.expectEqualStrings("C:/", it.root().?);
19462513
19472514 // Rooted
1948 it = try ComponentIterator(.windows, u8).init("\\a");
2515 it = ComponentIterator(.windows, u8).init("\\a");
19492516 try std.testing.expectEqualStrings("\\", it.root().?);
1950 it = try ComponentIterator(.windows, u8).init("/a");
2517 it = ComponentIterator(.windows, u8).init("/a");
19512518 try std.testing.expectEqualStrings("/", it.root().?);
19522519
19532520 // Root local device
1954 it = try ComponentIterator(.windows, u8).init("\\\\.");
2521 it = ComponentIterator(.windows, u8).init("\\\\.");
19552522 try std.testing.expectEqualStrings("\\\\.", it.root().?);
1956 it = try ComponentIterator(.windows, u8).init("//?");
2523 it = ComponentIterator(.windows, u8).init("//?");
19572524 try std.testing.expectEqualStrings("//?", it.root().?);
19582525
19592526 // UNC absolute
1960 it = try ComponentIterator(.windows, u8).init("//");
2527 it = ComponentIterator(.windows, u8).init("//");
19612528 try std.testing.expectEqualStrings("//", it.root().?);
1962 it = try ComponentIterator(.windows, u8).init("\\\\a");
2529 it = ComponentIterator(.windows, u8).init("\\\\a");
19632530 try std.testing.expectEqualStrings("\\\\a", it.root().?);
1964 it = try ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
1965 try std.testing.expectEqualStrings("\\\\a\\b\\\\", it.root().?);
1966 it = try ComponentIterator(.windows, u8).init("//a");
2531 it = ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
2532 try std.testing.expectEqualStrings("\\\\a\\b\\", it.root().?);
2533 it = ComponentIterator(.windows, u8).init("//a");
19672534 try std.testing.expectEqualStrings("//a", it.root().?);
1968 it = try ComponentIterator(.windows, u8).init("//a/b//c");
1969 try std.testing.expectEqualStrings("//a/b//", it.root().?);
2535 it = ComponentIterator(.windows, u8).init("//a/b//c");
2536 try std.testing.expectEqualStrings("//a/b/", it.root().?);
2537 // Malformed UNC path with empty server name
2538 it = ComponentIterator(.windows, u8).init("\\\\\\a\\b\\c");
2539 try std.testing.expectEqualStrings("\\\\\\a\\", it.root().?);
19702540 }
19712541}
19722542
lib/std/fs/test.zig+1-1
......@@ -56,7 +56,7 @@ const PathType = enum {
5656 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
5757 var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
5858 const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);
59 const windows_path_type = windows.getUnprefixedPathType(u8, dir_path);
59 const windows_path_type = windows.getWin32PathType(u8, dir_path);
6060 switch (windows_path_type) {
6161 .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),
6262 .drive_absolute => {
lib/std/os/windows.zig+325-262
......@@ -816,8 +816,11 @@ pub fn CreateSymbolicLink(
816816 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
817817 var is_target_absolute = false;
818818 const final_target_path = target_path: {
819 switch (getNamespacePrefix(u16, target_path)) {
820 .none => switch (getUnprefixedPathType(u16, target_path)) {
819 if (hasCommonNtPrefix(u16, target_path)) {
820 // Already an NT path, no need to do anything to it
821 break :target_path target_path;
822 } else {
823 switch (getWin32PathType(u16, target_path)) {
821824 // Rooted paths need to avoid getting put through wToPrefixedFileW
822825 // (and they are treated as relative in this context)
823826 // Note: It seems that rooted paths in symbolic links are relative to
......@@ -829,10 +832,7 @@ pub fn CreateSymbolicLink(
829832 // Keep relative paths relative, but anything else needs to get NT-prefixed.
830833 else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
831834 break :target_path target_path,
832 },
833 // Already an NT path, no need to do anything to it
834 .nt => break :target_path target_path,
835 else => {},
835 }
836836 }
837837 var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
838838 // We do this after prefixing to ensure that drive-relative paths are treated as absolute
......@@ -2145,7 +2145,7 @@ pub fn nanoSecondsToFileTime(ns: Io.Timestamp) FILETIME {
21452145/// Compares two WTF16 strings using the equivalent functionality of
21462146/// `RtlEqualUnicodeString` (with case insensitive comparison enabled).
21472147/// This function can be called on any target.
2148pub fn eqlIgnoreCaseWTF16(a: []const u16, b: []const u16) bool {
2148pub fn eqlIgnoreCaseWtf16(a: []const u16, b: []const u16) bool {
21492149 if (@inComptime() or builtin.os.tag != .windows) {
21502150 // This function compares the strings code unit by code unit (aka u16-to-u16),
21512151 // so any length difference implies inequality. In other words, there's no possible
......@@ -2222,19 +2222,19 @@ pub fn eqlIgnoreCaseWtf8(a: []const u8, b: []const u8) bool {
22222222
22232223fn testEqlIgnoreCase(comptime expect_eql: bool, comptime a: []const u8, comptime b: []const u8) !void {
22242224 try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWtf8(a, b));
2225 try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWTF16(
2225 try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWtf16(
22262226 std.unicode.utf8ToUtf16LeStringLiteral(a),
22272227 std.unicode.utf8ToUtf16LeStringLiteral(b),
22282228 ));
22292229
22302230 try comptime std.testing.expect(expect_eql == eqlIgnoreCaseWtf8(a, b));
2231 try comptime std.testing.expect(expect_eql == eqlIgnoreCaseWTF16(
2231 try comptime std.testing.expect(expect_eql == eqlIgnoreCaseWtf16(
22322232 std.unicode.utf8ToUtf16LeStringLiteral(a),
22332233 std.unicode.utf8ToUtf16LeStringLiteral(b),
22342234 ));
22352235}
22362236
2237test "eqlIgnoreCaseWTF16/Wtf8" {
2237test "eqlIgnoreCaseWtf16/Wtf8" {
22382238 try testEqlIgnoreCase(true, "\x01 a B Λ ɐ", "\x01 A b λ Ɐ");
22392239 // does not do case-insensitive comparison for codepoints >= U+10000
22402240 try testEqlIgnoreCase(false, "𐓏", "𐓷");
......@@ -2365,271 +2365,339 @@ pub const Wtf16ToPrefixedFileWError = error{
23652365/// - . and space are not stripped from the end of relative paths (potential TODO)
23662366pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {
23672367 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
2368 switch (getNamespacePrefix(u16, path)) {
2369 // TODO: Figure out a way to design an API that can avoid the copy for .nt,
2368 if (hasCommonNtPrefix(u16, path)) {
2369 // TODO: Figure out a way to design an API that can avoid the copy for NT,
23702370 // since it is always returned fully unmodified.
2371 .nt, .verbatim => {
2372 var path_space: PathSpace = undefined;
2373 path_space.data[0..nt_prefix.len].* = nt_prefix;
2374 const len_after_prefix = path.len - nt_prefix.len;
2375 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2376 path_space.len = path.len;
2377 path_space.data[path_space.len] = 0;
2378 return path_space;
2379 },
2380 .local_device, .fake_verbatim => {
2381 var path_space: PathSpace = undefined;
2382 const path_byte_len = ntdll.RtlGetFullPathName_U(
2383 path.ptr,
2384 path_space.data.len * 2,
2385 &path_space.data,
2386 null,
2387 );
2388 if (path_byte_len == 0) {
2389 // TODO: This may not be the right error
2390 return error.BadPathName;
2391 } else if (path_byte_len / 2 > path_space.data.len) {
2392 return error.NameTooLong;
2393 }
2394 path_space.len = path_byte_len / 2;
2395 // Both prefixes will be normalized but retained, so all
2396 // we need to do now is replace them with the NT prefix
2397 path_space.data[0..nt_prefix.len].* = nt_prefix;
2398 return path_space;
2399 },
2400 .none => {
2401 const path_type = getUnprefixedPathType(u16, path);
2402 var path_space: PathSpace = undefined;
2403 relative: {
2404 if (path_type == .relative) {
2405 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
2406 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
2407
2408 // TODO: Potentially strip all trailing . and space characters from the
2409 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
2410 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
2411 // are allowed, but such paths may not interact well with Windows (i.e.
2412 // files with these paths can't be deleted from explorer.exe, etc).
2413 // This could be something that normalizePath may want to do.
2414
2415 @memcpy(path_space.data[0..path.len], path);
2416 // Try to normalize, but if we get too many parent directories,
2417 // then we need to start over and use RtlGetFullPathName_U instead.
2418 path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
2419 error.TooManyParentDirs => break :relative,
2420 };
2371 var path_space: PathSpace = undefined;
2372 path_space.data[0..nt_prefix.len].* = nt_prefix;
2373 const len_after_prefix = path.len - nt_prefix.len;
2374 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2375 path_space.len = path.len;
2376 path_space.data[path_space.len] = 0;
2377 return path_space;
2378 } else {
2379 const path_type = getWin32PathType(u16, path);
2380 var path_space: PathSpace = undefined;
2381 if (path_type == .local_device) {
2382 switch (getLocalDevicePathType(u16, path)) {
2383 .verbatim => {
2384 path_space.data[0..nt_prefix.len].* = nt_prefix;
2385 const len_after_prefix = path.len - nt_prefix.len;
2386 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2387 path_space.len = path.len;
24212388 path_space.data[path_space.len] = 0;
24222389 return path_space;
2423 }
2390 },
2391 .local_device, .fake_verbatim => {
2392 const path_byte_len = ntdll.RtlGetFullPathName_U(
2393 path.ptr,
2394 path_space.data.len * 2,
2395 &path_space.data,
2396 null,
2397 );
2398 if (path_byte_len == 0) {
2399 // TODO: This may not be the right error
2400 return error.BadPathName;
2401 } else if (path_byte_len / 2 > path_space.data.len) {
2402 return error.NameTooLong;
2403 }
2404 path_space.len = path_byte_len / 2;
2405 // Both prefixes will be normalized but retained, so all
2406 // we need to do now is replace them with the NT prefix
2407 path_space.data[0..nt_prefix.len].* = nt_prefix;
2408 return path_space;
2409 },
24242410 }
2425 // We now know we are going to return an absolute NT path, so
2426 // we can unconditionally prefix it with the NT prefix.
2427 path_space.data[0..nt_prefix.len].* = nt_prefix;
2428 if (path_type == .root_local_device) {
2429 // `\\.` and `\\?` always get converted to `\??\` exactly, so
2430 // we can just stop here
2431 path_space.len = nt_prefix.len;
2411 }
2412 relative: {
2413 if (path_type == .relative) {
2414 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
2415 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
2416
2417 // TODO: Potentially strip all trailing . and space characters from the
2418 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
2419 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
2420 // are allowed, but such paths may not interact well with Windows (i.e.
2421 // files with these paths can't be deleted from explorer.exe, etc).
2422 // This could be something that normalizePath may want to do.
2423
2424 @memcpy(path_space.data[0..path.len], path);
2425 // Try to normalize, but if we get too many parent directories,
2426 // then we need to start over and use RtlGetFullPathName_U instead.
2427 path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
2428 error.TooManyParentDirs => break :relative,
2429 };
24322430 path_space.data[path_space.len] = 0;
24332431 return path_space;
24342432 }
2435 const path_buf_offset = switch (path_type) {
2436 // UNC paths will always start with `\\`. However, we want to
2437 // end up with something like `\??\UNC\server\share`, so to get
2438 // RtlGetFullPathName to write into the spot we want the `server`
2439 // part to end up, we need to provide an offset such that
2440 // the `\\` part gets written where the `C\` of `UNC\` will be
2441 // in the final NT path.
2442 .unc_absolute => nt_prefix.len + 2,
2443 else => nt_prefix.len,
2444 };
2445 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
2446 const path_to_get: [:0]const u16 = path_to_get: {
2447 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
2448 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
2449 if (path_type != .relative or dir == null) {
2450 break :path_to_get path;
2451 }
2452 // We can also skip GetFinalPathNameByHandle if the handle matches
2453 // the handle returned by fs.cwd()
2454 if (dir.? == std.fs.cwd().fd) {
2455 break :path_to_get path;
2456 }
2457 // At this point, we know we have a relative path that had too many
2458 // `..` components to be resolved by normalizePath, so we need to
2459 // convert it into an absolute path and let RtlGetFullPathName_U
2460 // canonicalize it. We do this by getting the path of the `dir`
2461 // and appending the relative path to it.
2462 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
2463 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
2464 // This mapping is not correct; it is actually expected
2465 // that calling GetFinalPathNameByHandle might return
2466 // error.UnrecognizedVolume, and in fact has been observed
2467 // in the wild. The problem is that wToPrefixedFileW was
2468 // never intended to make *any* OS syscall APIs. It's only
2469 // supposed to convert a string to one that is eligible to
2470 // be used in the ntdll syscalls.
2471 //
2472 // To solve this, this function needs to no longer call
2473 // GetFinalPathNameByHandle under any conditions, or the
2474 // calling function needs to get reworked to not need to
2475 // call this function.
2476 //
2477 // This may involve making breaking API changes.
2478 error.UnrecognizedVolume => return error.Unexpected,
2479 else => |e| return e,
2480 };
2481 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
2482 return error.NameTooLong;
2483 }
2484 // We don't have to worry about potentially doubling up path separators
2485 // here since RtlGetFullPathName_U will handle canonicalizing it.
2486 dir_path_buf[dir_path.len] = '\\';
2487 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
2488 const full_len = dir_path.len + 1 + path.len;
2489 dir_path_buf[full_len] = 0;
2490 break :path_to_get dir_path_buf[0..full_len :0];
2433 }
2434 // We now know we are going to return an absolute NT path, so
2435 // we can unconditionally prefix it with the NT prefix.
2436 path_space.data[0..nt_prefix.len].* = nt_prefix;
2437 if (path_type == .root_local_device) {
2438 // `\\.` and `\\?` always get converted to `\??\` exactly, so
2439 // we can just stop here
2440 path_space.len = nt_prefix.len;
2441 path_space.data[path_space.len] = 0;
2442 return path_space;
2443 }
2444 const path_buf_offset = switch (path_type) {
2445 // UNC paths will always start with `\\`. However, we want to
2446 // end up with something like `\??\UNC\server\share`, so to get
2447 // RtlGetFullPathName to write into the spot we want the `server`
2448 // part to end up, we need to provide an offset such that
2449 // the `\\` part gets written where the `C\` of `UNC\` will be
2450 // in the final NT path.
2451 .unc_absolute => nt_prefix.len + 2,
2452 else => nt_prefix.len,
2453 };
2454 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
2455 const path_to_get: [:0]const u16 = path_to_get: {
2456 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
2457 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
2458 if (path_type != .relative or dir == null) {
2459 break :path_to_get path;
2460 }
2461 // We can also skip GetFinalPathNameByHandle if the handle matches
2462 // the handle returned by fs.cwd()
2463 if (dir.? == std.fs.cwd().fd) {
2464 break :path_to_get path;
2465 }
2466 // At this point, we know we have a relative path that had too many
2467 // `..` components to be resolved by normalizePath, so we need to
2468 // convert it into an absolute path and let RtlGetFullPathName_U
2469 // canonicalize it. We do this by getting the path of the `dir`
2470 // and appending the relative path to it.
2471 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
2472 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
2473 // This mapping is not correct; it is actually expected
2474 // that calling GetFinalPathNameByHandle might return
2475 // error.UnrecognizedVolume, and in fact has been observed
2476 // in the wild. The problem is that wToPrefixedFileW was
2477 // never intended to make *any* OS syscall APIs. It's only
2478 // supposed to convert a string to one that is eligible to
2479 // be used in the ntdll syscalls.
2480 //
2481 // To solve this, this function needs to no longer call
2482 // GetFinalPathNameByHandle under any conditions, or the
2483 // calling function needs to get reworked to not need to
2484 // call this function.
2485 //
2486 // This may involve making breaking API changes.
2487 error.UnrecognizedVolume => return error.Unexpected,
2488 else => |e| return e,
24912489 };
2492 const path_byte_len = ntdll.RtlGetFullPathName_U(
2493 path_to_get.ptr,
2494 buf_len * 2,
2495 path_space.data[path_buf_offset..].ptr,
2496 null,
2497 );
2498 if (path_byte_len == 0) {
2499 // TODO: This may not be the right error
2500 return error.BadPathName;
2501 } else if (path_byte_len / 2 > buf_len) {
2490 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
25022491 return error.NameTooLong;
25032492 }
2504 path_space.len = path_buf_offset + (path_byte_len / 2);
2505 if (path_type == .unc_absolute) {
2506 // Now add in the UNC, the `C` should overwrite the first `\` of the
2507 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
2508 std.debug.assert(path_space.data[path_buf_offset] == '\\');
2509 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');
2510 const unc = [_]u16{ 'U', 'N', 'C' };
2511 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
2512 }
2513 return path_space;
2514 },
2515 }
2516}
2517
2518pub const NamespacePrefix = enum {
2519 none,
2520 /// `\\.\` (path separators can be `\` or `/`)
2521 local_device,
2522 /// `\\?\`
2523 /// When converted to an NT path, everything past the prefix is left
2524 /// untouched and `\\?\` is replaced by `\??\`.
2525 verbatim,
2526 /// `\\?\` without all path separators being `\`.
2527 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
2528 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
2529 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
2530 /// be treated as part of the final path])
2531 fake_verbatim,
2532 /// `\??\`
2533 nt,
2534};
2535
2536/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
2537pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix {
2538 if (path.len < 4) return .none;
2539 var all_backslash = switch (mem.littleToNative(T, path[0])) {
2540 '\\' => true,
2541 '/' => false,
2542 else => return .none,
2543 };
2544 all_backslash = all_backslash and switch (mem.littleToNative(T, path[3])) {
2545 '\\' => true,
2546 '/' => false,
2547 else => return .none,
2548 };
2549 switch (mem.littleToNative(T, path[1])) {
2550 '?' => if (mem.littleToNative(T, path[2]) == '?' and all_backslash) return .nt else return .none,
2551 '\\' => {},
2552 '/' => all_backslash = false,
2553 else => return .none,
2493 // We don't have to worry about potentially doubling up path separators
2494 // here since RtlGetFullPathName_U will handle canonicalizing it.
2495 dir_path_buf[dir_path.len] = '\\';
2496 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
2497 const full_len = dir_path.len + 1 + path.len;
2498 dir_path_buf[full_len] = 0;
2499 break :path_to_get dir_path_buf[0..full_len :0];
2500 };
2501 const path_byte_len = ntdll.RtlGetFullPathName_U(
2502 path_to_get.ptr,
2503 buf_len * 2,
2504 path_space.data[path_buf_offset..].ptr,
2505 null,
2506 );
2507 if (path_byte_len == 0) {
2508 // TODO: This may not be the right error
2509 return error.BadPathName;
2510 } else if (path_byte_len / 2 > buf_len) {
2511 return error.NameTooLong;
2512 }
2513 path_space.len = path_buf_offset + (path_byte_len / 2);
2514 if (path_type == .unc_absolute) {
2515 // Now add in the UNC, the `C` should overwrite the first `\` of the
2516 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
2517 std.debug.assert(path_space.data[path_buf_offset] == '\\');
2518 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');
2519 const unc = [_]u16{ 'U', 'N', 'C' };
2520 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
2521 }
2522 return path_space;
25542523 }
2555 return switch (mem.littleToNative(T, path[2])) {
2556 '?' => if (all_backslash) .verbatim else .fake_verbatim,
2557 '.' => .local_device,
2558 else => .none,
2559 };
2560}
2561
2562test getNamespacePrefix {
2563 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, ""));
2564 try std.testing.expectEqual(NamespacePrefix.nt, getNamespacePrefix(u8, "\\??\\"));
2565 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??/"));
2566 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??\\"));
2567 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "\\?\\\\"));
2568 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\.\\"));
2569 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\./"));
2570 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "/\\./"));
2571 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "//./"));
2572 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/.//"));
2573 try std.testing.expectEqual(NamespacePrefix.verbatim, getNamespacePrefix(u8, "\\\\?\\"));
2574 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?\\"));
2575 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?/"));
2576 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "//?/"));
25772524}
25782525
2579pub const UnprefixedPathType = enum {
2526/// Similar to `RTL_PATH_TYPE`, but without the `UNKNOWN` path type.
2527pub const Win32PathType = enum {
2528 /// `\\server\share\foo`
25802529 unc_absolute,
2530 /// `C:\foo`
25812531 drive_absolute,
2532 /// `C:foo`
25822533 drive_relative,
2534 /// `\foo`
25832535 rooted,
2536 /// `foo`
25842537 relative,
2538 /// `\\.\foo`, `\\?\foo`
2539 local_device,
2540 /// `\\.`, `\\?`
25852541 root_local_device,
25862542};
25872543
2588/// Get the path type of a path that is known to not have any namespace prefixes
2589/// (`\\?\`, `\\.\`, `\??\`).
2544/// Get the path type of a Win32 namespace path.
2545/// Similar to `RtlDetermineDosPathNameType_U`.
25902546/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
2591pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType {
2547pub fn getWin32PathType(comptime T: type, path: []const T) Win32PathType {
25922548 if (path.len < 1) return .relative;
25932549
2594 if (std.debug.runtime_safety) {
2595 std.debug.assert(getNamespacePrefix(T, path) == .none);
2596 }
2597
25982550 const windows_path = std.fs.path.PathType.windows;
2599 if (windows_path.isSep(T, mem.littleToNative(T, path[0]))) {
2551 if (windows_path.isSep(T, path[0])) {
26002552 // \x
2601 if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted;
2602 // exactly \\. or \\? with nothing trailing
2603 if (path.len == 3 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) return .root_local_device;
2553 if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted;
2554 // \\. or \\?
2555 if (path.len > 2 and (path[2] == mem.nativeToLittle(T, '.') or path[2] == mem.nativeToLittle(T, '?'))) {
2556 // exactly \\. or \\? with nothing trailing
2557 if (path.len == 3) return .root_local_device;
2558 // \\.\x or \\?\x
2559 if (windows_path.isSep(T, path[3])) return .local_device;
2560 }
26042561 // \\x
26052562 return .unc_absolute;
26062563 } else {
2564 // Some choice has to be made about how non-ASCII code points as drive-letters are handled, since
2565 // path[0] is a different size for WTF-16 vs WTF-8, leading to a potential mismatch in classification
2566 // for a WTF-8 path and its WTF-16 equivalent. For example, `€:\` encoded in WTF-16 is three code
2567 // units `<0x20AC>:\` whereas `€:\` encoded as WTF-8 is 6 code units `<0xE2><0x82><0xAC>:\` so
2568 // checking path[0], path[1] and path[2] would not behave the same between WTF-8/WTF-16.
2569 //
2570 // `RtlDetermineDosPathNameType_U` exclusively deals with WTF-16 and considers
2571 // `€:\` a drive-absolute path, but code points that take two WTF-16 code units to encode get
2572 // classified as a relative path (e.g. with U+20000 as the drive-letter that'd be encoded
2573 // in WTF-16 as `<0xD840><0xDC00>:\` and be considered a relative path).
2574 //
2575 // The choice made here is to emulate the behavior of `RtlDetermineDosPathNameType_U` for both
2576 // WTF-16 and WTF-8. This is because, while unlikely and not supported by the Disk Manager GUI,
2577 // drive letters are not actually restricted to A-Z. Using `SetVolumeMountPointW` will allow you
2578 // to set any byte value as a drive letter, and going through `IOCTL_MOUNTMGR_CREATE_POINT` will
2579 // allow you to set any WTF-16 code unit as a drive letter.
2580 //
2581 // Non-A-Z drive letters don't interact well with most of Windows, but certain things do work, e.g.
2582 // `cd /D €:\` will work, filesystem functions still work, etc.
2583 //
2584 // The unfortunate part of this is that this makes handling WTF-8 more complicated as we can't
2585 // just check path[0], path[1], path[2].
2586 const colon_i: usize = switch (T) {
2587 u8 => i: {
2588 const code_point_len = std.unicode.utf8ByteSequenceLength(path[0]) catch return .relative;
2589 // Conveniently, 4-byte sequences in WTF-8 have the same starting code point
2590 // as 2-code-unit sequences in WTF-16.
2591 if (code_point_len > 3) return .relative;
2592 break :i code_point_len;
2593 },
2594 u16 => 1,
2595 else => @compileError("unsupported type: " ++ @typeName(T)),
2596 };
26072597 // x
2608 if (path.len < 2 or mem.littleToNative(T, path[1]) != ':') return .relative;
2598 if (path.len < colon_i + 1 or path[colon_i] != mem.nativeToLittle(T, ':')) return .relative;
26092599 // x:\
2610 if (path.len > 2 and windows_path.isSep(T, mem.littleToNative(T, path[2]))) return .drive_absolute;
2600 if (path.len > colon_i + 1 and windows_path.isSep(T, path[colon_i + 1])) return .drive_absolute;
26112601 // x:
26122602 return .drive_relative;
26132603 }
26142604}
26152605
2616test getUnprefixedPathType {
2617 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, ""));
2618 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x"));
2619 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x\\"));
2620 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "//."));
2621 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "/\\?"));
2622 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "\\\\?"));
2623 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "\\\\x"));
2624 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "//x"));
2625 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "\\x"));
2626 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "/"));
2627 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:"));
2628 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:abc"));
2629 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:a/b/c"));
2630 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\"));
2631 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\abc"));
2632 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
2606test getWin32PathType {
2607 try std.testing.expectEqual(.relative, getWin32PathType(u8, ""));
2608 try std.testing.expectEqual(.relative, getWin32PathType(u8, "x"));
2609 try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\"));
2610
2611 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//."));
2612 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?"));
2613 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?"));
2614
2615 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x"));
2616 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x"));
2617 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x"));
2618 // local device paths require a path separator after the root, otherwise it is considered a UNC path
2619 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x"));
2620 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x"));
2621
2622 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//"));
2623 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\x"));
2624 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//x"));
2625
2626 try std.testing.expectEqual(.rooted, getWin32PathType(u8, "\\x"));
2627 try std.testing.expectEqual(.rooted, getWin32PathType(u8, "/"));
2628
2629 try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:"));
2630 try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:abc"));
2631 try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:a/b/c"));
2632
2633 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\"));
2634 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\abc"));
2635 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:/a/b/c"));
2636
2637 // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
2638 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "€:\\"));
2639 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:\\")));
2640 try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "€:"));
2641 try std.testing.expectEqual(.drive_relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:")));
2642 // But code points that are encoded as two WTF-16 code units are not
2643 try std.testing.expectEqual(.relative, getWin32PathType(u8, "\u{10000}:\\"));
2644 try std.testing.expectEqual(.relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("\u{10000}:\\")));
2645}
2646
2647/// Returns true if the path starts with `\??\`, which is indicative of an NT path
2648/// but is not enough to fully distinguish between NT paths and Win32 paths, as
2649/// `\??\` is not actually a distinct prefix but rather the path to a special virtual
2650/// folder in the Object Manager.
2651///
2652/// For example, `\Device\HarddiskVolume2` and `\DosDevices\C:` are also NT paths but
2653/// cannot be distinguished as such by their prefix.
2654///
2655/// So, inferring whether a path is an NT path or a Win32 path is usually a mistake;
2656/// that information should instead be known ahead-of-time.
2657///
2658/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
2659pub fn hasCommonNtPrefix(comptime T: type, path: []const T) bool {
2660 // Must be exactly \??\, forward slashes are not allowed
2661 const expected_wtf8_prefix = "\\??\\";
2662 const expected_prefix = switch (T) {
2663 u8 => expected_wtf8_prefix,
2664 u16 => std.unicode.wtf8ToWtf16LeStringLiteral(expected_wtf8_prefix),
2665 else => @compileError("unsupported type: " ++ @typeName(T)),
2666 };
2667 return mem.startsWith(T, path, expected_prefix);
2668}
2669
2670const LocalDevicePathType = enum {
2671 /// `\\.\` (path separators can be `\` or `/`)
2672 local_device,
2673 /// `\\?\`
2674 /// When converted to an NT path, everything past the prefix is left
2675 /// untouched and `\\?\` is replaced by `\??\`.
2676 verbatim,
2677 /// `\\?\` without all path separators being `\`.
2678 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
2679 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
2680 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
2681 /// be treated as part of the final path])
2682 fake_verbatim,
2683};
2684
2685/// Only relevant for Win32 -> NT path conversion.
2686/// Asserts `path` is of type `Win32PathType.local_device`.
2687fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
2688 if (std.debug.runtime_safety) {
2689 std.debug.assert(getWin32PathType(T, path) == .local_device);
2690 }
2691
2692 const backslash = mem.nativeToLittle(T, '\\');
2693 const all_backslash = path[0] == backslash and
2694 path[1] == backslash and
2695 path[3] == backslash;
2696 return switch (path[2]) {
2697 mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
2698 mem.nativeToLittle(T, '.') => .local_device,
2699 else => unreachable,
2700 };
26332701}
26342702
26352703/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
......@@ -2646,30 +2714,25 @@ test getUnprefixedPathType {
26462714/// Supports in-place modification (`path` and `out` may refer to the same slice).
26472715pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {
26482716 if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
2649
2650 const namespace_prefix = getNamespacePrefix(u16, path);
2651 switch (namespace_prefix) {
2652 .nt => {
2653 var dest_index: usize = 0;
2654 var after_prefix = path[4..]; // after the `\??\`
2655 // The prefix \??\UNC\ means this is a UNC path, in which case the
2656 // `\??\UNC\` should be replaced by `\\` (two backslashes)
2657 const is_unc = after_prefix.len >= 4 and
2658 eqlIgnoreCaseWTF16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
2659 std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));
2660 const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
2661 if (out.len < win32_len) return error.NameTooLong;
2662 if (is_unc) {
2663 out[0] = comptime std.mem.nativeToLittle(u16, '\\');
2664 dest_index += 1;
2665 // We want to include the last `\` of `\??\UNC\`
2666 after_prefix = path[7..];
2667 }
2668 @memmove(out[dest_index..][0..after_prefix.len], after_prefix);
2669 return out[0..win32_len];
2670 },
2671 else => return error.NotNtPath,
2717 if (!hasCommonNtPrefix(u16, path)) return error.NotNtPath;
2718
2719 var dest_index: usize = 0;
2720 var after_prefix = path[4..]; // after the `\??\`
2721 // The prefix \??\UNC\ means this is a UNC path, in which case the
2722 // `\??\UNC\` should be replaced by `\\` (two backslashes)
2723 const is_unc = after_prefix.len >= 4 and
2724 eqlIgnoreCaseWtf16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
2725 std.fs.path.PathType.windows.isSep(u16, after_prefix[3]);
2726 const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
2727 if (out.len < win32_len) return error.NameTooLong;
2728 if (is_unc) {
2729 out[0] = comptime std.mem.nativeToLittle(u16, '\\');
2730 dest_index += 1;
2731 // We want to include the last `\` of `\??\UNC\`
2732 after_prefix = path[7..];
26722733 }
2734 @memmove(out[dest_index..][0..after_prefix.len], after_prefix);
2735 return out[0..win32_len];
26732736}
26742737
26752738test ntToWin32Namespace {
lib/std/os/windows/test.zig+102-2
......@@ -54,8 +54,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
5454}
5555
5656test "toPrefixedFileW" {
57 if (builtin.os.tag != .windows)
58 return;
57 if (builtin.os.tag != .windows) return error.SkipZigTest;
5958
6059 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
6160 // Note that these tests do not actually touch the filesystem or care about whether or not
......@@ -237,3 +236,104 @@ test "removeDotDirs" {
237236 try testRemoveDotDirs("a\\b\\..\\", "a\\");
238237 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
239238}
239
240const RTL_PATH_TYPE = enum(c_int) {
241 Unknown,
242 UncAbsolute,
243 DriveAbsolute,
244 DriveRelative,
245 Rooted,
246 Relative,
247 LocalDevice,
248 RootLocalDevice,
249};
250
251pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
252 Path: [*:0]const u16,
253) callconv(.winapi) RTL_PATH_TYPE;
254
255test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
256 if (builtin.os.tag != .windows) return error.SkipZigTest;
257
258 var buf: std.ArrayList(u16) = .empty;
259 defer buf.deinit(std.testing.allocator);
260
261 var wtf8_buf: std.ArrayList(u8) = .empty;
262 defer wtf8_buf.deinit(std.testing.allocator);
263
264 var random = std.Random.DefaultPrng.init(std.testing.random_seed);
265 const rand = random.random();
266
267 for (0..1000) |_| {
268 buf.clearRetainingCapacity();
269 const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
270 wtf8_buf.clearRetainingCapacity();
271 const wtf8_len = std.unicode.calcWtf8Len(path);
272 try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
273 wtf8_buf.items.len = wtf8_len;
274 std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
275
276 const windows_type = RtlDetermineDosPathNameType_U(path);
277 const wtf16_type = windows.getWin32PathType(u16, path);
278 const wtf8_type = windows.getWin32PathType(u8, wtf8_buf.items);
279
280 checkPathType(windows_type, wtf16_type) catch |err| {
281 std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
282 std.debug.print("path bytes:\n", .{});
283 std.debug.dumpHex(std.mem.sliceAsBytes(path));
284 return err;
285 };
286
287 if (wtf16_type != wtf8_type) {
288 std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
289 std.debug.print("wtf-16 path bytes:\n", .{});
290 std.debug.dumpHex(std.mem.sliceAsBytes(path));
291 std.debug.print("wtf-8 path bytes:\n", .{});
292 std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
293 return error.Wtf8Wtf16Mismatch;
294 }
295 }
296}
297
298fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: windows.Win32PathType) !void {
299 const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
300 .unc_absolute => .UncAbsolute,
301 .drive_absolute => .DriveAbsolute,
302 .drive_relative => .DriveRelative,
303 .rooted => .Rooted,
304 .relative => .Relative,
305 .local_device => .LocalDevice,
306 .root_local_device => .RootLocalDevice,
307 };
308 if (windows_type != expected_windows_type) return error.PathTypeMismatch;
309}
310
311fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
312 const Choice = enum {
313 backslash,
314 slash,
315 control,
316 printable,
317 non_ascii,
318 };
319
320 const choices = rand.uintAtMostBiased(u16, 32);
321
322 for (0..choices) |_| {
323 const choice = rand.enumValue(Choice);
324 const code_unit = switch (choice) {
325 .backslash => '\\',
326 .slash => '/',
327 .control => switch (rand.uintAtMostBiased(u8, 0x20)) {
328 0x20 => '\x7F',
329 else => |b| b + 1, // no NUL
330 },
331 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
332 .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
333 };
334 try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
335 }
336
337 try buf.append(allocator, 0);
338 return buf.items[0 .. buf.items.len - 1 :0];
339}
lib/std/process.zig+6-4
......@@ -22,16 +22,17 @@ pub const GetCwdError = posix.GetCwdError;
2222/// The result is a slice of `out_buffer`, from index `0`.
2323/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
2424/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
25pub fn getCwd(out_buffer: []u8) ![]u8 {
25pub fn getCwd(out_buffer: []u8) GetCwdError![]u8 {
2626 return posix.getcwd(out_buffer);
2727}
2828
29pub const GetCwdAllocError = Allocator.Error || posix.GetCwdError;
29// Same as GetCwdError, minus error.NameTooLong + Allocator.Error
30pub const GetCwdAllocError = Allocator.Error || error{CurrentWorkingDirectoryUnlinked} || posix.UnexpectedError;
3031
3132/// Caller must free the returned memory.
3233/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
3334/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.
34pub fn getCwdAlloc(allocator: Allocator) ![]u8 {
35pub fn getCwdAlloc(allocator: Allocator) GetCwdAllocError![]u8 {
3536 // The use of max_path_bytes here is just a heuristic: most paths will fit
3637 // in stack_buf, avoiding an extra allocation in the common case.
3738 var stack_buf: [fs.max_path_bytes]u8 = undefined;
......@@ -529,6 +530,7 @@ pub fn hasNonEmptyEnvVar(allocator: Allocator, key: []const u8) HasEnvVarError!b
529530}
530531
531532/// Windows-only. Get an environment variable with a null-terminated, WTF-16 encoded name.
533/// The returned slice points to memory in the PEB.
532534///
533535/// This function performs a Unicode-aware case-insensitive lookup using RtlEqualUnicodeString.
534536///
......@@ -564,7 +566,7 @@ pub fn getenvW(key: [*:0]const u16) ?[:0]const u16 {
564566 };
565567
566568 const this_key = key_value[0..equal_index];
567 if (windows.eqlIgnoreCaseWTF16(key_slice, this_key)) {
569 if (windows.eqlIgnoreCaseWtf16(key_slice, this_key)) {
568570 return key_value[equal_index + 1 ..];
569571 }
570572
lib/std/process/Child.zig+2-2
......@@ -1227,7 +1227,7 @@ fn windowsCreateProcessPathExt(
12271227 const app_name = app_buf.items[0..app_name_len];
12281228 const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;
12291229 const ext = app_name[ext_start..];
1230 if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
1230 if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
12311231 return error.UnrecoverableInvalidExe;
12321232 }
12331233 break :unappended err;
......@@ -1278,7 +1278,7 @@ fn windowsCreateProcessPathExt(
12781278 // On InvalidExe, if the extension of the app name is .exe then
12791279 // it's treated as an unrecoverable error. Otherwise, it'll be
12801280 // skipped as normal.
1281 if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
1281 if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
12821282 return error.UnrecoverableInvalidExe;
12831283 }
12841284 continue;
lib/std/zig/WindowsSdk.zig+1-1
......@@ -643,7 +643,7 @@ const MsvcLibDir = struct {
643643
644644 if (!std.fs.path.isAbsolute(dll_path)) return error.PathNotFound;
645645
646 var path_it = std.fs.path.componentIterator(dll_path) catch return error.PathNotFound;
646 var path_it = std.fs.path.componentIterator(dll_path);
647647 // the .dll filename
648648 _ = path_it.last();
649649 const root_path = while (path_it.previous()) |dir_component| {
src/main.zig+1-1
......@@ -3883,7 +3883,7 @@ fn createModule(
38833883 if (create_module.sysroot) |root| {
38843884 for (create_module.lib_dir_args.items) |lib_dir_arg| {
38853885 if (fs.path.isAbsolute(lib_dir_arg)) {
3886 const stripped_dir = lib_dir_arg[fs.path.diskDesignator(lib_dir_arg).len..];
3886 const stripped_dir = lib_dir_arg[fs.path.parsePath(lib_dir_arg).root.len..];
38873887 const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir });
38883888 addLibDirectoryWarn(&create_module.lib_directories, full_path);
38893889 } else {
test/standalone/build.zig.zon+3
......@@ -126,6 +126,9 @@
126126 .windows_bat_args = .{
127127 .path = "windows_bat_args",
128128 },
129 .windows_paths = .{
130 .path = "windows_paths",
131 },
129132 .self_exe_symlink = .{
130133 .path = "self_exe_symlink",
131134 },
test/standalone/windows_paths/build.zig created+37
......@@ -0,0 +1,37 @@
1const std = @import("std");
2const builtin = @import("builtin");
3
4pub fn build(b: *std.Build) void {
5 const test_step = b.step("test", "Test it");
6 b.default_step = test_step;
7
8 const optimize: std.builtin.OptimizeMode = .Debug;
9 const target = b.graph.host;
10
11 if (builtin.os.tag != .windows) return;
12
13 const relative = b.addExecutable(.{
14 .name = "relative",
15 .root_module = b.createModule(.{
16 .root_source_file = b.path("relative.zig"),
17 .optimize = optimize,
18 .target = target,
19 }),
20 });
21
22 const main = b.addExecutable(.{
23 .name = "test",
24 .root_module = b.createModule(.{
25 .root_source_file = b.path("test.zig"),
26 .optimize = optimize,
27 .target = target,
28 }),
29 });
30
31 const run = b.addRunArtifact(main);
32 run.addArtifactArg(relative);
33 run.expectExitCode(0);
34 run.skip_foreign_checks = true;
35
36 test_step.dependOn(&run.step);
37}
test/standalone/windows_paths/relative.zig created+19
......@@ -0,0 +1,19 @@
1const std = @import("std");
2
3pub fn main() !void {
4 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
5 defer std.debug.assert(gpa.deinit() == .ok);
6 const allocator = gpa.allocator();
7
8 const args = try std.process.argsAlloc(allocator);
9 defer std.process.argsFree(allocator, args);
10
11 if (args.len < 3) return error.MissingArgs;
12
13 const relative = try std.fs.path.relative(allocator, args[1], args[2]);
14 defer allocator.free(relative);
15
16 var stdout_writer = std.fs.File.stdout().writerStreaming(&.{});
17 const stdout = &stdout_writer.interface;
18 try stdout.writeAll(relative);
19}
test/standalone/windows_paths/test.zig created+131
......@@ -0,0 +1,131 @@
1const std = @import("std");
2
3pub fn main() anyerror!void {
4 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
5 defer arena_state.deinit();
6 const arena = arena_state.allocator();
7
8 const args = try std.process.argsAlloc(arena);
9
10 if (args.len < 2) return error.MissingArgs;
11
12 const exe_path = args[1];
13
14 const cwd_path = try std.process.getCwdAlloc(arena);
15 const parsed_cwd_path = std.fs.path.parsePathWindows(u8, cwd_path);
16
17 if (parsed_cwd_path.kind == .drive_absolute and !std.ascii.isAlphabetic(cwd_path[0])) {
18 // Technically possible, but not worth supporting here
19 return error.NonAlphabeticDriveLetter;
20 }
21
22 const alt_drive_letter = try getAltDriveLetter(cwd_path);
23 const alt_drive_cwd_key = try std.fmt.allocPrint(arena, "={c}:", .{alt_drive_letter});
24 const alt_drive_cwd = try std.fmt.allocPrint(arena, "{c}:\\baz", .{alt_drive_letter});
25 var alt_drive_env_map = std.process.EnvMap.init(arena);
26 try alt_drive_env_map.put(alt_drive_cwd_key, alt_drive_cwd);
27
28 const empty_env = std.process.EnvMap.init(arena);
29
30 {
31 const drive_rel = try std.fmt.allocPrint(arena, "{c}:foo", .{alt_drive_letter});
32 const drive_abs = try std.fmt.allocPrint(arena, "{c}:\\bar", .{alt_drive_letter});
33
34 // With the special =X: environment variable set, drive-relative paths that
35 // don't match the CWD's drive letter are resolved against that env var.
36 try checkRelative(arena, "..\\..\\bar", &.{ exe_path, drive_rel, drive_abs }, null, &alt_drive_env_map);
37 try checkRelative(arena, "..\\baz\\foo", &.{ exe_path, drive_abs, drive_rel }, null, &alt_drive_env_map);
38
39 // Without that environment variable set, drive-relative paths that don't match the
40 // CWD's drive letter are resolved against the root of the drive.
41 try checkRelative(arena, "..\\bar", &.{ exe_path, drive_rel, drive_abs }, null, &empty_env);
42 try checkRelative(arena, "..\\foo", &.{ exe_path, drive_abs, drive_rel }, null, &empty_env);
43
44 // Bare drive-relative path with no components
45 try checkRelative(arena, "bar", &.{ exe_path, drive_rel[0..2], drive_abs }, null, &empty_env);
46 try checkRelative(arena, "..", &.{ exe_path, drive_abs, drive_rel[0..2] }, null, &empty_env);
47
48 // Bare drive-relative path with no components, drive-CWD set
49 try checkRelative(arena, "..\\bar", &.{ exe_path, drive_rel[0..2], drive_abs }, null, &alt_drive_env_map);
50 try checkRelative(arena, "..\\baz", &.{ exe_path, drive_abs, drive_rel[0..2] }, null, &alt_drive_env_map);
51
52 // Bare drive-relative path relative to the CWD should be equivalent if drive-CWD is set
53 try checkRelative(arena, "", &.{ exe_path, alt_drive_cwd, drive_rel[0..2] }, null, &alt_drive_env_map);
54 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], alt_drive_cwd }, null, &alt_drive_env_map);
55
56 // Bare drive-relative should always be equivalent to itself
57 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &alt_drive_env_map);
58 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &alt_drive_env_map);
59 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &empty_env);
60 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &empty_env);
61 }
62
63 if (parsed_cwd_path.kind == .unc_absolute) {
64 const drive_abs_path = try std.fmt.allocPrint(arena, "{c}:\\foo\\bar", .{alt_drive_letter});
65
66 {
67 try checkRelative(arena, drive_abs_path, &.{ exe_path, cwd_path, drive_abs_path }, null, &empty_env);
68 try checkRelative(arena, cwd_path, &.{ exe_path, drive_abs_path, cwd_path }, null, &empty_env);
69 }
70 } else if (parsed_cwd_path.kind == .drive_absolute) {
71 const cur_drive_letter = parsed_cwd_path.root[0];
72 const path_beyond_root = cwd_path[3..];
73 const unc_cwd = try std.fmt.allocPrint(arena, "\\\\127.0.0.1\\{c}$\\{s}", .{ cur_drive_letter, path_beyond_root });
74
75 {
76 try checkRelative(arena, cwd_path, &.{ exe_path, unc_cwd, cwd_path }, null, &empty_env);
77 try checkRelative(arena, unc_cwd, &.{ exe_path, cwd_path, unc_cwd }, null, &empty_env);
78 }
79 {
80 const drive_abs = cwd_path;
81 const drive_rel = parsed_cwd_path.root[0..2];
82 try checkRelative(arena, "", &.{ exe_path, drive_abs, drive_rel }, null, &empty_env);
83 try checkRelative(arena, "", &.{ exe_path, drive_rel, drive_abs }, null, &empty_env);
84 }
85 } else {
86 return error.UnexpectedPathType;
87 }
88}
89
90fn checkRelative(
91 allocator: std.mem.Allocator,
92 expected_stdout: []const u8,
93 argv: []const []const u8,
94 cwd: ?[]const u8,
95 env_map: ?*const std.process.EnvMap,
96) !void {
97 const result = try std.process.Child.run(.{
98 .allocator = allocator,
99 .argv = argv,
100 .cwd = cwd,
101 .env_map = env_map,
102 });
103 defer allocator.free(result.stdout);
104 defer allocator.free(result.stderr);
105
106 try std.testing.expectEqualStrings("", result.stderr);
107 try std.testing.expectEqualStrings(expected_stdout, result.stdout);
108}
109
110fn getAltDriveLetter(path: []const u8) !u8 {
111 const parsed = std.fs.path.parsePathWindows(u8, path);
112 return switch (parsed.kind) {
113 .drive_absolute => {
114 const cur_drive_letter = parsed.root[0];
115 const next_drive_letter_index = (std.ascii.toUpper(cur_drive_letter) - 'A' + 1) % 26;
116 const next_drive_letter = next_drive_letter_index + 'A';
117 return next_drive_letter;
118 },
119 .unc_absolute => {
120 return 'C';
121 },
122 else => return error.UnexpectedPathType,
123 };
124}
125
126test getAltDriveLetter {
127 try std.testing.expectEqual('D', try getAltDriveLetter("C:\\"));
128 try std.testing.expectEqual('B', try getAltDriveLetter("a:\\"));
129 try std.testing.expectEqual('A', try getAltDriveLetter("Z:\\"));
130 try std.testing.expectEqual('C', try getAltDriveLetter("\\\\foo\\bar"));
131}