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 {...@@ -2914,7 +2914,7 @@ fn validateSearchPath(path: []const u8) error{BadPathName}!void {
2914 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).2914 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
2915 // Those path types are something of an unavoidable way to2915 // Those path types are something of an unavoidable way to
2916 // still hit unreachable during the openDir call.2916 // 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);
2918 while (component_iterator.next()) |component| {2918 while (component_iterator.next()) |component| {
2919 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file2919 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2920 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;2920 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 {...@@ -104,9 +104,7 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath {
104fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {104fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {
105 const relative = try fs.path.relative(allocator, prefix, path);105 const relative = try fs.path.relative(allocator, prefix, path);
106 errdefer allocator.free(relative);106 errdefer allocator.free(relative);
107 var component_iterator = fs.path.NativeComponentIterator.init(relative) catch {107 var component_iterator = fs.path.NativeComponentIterator.init(relative);
108 return error.NotASubPath;
109 };
110 if (component_iterator.root() != null) {108 if (component_iterator.root() != null) {
111 return error.NotASubPath;109 return error.NotASubPath;
112 }110 }
lib/std/Build/Watch/FsEvents.zig+1-1
...@@ -167,7 +167,7 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)...@@ -167,7 +167,7 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)
167 }.lessThan);167 }.lessThan);
168 need_dirs.clearRetainingCapacity();168 need_dirs.clearRetainingCapacity();
169 for (old_dirs) |dir_path| {169 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);
171 while (it.next()) |component| {171 while (it.next()) |component| {
172 if (need_dirs.contains(component.path)) {172 if (need_dirs.contains(component.path)) {
173 // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added173 // 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 };...@@ -318,7 +318,7 @@ pub const MakePathStatus = enum { existed, created };
318/// Same as `makePath` except returns whether the path already existed or was318/// Same as `makePath` except returns whether the path already existed or was
319/// successfully created.319/// successfully created.
320pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus {320pub 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);
322 var status: MakePathStatus = .existed;322 var status: MakePathStatus = .existed;
323 var component = it.last() orelse return error.BadPathName;323 var component = it.last() orelse return error.BadPathName;
324 while (true) {324 while (true) {
lib/std/Io/Threaded.zig+1-1
...@@ -1210,7 +1210,7 @@ fn dirMakeOpenPathWindows(...@@ -1210,7 +1210,7 @@ fn dirMakeOpenPathWindows(
1210 w.SYNCHRONIZE | w.FILE_TRAVERSE |1210 w.SYNCHRONIZE | w.FILE_TRAVERSE |
1211 (if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0));1211 (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);
1214 // If there are no components in the path, then create a dummy component with the full path.1214 // If there are no components in the path, then create a dummy component with the full path.
1215 var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{1215 var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
1216 .name = "",1216 .name = "",
lib/std/fs/path.zig+1066-496
...@@ -20,10 +20,7 @@ const testing = std.testing;...@@ -20,10 +20,7 @@ const testing = std.testing;
20const mem = std.mem;20const mem = std.mem;
21const ascii = std.ascii;21const ascii = std.ascii;
22const Allocator = mem.Allocator;22const Allocator = mem.Allocator;
23const math = std.math;
24const windows = std.os.windows;23const windows = std.os.windows;
25const os = std.os;
26const fs = std.fs;
27const process = std.process;24const process = std.process;
28const native_os = builtin.target.os.tag;25const native_os = builtin.target.os.tag;
2926
...@@ -60,11 +57,12 @@ pub const PathType = enum {...@@ -60,11 +57,12 @@ pub const PathType = enum {
60 posix,57 posix,
6158
62 /// Returns true if `c` is a valid path separator for the `path_type`.59 /// 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.
63 pub inline fn isSep(comptime path_type: PathType, comptime T: type, c: T) bool {61 pub inline fn isSep(comptime path_type: PathType, comptime T: type, c: T) bool {
64 return switch (path_type) {62 return switch (path_type) {
65 .windows => c == '/' or c == '\\',63 .windows => c == mem.nativeToLittle(T, '/') or c == mem.nativeToLittle(T, '\\'),
66 .posix => c == '/',64 .posix => c == mem.nativeToLittle(T, '/'),
67 .uefi => c == '\\',65 .uefi => c == mem.nativeToLittle(T, '\\'),
68 };66 };
69 }67 }
70};68};
...@@ -221,7 +219,7 @@ test join {...@@ -221,7 +219,7 @@ test join {
221 try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);219 try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);
222 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);220 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
223 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);221 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
226 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);
227 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero);225 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero);
...@@ -283,26 +281,16 @@ pub fn isAbsolute(path: []const u8) bool {...@@ -283,26 +281,16 @@ pub fn isAbsolute(path: []const u8) bool {
283}281}
284282
285fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {283fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {
286 if (path.len < 1)284 return switch (windows.getWin32PathType(T, path)) {
287 return false;285 // Unambiguously absolute
288286 .drive_absolute, .unc_absolute, .local_device, .root_local_device => true,
289 if (path[0] == '/')287 // Unambiguously relative
290 return true;288 .relative => false,
291289 // Ambiguous, more absolute than relative
292 if (path[0] == '\\')290 .rooted => true,
293 return true;291 // Ambiguous, more relative than absolute
294292 .drive_relative => false,
295 if (path.len < 3)293 };
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;
306}294}
307295
308pub fn isAbsoluteWindows(path: []const u8) bool {296pub fn isAbsoluteWindows(path: []const u8) bool {
...@@ -347,6 +335,9 @@ test isAbsoluteWindows {...@@ -347,6 +335,9 @@ test isAbsoluteWindows {
347 try testIsAbsoluteWindows("C:\\Users\\", true);335 try testIsAbsoluteWindows("C:\\Users\\", true);
348 try testIsAbsoluteWindows("C:cwd/another", false);336 try testIsAbsoluteWindows("C:cwd/another", false);
349 try testIsAbsoluteWindows("C:cwd\\another", false);337 try testIsAbsoluteWindows("C:cwd\\another", false);
338 try testIsAbsoluteWindows("λ:\\", true);
339 try testIsAbsoluteWindows("λ:", false);
340 try testIsAbsoluteWindows("\u{10000}:\\", false);
350 try testIsAbsoluteWindows("directory/directory", false);341 try testIsAbsoluteWindows("directory/directory", false);
351 try testIsAbsoluteWindows("directory\\directory", false);342 try testIsAbsoluteWindows("directory\\directory", false);
352 try testIsAbsoluteWindows("/usr/local", true);343 try testIsAbsoluteWindows("/usr/local", true);
...@@ -362,12 +353,17 @@ test isAbsolutePosix {...@@ -362,12 +353,17 @@ test isAbsolutePosix {
362353
363fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {354fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {
364 try testing.expectEqual(expected_result, isAbsoluteWindows(path));355 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));
365}360}
366361
367fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {362fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {
368 try testing.expectEqual(expected_result, isAbsolutePosix(path));363 try testing.expectEqual(expected_result, isAbsolutePosix(path));
369}364}
370365
366/// Deprecated; see `WindowsPath2`
371pub const WindowsPath = struct {367pub const WindowsPath = struct {
372 is_abs: bool,368 is_abs: bool,
373 kind: Kind,369 kind: Kind,
...@@ -380,6 +376,7 @@ pub const WindowsPath = struct {...@@ -380,6 +376,7 @@ pub const WindowsPath = struct {
380 };376 };
381};377};
382378
379/// Deprecated; see `parsePathWindows`
383pub fn windowsParsePath(path: []const u8) WindowsPath {380pub fn windowsParsePath(path: []const u8) WindowsPath {
384 if (path.len >= 2 and path[1] == ':') {381 if (path.len >= 2 and path[1] == ':') {
385 return WindowsPath{382 return WindowsPath{
...@@ -402,26 +399,18 @@ pub fn windowsParsePath(path: []const u8) WindowsPath {...@@ -402,26 +399,18 @@ pub fn windowsParsePath(path: []const u8) WindowsPath {
402 .disk_designator = &[_]u8{},399 .disk_designator = &[_]u8{},
403 .is_abs = false,400 .is_abs = false,
404 };401 };
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, "/\\");403 if (path.len >= 2 and PathType.windows.isSep(u8, path[0]) and PathType.windows.isSep(u8, path[1])) {
417 _ = (it.next() orelse return relative_path);404 const root_end = root_end: {
418 _ = (it.next() orelse return relative_path);405 var server_end = mem.indexOfAnyPos(u8, path, 2, "/\\") orelse break :root_end path.len;
419 return WindowsPath{406 while (server_end < path.len and PathType.windows.isSep(u8, path[server_end])) server_end += 1;
420 .is_abs = isAbsoluteWindows(path),407 break :root_end mem.indexOfAnyPos(u8, path, server_end, "/\\") orelse path.len;
421 .kind = WindowsPath.Kind.NetworkShare,408 };
422 .disk_designator = path[0..it.index],409 return WindowsPath{
423 };410 .is_abs = true,
424 }411 .kind = WindowsPath.Kind.NetworkShare,
412 .disk_designator = path[0..root_end],
413 };
425 }414 }
426 return relative_path;415 return relative_path;
427}416}
...@@ -446,10 +435,22 @@ test windowsParsePath {...@@ -446,10 +435,22 @@ test windowsParsePath {
446 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));435 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));
447 }436 }
448 {437 {
449 const parsed = windowsParsePath("\\\\a\\");438 const parsed = windowsParsePath("\\/a\\");
450 try testing.expect(!parsed.is_abs);439 try testing.expect(parsed.is_abs);
451 try testing.expect(parsed.kind == WindowsPath.Kind.None);440 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
452 try testing.expect(mem.eql(u8, parsed.disk_designator, ""));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"));
453 }454 }
454 {455 {
455 const parsed = windowsParsePath("/usr/local");456 const parsed = windowsParsePath("/usr/local");
...@@ -465,6 +466,229 @@ test windowsParsePath {...@@ -465,6 +466,229 @@ test windowsParsePath {
465 }466 }
466}467}
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`
468pub fn diskDesignator(path: []const u8) []const u8 {692pub fn diskDesignator(path: []const u8) []const u8 {
469 if (native_os == .windows) {693 if (native_os == .windows) {
470 return diskDesignatorWindows(path);694 return diskDesignatorWindows(path);
...@@ -473,41 +697,172 @@ pub fn diskDesignator(path: []const u8) []const u8 {...@@ -473,41 +697,172 @@ pub fn diskDesignator(path: []const u8) []const u8 {
473 }697 }
474}698}
475699
700/// Deprecated; use `parsePathWindows`
476pub fn diskDesignatorWindows(path: []const u8) []const u8 {701pub fn diskDesignatorWindows(path: []const u8) []const u8 {
477 return windowsParsePath(path).disk_designator;702 return windowsParsePath(path).disk_designator;
478}703}
479704
480fn networkShareServersEql(ns1: []const u8, ns2: []const u8) bool {705fn WindowsUNC(comptime T: type) type {
481 const sep1 = ns1[0];706 return struct {
482 const sep2 = ns2[0];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);714/// Asserts that `path` starts with two path separators
485 var it2 = mem.tokenizeScalar(u8, ns2, sep2);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 }
488}799}
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 };
491 switch (kind) {810 switch (kind) {
492 WindowsPath.Kind.None => {811 .drive => {
493 assert(p1.len == 0);812 const drive_letter1 = getDriveLetter(T, p1);
494 assert(p2.len == 0);813 const drive_letter2 = getDriveLetter(T, p2);
495 return true;814
496 },815 return eql(drive_letter1, drive_letter2);
497 WindowsPath.Kind.Drive => {
498 return ascii.toUpper(p1[0]) == ascii.toUpper(p2[0]);
499 },816 },
500 WindowsPath.Kind.NetworkShare => {817 .unc => {
501 var it1 = mem.tokenizeAny(u8, p1, "/\\");818 var unc1 = parseUNC(T, p1);
502 var it2 = mem.tokenizeAny(u8, p2, "/\\");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);
505 },823 },
506 }824 }
507}825}
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
509/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.864/// 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 {
511 if (native_os == .windows) {866 if (native_os == .windows) {
512 return resolveWindows(allocator, paths);867 return resolveWindows(allocator, paths);
513 } else {868 } else {
...@@ -516,184 +871,232 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {...@@ -516,184 +871,232 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
516}871}
517872
518/// This function is like a series of `cd` statements executed one after another.873/// 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.874/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
520/// The result does not have a trailing path separator.875/// an absolute path, use std.fs.Dir.realpath instead.
521/// Each drive has its own current working directory.876/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.
522/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.877/// 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///
523/// Note: all usage of this function should be audited due to the existence of symlinks.892/// Note: all usage of this function should be audited due to the existence of symlinks.
524/// Without performing actual syscalls, resolving `..` could be incorrect.893/// Without performing actual syscalls, resolving `..` could be incorrect.
525/// This API may break in the future: https://github.com/ziglang/zig/issues/13613894/// This API may break in the future: https://github.com/ziglang/zig/issues/13613
526pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {895pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
527 assert(paths.len > 0);896 // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
528897 // (we use `* 3` because stackFallback uses 1 usize as a length)
529 // determine which disk designator we will result with, if any898 var bit_set_allocator_state = std.heap.stackFallback(@sizeOf(usize) * 3, allocator);
530 var result_drive_buf = "_:".*;899 const bit_set_allocator = bit_set_allocator_state.get();
531 var disk_designator: []const u8 = "";900 var relevant_paths = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(bit_set_allocator, paths.len);
532 var drive_kind = WindowsPath.Kind.None;901 defer relevant_paths.deinit(bit_set_allocator);
533 var have_abs_path = false;902
534 var first_index: usize = 0;903 // Iterate the paths backwards, marking the relevant paths along the way.
535 for (paths, 0..) |p, i| {904 // This also allows us to break from the loop whenever any earlier paths are known to be irrelevant.
536 const parsed = windowsParsePath(p);905 var first_path_i: usize = paths.len;
537 if (parsed.is_abs) {906 const effective_root_path: WindowsPath2(u8) = root: {
538 have_abs_path = true;907 var last_effective_root_path: WindowsPath2(u8) = .{ .kind = .relative, .root = "" };
539 first_index = i;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 }
540 }971 }
541 switch (parsed.kind) {972 // After iterating, if the pending effective root is drive-relative then that means
542 .Drive => {973 // nothing has led to forcing a drive-absolute root (a path that allows resolving the
543 result_drive_buf[0] = ascii.toUpper(parsed.disk_designator[0]);974 // drive-specific CWD would cause an early break), so we now need to ignore all paths
544 disk_designator = result_drive_buf[0..];975 // before the most recent drive-relative one. For example, if we're resolving
545 drive_kind = WindowsPath.Kind.Drive;976 // { "\\rooted", "relative", "C:drive-relative" }
546 },977 // then the `\rooted` and `relative` needs to be ignored since we can't
547 .NetworkShare => {978 // know what the rooted path is rooted against as that'd require knowing the CWD.
548 disk_designator = parsed.disk_designator;979 if (last_effective_root_path.kind == .drive_relative) {
549 drive_kind = WindowsPath.Kind.NetworkShare;980 for (0..last_drive_relative_path_i) |i| {
550 },981 relevant_paths.unset(i);
551 .None => {},982 }
552 }983 }
553 }984 break :root last_effective_root_path;
985 };
554986
555 // if we will result with a disk designator, loop again to determine987 var result: std.ArrayList(u8) = .empty;
556 // which is the last time the disk designator is absolutely specified, if any988 defer result.deinit(allocator);
557 // and count up the max bytes for paths related to this disk designator989
558 if (drive_kind != WindowsPath.Kind.None) {990 var want_path_sep_between_root_and_component = false;
559 have_abs_path = false;991 switch (effective_root_path.kind) {
560 first_index = 0;992 .root_local_device, .local_device => {
561 var correct_disk_designator = false;993 try result.ensureUnusedCapacity(allocator, 3);
562994 result.appendSliceAssumeCapacity("\\\\");
563 for (paths, 0..) |p, i| {995 result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
564 const parsed = windowsParsePath(p);996 want_path_sep_between_root_and_component = true;
565 if (parsed.kind != WindowsPath.Kind.None) {997 },
566 if (parsed.kind == drive_kind) {998 .drive_absolute, .drive_relative => {
567 correct_disk_designator = compareDiskDesignators(drive_kind, disk_designator, parsed.disk_designator);999 try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);
568 } else {1000 result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
569 continue;1001 result.appendAssumeCapacity(':');
570 }1002 if (effective_root_path.kind == .drive_absolute) {
1003 result.appendAssumeCapacity('\\');
571 }1004 }
572 if (!correct_disk_designator) {1005 },
573 continue;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;
574 }1023 }
575 if (parsed.is_abs) {1024 if (unc.share.len > 0) {
576 first_index = i;1025 result.appendSliceAssumeCapacity(unc.share);
577 have_abs_path = true;1026 if (unc.sep_after_share)
1027 result.appendAssumeCapacity('\\')
1028 else
1029 want_path_sep_between_root_and_component = true;
578 }1030 }
579 }1031 },
1032 .rooted => {
1033 try result.append(allocator, '\\');
1034 },
1035 .relative => {},
580 }1036 }
5811037
582 // Allocate result and fill in the disk designator.1038 const root_len = result.items.len;
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;
613 var negative_count: usize = 0;1039 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| {1043 const parsed = parsePathWindows(u8, path);
616 const parsed = windowsParsePath(p);1044 const skip_len = parsed.root.len;
6171045 var it = mem.tokenizeAny(u8, path[skip_len..], "/\\");
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..], "/\\");
630 while (it.next()) |component| {1046 while (it.next()) |component| {
631 if (mem.eql(u8, component, ".")) {1047 if (mem.eql(u8, component, ".")) {
632 continue;1048 continue;
633 } else if (mem.eql(u8, component, "..")) {1049 } 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)) {
635 negative_count += 1;1051 negative_count += 1;
636 continue;1052 continue;
637 }1053 }
638 while (true) {1054 while (true) {
639 if (result.items.len == disk_designator_len) {1055 if (result.items.len == root_len) {
640 break;1056 break;
641 }1057 }
642 const end_with_sep = switch (result.items[result.items.len - 1]) {1058 const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
643 '\\', '/' => true,
644 else => false,
645 };
646 result.items.len -= 1;1059 result.items.len -= 1;
647 if (end_with_sep or result.items.len == 0) break;1060 if (end_with_sep) break;
648 }1061 }
649 } else if (!have_abs_path and result.items.len == 0) {1062 } else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {
650 try result.appendSlice(component);1063 try result.appendSlice(allocator, component);
651 } else {1064 } else {
652 try result.ensureUnusedCapacity(1 + component.len);1065 try result.ensureUnusedCapacity(allocator, 1 + component.len);
653 result.appendAssumeCapacity('\\');1066 result.appendAssumeCapacity('\\');
654 result.appendSliceAssumeCapacity(component);1067 result.appendSliceAssumeCapacity(component);
655 }1068 }
656 }1069 }
657 }1070 }
6581071
659 if (disk_designator_len != 0 and result.items.len == disk_designator_len) {1072 if (root_len != 0 and result.items.len == root_len and negative_count == 0) {
660 try result.append('\\');1073 return result.toOwnedSlice(allocator);
661 return result.toOwnedSlice();
662 }1074 }
6631075
664 if (result.items.len == 0) {1076 if (result.items.len == root_len) {
665 if (negative_count == 0) {1077 if (negative_count == 0) {
666 return allocator.dupe(u8, ".");1078 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;
677 }1079 }
678 }
6791080
680 if (negative_count == 0) {1081 try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);
681 return result.toOwnedSlice();1082 for (0..negative_count - 1) |_| {
1083 result.appendSliceAssumeCapacity("..\\");
1084 }
1085 result.appendSliceAssumeCapacity("..");
682 } else {1086 } else {
683 const real_result = try allocator.alloc(u8, 3 * negative_count + result.items.len);1087 const dest = try result.addManyAt(allocator, root_len, 3 * negative_count);
684 var count = negative_count;1088 for (0..negative_count) |i| {
685 var i: usize = 0;1089 dest[i * 3 ..][0..3].* = "..\\".*;
686 while (count > 0) : (count -= 1) {
687 real_result[i..][0..3].* = "..\\".*;
688 i += 3;
689 }1090 }
690 @memcpy(real_result[i..][0..result.items.len], result.items);
691 return real_result;
692 }1091 }
1092
1093 return result.toOwnedSlice(allocator);
693}1094}
6941095
695/// This function is like a series of `cd` statements executed one after another.1096/// 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.
697/// The result does not have a trailing path separator.1100/// The result does not have a trailing path separator.
698/// This function does not perform any syscalls. Executing this series of path1101/// This function does not perform any syscalls. Executing this series of path
699/// lookups on the actual filesystem may produce different results due to1102/// 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...@@ -772,10 +1175,14 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
772}1175}
7731176
774test resolve {1177test resolve {
1178 try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");
1179 try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");
775 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");1180 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
776 try testResolveWindows(&[_][]const u8{"."}, ".");1181 try testResolveWindows(&[_][]const u8{"."}, ".");
777 try testResolveWindows(&[_][]const u8{""}, ".");1182 try testResolveWindows(&[_][]const u8{""}, ".");
7781183
1184 try testResolvePosix(&[_][]const u8{ "a", "../../.." }, "../..");
1185 try testResolvePosix(&[_][]const u8{ "..", "", "../../foo" }, "../../../foo");
779 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");1186 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
780 try testResolvePosix(&[_][]const u8{"."}, ".");1187 try testResolvePosix(&[_][]const u8{"."}, ".");
781 try testResolvePosix(&[_][]const u8{""}, ".");1188 try testResolvePosix(&[_][]const u8{""}, ".");
...@@ -792,22 +1199,81 @@ test resolveWindows {...@@ -792,22 +1199,81 @@ test resolveWindows {
792 );1199 );
7931200
794 try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok");1201 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");
795 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");1203 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");
796 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");1204 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");
797 try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");1205 try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");
798 try testResolveWindows(&[_][]const u8{ "c:/ignore", "c:/some/file" }, "C:\\some\\file");1206 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");
800 try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");1209 try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
801 try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");1210 try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
802 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "C:\\");1211 try testResolveWindows(&[_][]const u8{ "\\\\server/share/ignore", "//server/share/bar" }, "\\\\server\\share\\bar");
803 try testResolveWindows(&[_][]const u8{ "c:/", "//dir" }, "C:\\dir");1212 try testResolveWindows(&[_][]const u8{ "\\/server\\share/", "..", "relative" }, "\\\\server\\share\\relative");
804 try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share\\");1213 try testResolveWindows(&[_][]const u8{ "\\\\server\\share", "C:drive-relative" }, "C:drive-relative");
805 try testResolveWindows(&[_][]const u8{ "c:/", "//server//share" }, "\\\\server\\share\\");1214 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "\\\\");
806 try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "C:\\some\\dir");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");
807 try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js");1220 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
809 // Keep relative paths relative.1260 // Keep relative paths relative.
810 try testResolveWindows(&[_][]const u8{"a/b"}, "a\\b");1261 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");
811}1277}
8121278
813test resolvePosix {1279test resolvePosix {
...@@ -855,63 +1321,18 @@ pub fn dirname(path: []const u8) ?[]const u8 {...@@ -855,63 +1321,18 @@ pub fn dirname(path: []const u8) ?[]const u8 {
855}1321}
8561322
857pub fn dirnameWindows(path: []const u8) ?[]const u8 {1323pub fn dirnameWindows(path: []const u8) ?[]const u8 {
858 if (path.len == 0)1324 return dirnameInner(.windows, path);
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];
889}1325}
8901326
891pub fn dirnamePosix(path: []const u8) ?[]const u8 {1327pub fn dirnamePosix(path: []const u8) ?[]const u8 {
892 if (path.len == 0)1328 return dirnameInner(.posix, path);
893 return null;1329}
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;
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;
915}1336}
9161337
917test dirnamePosix {1338test dirnamePosix {
...@@ -930,11 +1351,12 @@ test dirnamePosix {...@@ -930,11 +1351,12 @@ test dirnamePosix {
9301351
931test dirnameWindows {1352test dirnameWindows {
932 try testDirnameWindows("c:\\", null);1353 try testDirnameWindows("c:\\", null);
1354 try testDirnameWindows("c:\\\\", null);
933 try testDirnameWindows("c:\\foo", "c:\\");1355 try testDirnameWindows("c:\\foo", "c:\\");
934 try testDirnameWindows("c:\\foo\\", "c:\\");1356 try testDirnameWindows("c:\\\\foo\\", "c:\\");
935 try testDirnameWindows("c:\\foo\\bar", "c:\\foo");1357 try testDirnameWindows("c:\\foo\\bar", "c:\\foo");
936 try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");1358 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");
938 try testDirnameWindows("\\", null);1360 try testDirnameWindows("\\", null);
939 try testDirnameWindows("\\foo", "\\");1361 try testDirnameWindows("\\foo", "\\");
940 try testDirnameWindows("\\foo\\", "\\");1362 try testDirnameWindows("\\foo\\", "\\");
...@@ -942,19 +1364,30 @@ test dirnameWindows {...@@ -942,19 +1364,30 @@ test dirnameWindows {
942 try testDirnameWindows("\\foo\\bar\\", "\\foo");1364 try testDirnameWindows("\\foo\\bar\\", "\\foo");
943 try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");1365 try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
944 try testDirnameWindows("c:", null);1366 try testDirnameWindows("c:", null);
945 try testDirnameWindows("c:foo", null);1367 try testDirnameWindows("c:foo", "c:");
946 try testDirnameWindows("c:foo\\", null);1368 try testDirnameWindows("c:foo\\", "c:");
947 try testDirnameWindows("c:foo\\bar", "c:foo");1369 try testDirnameWindows("c:foo\\bar", "c:foo");
948 try testDirnameWindows("c:foo\\bar\\", "c:foo");1370 try testDirnameWindows("c:foo\\bar\\", "c:foo");
949 try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");1371 try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
950 try testDirnameWindows("file:stream", null);1372 try testDirnameWindows("file:stream", null);
951 try testDirnameWindows("dir\\file:stream", "dir");1373 try testDirnameWindows("dir\\file:stream", "dir");
952 try testDirnameWindows("\\\\unc\\share", null);1374 try testDirnameWindows("\\\\unc\\share", null);
1375 try testDirnameWindows("\\\\unc\\share\\\\", null);
953 try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");1376 try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
954 try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");1377 try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
955 try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");1378 try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
956 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");1379 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
957 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");1380 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");
958 try testDirnameWindows("/a/b/", "/a");1391 try testDirnameWindows("/a/b/", "/a");
959 try testDirnameWindows("/a/b", "/a");1392 try testDirnameWindows("/a/b", "/a");
960 try testDirnameWindows("/a", "/");1393 try testDirnameWindows("/a", "/");
...@@ -974,7 +1407,7 @@ fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {...@@ -974,7 +1407,7 @@ fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {
9741407
975fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {1408fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {
976 if (dirnameWindows(input)) |output| {1409 if (dirnameWindows(input)) |output| {
977 try testing.expect(mem.eql(u8, output, expected_output.?));1410 try testing.expectEqualStrings(expected_output.?, output);
978 } else {1411 } else {
979 try testing.expect(expected_output == null);1412 try testing.expect(expected_output == null);
980 }1413 }
...@@ -989,56 +1422,17 @@ pub fn basename(path: []const u8) []const u8 {...@@ -989,56 +1422,17 @@ pub fn basename(path: []const u8) []const u8 {
989}1422}
9901423
991pub fn basenamePosix(path: []const u8) []const u8 {1424pub fn basenamePosix(path: []const u8) []const u8 {
992 if (path.len == 0)1425 return basenameInner(.posix, path);
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];
1010}1426}
10111427
1012pub fn basenameWindows(path: []const u8) []const u8 {1428pub fn basenameWindows(path: []const u8) []const u8 {
1013 if (path.len == 0)1429 return basenameInner(.windows, path);
1014 return &[_]u8{};1430}
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 }
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;
1042}1436}
10431437
1044test basename {1438test basename {
...@@ -1053,7 +1447,9 @@ test basename {...@@ -1053,7 +1447,9 @@ test basename {
1053 try testBasename("/aaa/", "aaa");1447 try testBasename("/aaa/", "aaa");
1054 try testBasename("/aaa/b", "b");1448 try testBasename("/aaa/b", "b");
1055 try testBasename("/a/b", "b");1449 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
1058 try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");1454 try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
1059 try testBasenamePosix("\\basename.ext", "\\basename.ext");1455 try testBasenamePosix("\\basename.ext", "\\basename.ext");
...@@ -1076,6 +1472,12 @@ test basename {...@@ -1076,6 +1472,12 @@ test basename {
1076 try testBasenameWindows("C:basename.ext", "basename.ext");1472 try testBasenameWindows("C:basename.ext", "basename.ext");
1077 try testBasenameWindows("C:basename.ext\\", "basename.ext");1473 try testBasenameWindows("C:basename.ext\\", "basename.ext");
1078 try testBasenameWindows("C:basename.ext\\\\", "basename.ext");1474 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");
1079 try testBasenameWindows("C:foo", "foo");1481 try testBasenameWindows("C:foo", "foo");
1080 try testBasenameWindows("file:stream", "file:stream");1482 try testBasenameWindows("file:stream", "file:stream");
1081}1483}
...@@ -1092,11 +1494,15 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {...@@ -1092,11 +1494,15 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
1092 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));1494 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
1093}1495}
10941496
1497pub const RelativeError = std.process.GetCwdAllocError;
1498
1095/// Returns the relative path from `from` to `to`. If `from` and `to` each1499/// Returns the relative path from `from` to `to`. If `from` and `to` each
1096/// resolve to the same path (after calling `resolve` on each), a zero-length1500/// resolve to the same path (after calling `resolve` on each), a zero-length
1097/// string is returned.1501/// string is returned.
1098/// On Windows this canonicalizes the drive to a capital letter and paths to `\\`.1502/// On Windows, the result is not guaranteed to be relative, as the paths may be
1099pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {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 {
1100 if (native_os == .windows) {1506 if (native_os == .windows) {
1101 return relativeWindows(allocator, from, to);1507 return relativeWindows(allocator, from, to);
1102 } else {1508 } else {
...@@ -1105,30 +1511,53 @@ pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {...@@ -1105,30 +1511,53 @@ pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1105}1511}
11061512
1107pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {1513pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1108 const cwd = try process.getCwdAlloc(allocator);1514 if (native_os != .windows) @compileError("this function relies on Windows-specific semantics");
1109 defer allocator.free(cwd);
1110 const resolved_from = try resolveWindows(allocator, &[_][]const u8{ cwd, from });
1111 defer allocator.free(resolved_from);
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);
1113 var clean_up_resolved_to = true;1541 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);
1115 defer if (clean_up_resolved_to) allocator.free(resolved_to);1543 defer if (clean_up_resolved_to) allocator.free(resolved_to);
11161544
1117 const parsed_from = windowsParsePath(resolved_from);1545 const parsed_resolved_from = parsePathWindows(u8, resolved_from);
1118 const parsed_to = windowsParsePath(resolved_to);1546 const parsed_resolved_to = parsePathWindows(u8, resolved_to);
1547
1119 const result_is_to = x: {1548 const result_is_to = x: {
1120 if (parsed_from.kind != parsed_to.kind) {1549 if (parsed_resolved_from.kind != parsed_resolved_to.kind) {
1121 break :x true;1550 break :x true;
1122 } else switch (parsed_from.kind) {1551 }
1123 .NetworkShare => {1552 switch (parsed_resolved_from.kind) {
1124 break :x !networkShareServersEql(parsed_to.disk_designator, parsed_from.disk_designator);1553 .drive_absolute, .drive_relative => {
1125 },1554 break :x !compareDiskDesignators(u8, .drive, parsed_resolved_from.root, parsed_resolved_to.root);
1126 .Drive => {
1127 break :x ascii.toUpper(parsed_from.disk_designator[0]) != ascii.toUpper(parsed_to.disk_designator[0]);
1128 },1555 },
1129 .None => {1556 .unc_absolute => {
1130 break :x false;1557 break :x !compareDiskDesignators(u8, .unc, parsed_resolved_from.root, parsed_resolved_to.root);
1131 },1558 },
1559 .relative, .rooted, .local_device => break :x false,
1560 .root_local_device => break :x true,
1132 }1561 }
1133 };1562 };
11341563
...@@ -1137,8 +1566,8 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !...@@ -1137,8 +1566,8 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !
1137 return resolved_to;1566 return resolved_to;
1138 }1567 }
11391568
1140 var from_it = mem.tokenizeAny(u8, resolved_from, "/\\");1569 var from_it = mem.tokenizeAny(u8, resolved_from[parsed_resolved_from.root.len..], "/\\");
1141 var to_it = mem.tokenizeAny(u8, resolved_to, "/\\");1570 var to_it = mem.tokenizeAny(u8, resolved_to[parsed_resolved_to.root.len..], "/\\");
1142 while (true) {1571 while (true) {
1143 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());1572 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
1144 const to_rest = to_it.rest();1573 const to_rest = to_it.rest();
...@@ -1170,11 +1599,101 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !...@@ -1170,11 +1599,101 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !
11701599
1171 return allocator.realloc(result, result_index);1600 return allocator.realloc(result, result_index);
1172 }1601 }
1173
1174 return [_]u8{};1602 return [_]u8{};
1175}1603}
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
1177pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {1694pub 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
1178 const cwd = try process.getCwdAlloc(allocator);1697 const cwd = try process.getCwdAlloc(allocator);
1179 defer allocator.free(cwd);1698 defer allocator.free(cwd);
1180 const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });1699 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) ![]...@@ -1217,51 +1736,59 @@ pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]
1217}1736}
12181737
1219test relative {1738test relative {
1220 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");1739 if (native_os == .windows) {
1221 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");1740 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
1222 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");1741 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
1223 try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");1742 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
1224 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");1743 try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
1225 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");1744 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
1226 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");1745 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
1227 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");1746 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
1228 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");1747 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
1229 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");1748 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
1230 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");1749 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
1231 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");1750 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
1232 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");1751 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
1233 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");1752 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
1234 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");1753 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
1235 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");1754 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
1236 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");1755 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
1237 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");1756 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
1238 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");1757 try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz");
1239 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");1758 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
1240 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz");1759 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
1241 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux");1760 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
1242 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");1761 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz");
1243 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");1762 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux");
12441763 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
1245 try testRelativeWindows("a/b/c", "a\\b", "..");1764 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
1246 try testRelativeWindows("a/b/c", "a", "..\\..");1765
1247 try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");1766 try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo");
12481767 try testRelativeWindows("c:foo", "c:foo\\bar", "bar");
1249 try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");1768 try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo");
1250 // Unicode-aware case-insensitive path comparison1769 try testRelativeWindows("\\foo", "\\foo\\bar", "bar");
1251 try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");1770
12521771 try testRelativeWindows("a/b/c", "a\\b", "..");
1253 try testRelativePosix("/var/lib", "/var", "..");1772 try testRelativeWindows("a/b/c", "a", "..\\..");
1254 try testRelativePosix("/var/lib", "/bin", "../../bin");1773 try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");
1255 try testRelativePosix("/var/lib", "/var/lib", "");1774
1256 try testRelativePosix("/var/lib", "/var/apache", "../apache");1775 try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
1257 try testRelativePosix("/var/", "/var/lib", "lib");1776 // Unicode-aware case-insensitive path comparison
1258 try testRelativePosix("/", "/var/lib", "var/lib");1777 try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");
1259 try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");1778 } else {
1260 try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");1779 try testRelativePosix("/var/lib", "/var", "..");
1261 try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");1780 try testRelativePosix("/var/lib", "/bin", "../../bin");
1262 try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");1781 try testRelativePosix("/var/lib", "/var/lib", "");
1263 try testRelativePosix("/baz-quux", "/baz", "../baz");1782 try testRelativePosix("/var/lib", "/var/apache", "../apache");
1264 try testRelativePosix("/baz", "/baz-quux", "../baz-quux");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 }
1265}1792}
12661793
1267fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {1794fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
...@@ -1391,7 +1918,10 @@ test stem {...@@ -1391,7 +1918,10 @@ test stem {
1391pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {1918pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
1392 return struct {1919 return struct {
1393 path: []const T,1920 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,
1395 start_index: usize = 0,1925 start_index: usize = 0,
1396 end_index: usize = 0,1926 end_index: usize = 0,
13971927
...@@ -1406,100 +1936,45 @@ pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {...@@ -1406,100 +1936,45 @@ pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
1406 path: []const T,1936 path: []const T,
1407 };1937 };
14081938
1409 const InitError = switch (path_type) {
1410 .windows => error{BadPathName},
1411 else => error{},
1412 };
1413
1414 /// After `init`, `next` will return the first component after the root1939 /// After `init`, `next` will return the first component after the root
1415 /// (there is no need to call `first` after `init`).1940 /// (there is no need to call `first` after `init`).
1416 /// To iterate backwards (from the end of the path to the beginning), call `last`1941 /// To iterate backwards (from the end of the path to the beginning), call `last`
1417 /// after `init` and then iterate via `previous` calls.1942 /// after `init` and then iterate via `previous` calls.
1418 /// For Windows paths, `error.BadPathName` is returned if the `path` has an explicit1943 /// For Windows paths, paths are assumed to be in the Win32 namespace.
1419 /// namespace prefix (`\\.\`, `\\?\`, or `\??\`) or if it is a UNC path with more1944 pub fn init(path: []const T) Self {
1420 /// than two path separators at the beginning.1945 const root_len: usize = switch (path_type) {
1421 pub fn init(path: []const T) InitError!Self {
1422 const root_end_index: usize = switch (path_type) {
1423 .posix, .uefi => posix: {1946 .posix, .uefi => posix: {
1424 // Root on UEFI and POSIX only differs by the path separator1947 // Root on UEFI and POSIX only differs by the path separator
1425 var root_end_index: usize = 0;1948 break :posix if (path.len > 0 and path_type.isSep(T, path[0])) 1 else 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;
1432 },1949 },
1433 .windows => windows: {1950 .windows => windows: {
1434 // Namespaces other than the Win32 file namespace are tricky1951 break :windows parsePathWindows(T, path).root.len;
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 };
1486 },1952 },
1487 };1953 };
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 }
1488 return .{1962 return .{
1489 .path = path,1963 .path = path,
1964 .root_len = root_len,
1490 .root_end_index = root_end_index,1965 .root_end_index = root_end_index,
1491 .start_index = root_end_index,1966 .start_index = root_end_index,
1492 .end_index = root_end_index,1967 .end_index = root_end_index,
1493 };1968 };
1494 }1969 }
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.
1497 /// For POSIX paths, this will be `/`.1972 /// For POSIX paths, this will be `/`.
1498 /// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.1973 /// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.
1499 /// For UEFI paths, this will be `\`.1974 /// For UEFI paths, this will be `\`.
1500 pub fn root(self: Self) ?[]const T {1975 pub fn root(self: Self) ?[]const T {
1501 if (self.root_end_index == 0) return null;1976 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];
1503 }1978 }
15041979
1505 /// Returns the first component (from the beginning of the path).1980 /// Returns the first component (from the beginning of the path).
...@@ -1614,7 +2089,7 @@ pub const NativeComponentIterator = ComponentIterator(switch (native_os) {...@@ -1614,7 +2089,7 @@ pub const NativeComponentIterator = ComponentIterator(switch (native_os) {
1614 else => .posix,2089 else => .posix,
1615}, u8);2090}, u8);
16162091
1617pub fn componentIterator(path: []const u8) !NativeComponentIterator {2092pub fn componentIterator(path: []const u8) NativeComponentIterator {
1618 return NativeComponentIterator.init(path);2093 return NativeComponentIterator.init(path);
1619}2094}
16202095
...@@ -1622,8 +2097,9 @@ test "ComponentIterator posix" {...@@ -1622,8 +2097,9 @@ test "ComponentIterator posix" {
1622 const PosixComponentIterator = ComponentIterator(.posix, u8);2097 const PosixComponentIterator = ComponentIterator(.posix, u8);
1623 {2098 {
1624 const path = "a/b/c/";2099 const path = "a/b/c/";
1625 var it = try PosixComponentIterator.init(path);2100 var it = PosixComponentIterator.init(path);
1626 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);2101 try std.testing.expectEqual(0, it.root_len);
2102 try std.testing.expectEqual(0, it.root_end_index);
1627 try std.testing.expect(null == it.root());2103 try std.testing.expect(null == it.root());
1628 {2104 {
1629 try std.testing.expect(null == it.previous());2105 try std.testing.expect(null == it.previous());
...@@ -1669,8 +2145,9 @@ test "ComponentIterator posix" {...@@ -1669,8 +2145,9 @@ test "ComponentIterator posix" {
16692145
1670 {2146 {
1671 const path = "/a/b/c/";2147 const path = "/a/b/c/";
1672 var it = try PosixComponentIterator.init(path);2148 var it = PosixComponentIterator.init(path);
1673 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);2149 try std.testing.expectEqual(1, it.root_len);
2150 try std.testing.expectEqual(1, it.root_end_index);
1674 try std.testing.expectEqualStrings("/", it.root().?);2151 try std.testing.expectEqualStrings("/", it.root().?);
1675 {2152 {
1676 try std.testing.expect(null == it.previous());2153 try std.testing.expect(null == it.previous());
...@@ -1714,10 +2191,59 @@ test "ComponentIterator posix" {...@@ -1714,10 +2191,59 @@ test "ComponentIterator posix" {
1714 }2191 }
1715 }2192 }
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
1717 {2242 {
1718 const path = "/";2243 const path = "/";
1719 var it = try PosixComponentIterator.init(path);2244 var it = PosixComponentIterator.init(path);
1720 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);2245 try std.testing.expectEqual(1, it.root_len);
2246 try std.testing.expectEqual(1, it.root_end_index);
1721 try std.testing.expectEqualStrings("/", it.root().?);2247 try std.testing.expectEqualStrings("/", it.root().?);
17222248
1723 try std.testing.expect(null == it.first());2249 try std.testing.expect(null == it.first());
...@@ -1733,8 +2259,9 @@ test "ComponentIterator posix" {...@@ -1733,8 +2259,9 @@ test "ComponentIterator posix" {
17332259
1734 {2260 {
1735 const path = "";2261 const path = "";
1736 var it = try PosixComponentIterator.init(path);2262 var it = PosixComponentIterator.init(path);
1737 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);2263 try std.testing.expectEqual(0, it.root_len);
2264 try std.testing.expectEqual(0, it.root_end_index);
1738 try std.testing.expect(null == it.root());2265 try std.testing.expect(null == it.root());
17392266
1740 try std.testing.expect(null == it.first());2267 try std.testing.expect(null == it.first());
...@@ -1753,8 +2280,9 @@ test "ComponentIterator windows" {...@@ -1753,8 +2280,9 @@ test "ComponentIterator windows" {
1753 const WindowsComponentIterator = ComponentIterator(.windows, u8);2280 const WindowsComponentIterator = ComponentIterator(.windows, u8);
1754 {2281 {
1755 const path = "a/b\\c//";2282 const path = "a/b\\c//";
1756 var it = try WindowsComponentIterator.init(path);2283 var it = WindowsComponentIterator.init(path);
1757 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);2284 try std.testing.expectEqual(0, it.root_len);
2285 try std.testing.expectEqual(0, it.root_end_index);
1758 try std.testing.expect(null == it.root());2286 try std.testing.expect(null == it.root());
1759 {2287 {
1760 try std.testing.expect(null == it.previous());2288 try std.testing.expect(null == it.previous());
...@@ -1800,8 +2328,9 @@ test "ComponentIterator windows" {...@@ -1800,8 +2328,9 @@ test "ComponentIterator windows" {
18002328
1801 {2329 {
1802 const path = "C:\\a/b/c/";2330 const path = "C:\\a/b/c/";
1803 var it = try WindowsComponentIterator.init(path);2331 var it = WindowsComponentIterator.init(path);
1804 try std.testing.expectEqual(@as(usize, 3), it.root_end_index);2332 try std.testing.expectEqual(3, it.root_len);
2333 try std.testing.expectEqual(3, it.root_end_index);
1805 try std.testing.expectEqualStrings("C:\\", it.root().?);2334 try std.testing.expectEqualStrings("C:\\", it.root().?);
1806 {2335 {
1807 const first = it.first().?;2336 const first = it.first().?;
...@@ -1835,10 +2364,49 @@ test "ComponentIterator windows" {...@@ -1835,10 +2364,49 @@ test "ComponentIterator windows" {
1835 }2364 }
1836 }2365 }
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
1838 {2405 {
1839 const path = "/";2406 const path = "/";
1840 var it = try WindowsComponentIterator.init(path);2407 var it = WindowsComponentIterator.init(path);
1841 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);2408 try std.testing.expectEqual(1, it.root_len);
2409 try std.testing.expectEqual(1, it.root_end_index);
1842 try std.testing.expectEqualStrings("/", it.root().?);2410 try std.testing.expectEqualStrings("/", it.root().?);
18432411
1844 try std.testing.expect(null == it.first());2412 try std.testing.expect(null == it.first());
...@@ -1854,8 +2422,9 @@ test "ComponentIterator windows" {...@@ -1854,8 +2422,9 @@ test "ComponentIterator windows" {
18542422
1855 {2423 {
1856 const path = "";2424 const path = "";
1857 var it = try WindowsComponentIterator.init(path);2425 var it = WindowsComponentIterator.init(path);
1858 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);2426 try std.testing.expectEqual(0, it.root_len);
2427 try std.testing.expectEqual(0, it.root_end_index);
1859 try std.testing.expect(null == it.root());2428 try std.testing.expect(null == it.root());
18602429
1861 try std.testing.expect(null == it.first());2430 try std.testing.expect(null == it.first());
...@@ -1871,17 +2440,13 @@ test "ComponentIterator windows" {...@@ -1871,17 +2440,13 @@ test "ComponentIterator windows" {
1871}2440}
18722441
1873test "ComponentIterator windows WTF-16" {2442test "ComponentIterator windows WTF-16" {
1874 // TODO: Fix on big endian architectures
1875 if (builtin.cpu.arch.endian() != .little) {
1876 return error.SkipZigTest;
1877 }
1878
1879 const WindowsComponentIterator = ComponentIterator(.windows, u16);2443 const WindowsComponentIterator = ComponentIterator(.windows, u16);
1880 const L = std.unicode.utf8ToUtf16LeStringLiteral;2444 const L = std.unicode.utf8ToUtf16LeStringLiteral;
18812445
1882 const path = L("C:\\a/b/c/");2446 const path = L("C:\\a/b/c/");
1883 var it = try WindowsComponentIterator.init(path);2447 var it = WindowsComponentIterator.init(path);
1884 try std.testing.expectEqual(@as(usize, 3), it.root_end_index);2448 try std.testing.expectEqual(3, it.root_len);
2449 try std.testing.expectEqual(3, it.root_end_index);
1885 try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);2450 try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);
1886 {2451 {
1887 const first = it.first().?;2452 const first = it.first().?;
...@@ -1918,55 +2483,60 @@ test "ComponentIterator windows WTF-16" {...@@ -1918,55 +2483,60 @@ test "ComponentIterator windows WTF-16" {
1918test "ComponentIterator roots" {2483test "ComponentIterator roots" {
1919 // UEFI2484 // UEFI
1920 {2485 {
1921 var it = try ComponentIterator(.uefi, u8).init("\\\\a");2486 var it = ComponentIterator(.uefi, u8).init("\\\\a");
1922 try std.testing.expectEqualStrings("\\\\", it.root().?);2487 try std.testing.expectEqualStrings("\\", it.root().?);
19232488
1924 it = try ComponentIterator(.uefi, u8).init("//a");2489 it = ComponentIterator(.uefi, u8).init("//a");
1925 try std.testing.expect(null == it.root());2490 try std.testing.expect(null == it.root());
1926 }2491 }
1927 // POSIX2492 // POSIX
1928 {2493 {
1929 var it = try ComponentIterator(.posix, u8).init("//a");2494 var it = ComponentIterator(.posix, u8).init("//a");
1930 try std.testing.expectEqualStrings("//", it.root().?);2495 try std.testing.expectEqualStrings("/", it.root().?);
19312496
1932 it = try ComponentIterator(.posix, u8).init("\\\\a");2497 it = ComponentIterator(.posix, u8).init("\\\\a");
1933 try std.testing.expect(null == it.root());2498 try std.testing.expect(null == it.root());
1934 }2499 }
1935 // Windows2500 // Windows
1936 {2501 {
1937 // Drive relative2502 // Drive relative
1938 var it = try ComponentIterator(.windows, u8).init("C:a");2503 var it = ComponentIterator(.windows, u8).init("C:a");
1939 try std.testing.expectEqualStrings("C:", it.root().?);2504 try std.testing.expectEqualStrings("C:", it.root().?);
19402505
1941 // Drive absolute2506 // Drive absolute
1942 it = try ComponentIterator(.windows, u8).init("C://a");2507 it = ComponentIterator(.windows, u8).init("C:/a");
1943 try std.testing.expectEqualStrings("C://", it.root().?);2508 try std.testing.expectEqualStrings("C:/", it.root().?);
1944 it = try ComponentIterator(.windows, u8).init("C:\\a");2509 it = ComponentIterator(.windows, u8).init("C:\\a");
1945 try std.testing.expectEqualStrings("C:\\", it.root().?);2510 try std.testing.expectEqualStrings("C:\\", it.root().?);
2511 it = ComponentIterator(.windows, u8).init("C:///a");
2512 try std.testing.expectEqualStrings("C:/", it.root().?);
19462513
1947 // Rooted2514 // Rooted
1948 it = try ComponentIterator(.windows, u8).init("\\a");2515 it = ComponentIterator(.windows, u8).init("\\a");
1949 try std.testing.expectEqualStrings("\\", it.root().?);2516 try std.testing.expectEqualStrings("\\", it.root().?);
1950 it = try ComponentIterator(.windows, u8).init("/a");2517 it = ComponentIterator(.windows, u8).init("/a");
1951 try std.testing.expectEqualStrings("/", it.root().?);2518 try std.testing.expectEqualStrings("/", it.root().?);
19522519
1953 // Root local device2520 // Root local device
1954 it = try ComponentIterator(.windows, u8).init("\\\\.");2521 it = ComponentIterator(.windows, u8).init("\\\\.");
1955 try std.testing.expectEqualStrings("\\\\.", it.root().?);2522 try std.testing.expectEqualStrings("\\\\.", it.root().?);
1956 it = try ComponentIterator(.windows, u8).init("//?");2523 it = ComponentIterator(.windows, u8).init("//?");
1957 try std.testing.expectEqualStrings("//?", it.root().?);2524 try std.testing.expectEqualStrings("//?", it.root().?);
19582525
1959 // UNC absolute2526 // UNC absolute
1960 it = try ComponentIterator(.windows, u8).init("//");2527 it = ComponentIterator(.windows, u8).init("//");
1961 try std.testing.expectEqualStrings("//", it.root().?);2528 try std.testing.expectEqualStrings("//", it.root().?);
1962 it = try ComponentIterator(.windows, u8).init("\\\\a");2529 it = ComponentIterator(.windows, u8).init("\\\\a");
1963 try std.testing.expectEqualStrings("\\\\a", it.root().?);2530 try std.testing.expectEqualStrings("\\\\a", it.root().?);
1964 it = try ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");2531 it = ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
1965 try std.testing.expectEqualStrings("\\\\a\\b\\\\", it.root().?);2532 try std.testing.expectEqualStrings("\\\\a\\b\\", it.root().?);
1966 it = try ComponentIterator(.windows, u8).init("//a");2533 it = ComponentIterator(.windows, u8).init("//a");
1967 try std.testing.expectEqualStrings("//a", it.root().?);2534 try std.testing.expectEqualStrings("//a", it.root().?);
1968 it = try ComponentIterator(.windows, u8).init("//a/b//c");2535 it = ComponentIterator(.windows, u8).init("//a/b//c");
1969 try std.testing.expectEqualStrings("//a/b//", it.root().?);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().?);
1970 }2540 }
1971}2541}
19722542
lib/std/fs/test.zig+1-1
...@@ -56,7 +56,7 @@ const PathType = enum {...@@ -56,7 +56,7 @@ const PathType = enum {
56 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.56 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
57 var fd_path_buf: [fs.max_path_bytes]u8 = undefined;57 var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
58 const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);58 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);
60 switch (windows_path_type) {60 switch (windows_path_type) {
61 .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),61 .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),
62 .drive_absolute => {62 .drive_absolute => {
lib/std/os/windows.zig+325-262
...@@ -816,8 +816,11 @@ pub fn CreateSymbolicLink(...@@ -816,8 +816,11 @@ pub fn CreateSymbolicLink(
816 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw816 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
817 var is_target_absolute = false;817 var is_target_absolute = false;
818 const final_target_path = target_path: {818 const final_target_path = target_path: {
819 switch (getNamespacePrefix(u16, target_path)) {819 if (hasCommonNtPrefix(u16, target_path)) {
820 .none => switch (getUnprefixedPathType(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)) {
821 // Rooted paths need to avoid getting put through wToPrefixedFileW824 // Rooted paths need to avoid getting put through wToPrefixedFileW
822 // (and they are treated as relative in this context)825 // (and they are treated as relative in this context)
823 // Note: It seems that rooted paths in symbolic links are relative to826 // Note: It seems that rooted paths in symbolic links are relative to
...@@ -829,10 +832,7 @@ pub fn CreateSymbolicLink(...@@ -829,10 +832,7 @@ pub fn CreateSymbolicLink(
829 // Keep relative paths relative, but anything else needs to get NT-prefixed.832 // Keep relative paths relative, but anything else needs to get NT-prefixed.
830 else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))833 else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
831 break :target_path target_path,834 break :target_path target_path,
832 },835 }
833 // Already an NT path, no need to do anything to it
834 .nt => break :target_path target_path,
835 else => {},
836 }836 }
837 var prefixed_target_path = try wToPrefixedFileW(dir, target_path);837 var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
838 // We do this after prefixing to ensure that drive-relative paths are treated as absolute838 // 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 {...@@ -2145,7 +2145,7 @@ pub fn nanoSecondsToFileTime(ns: Io.Timestamp) FILETIME {
2145/// Compares two WTF16 strings using the equivalent functionality of2145/// Compares two WTF16 strings using the equivalent functionality of
2146/// `RtlEqualUnicodeString` (with case insensitive comparison enabled).2146/// `RtlEqualUnicodeString` (with case insensitive comparison enabled).
2147/// This function can be called on any target.2147/// 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 {
2149 if (@inComptime() or builtin.os.tag != .windows) {2149 if (@inComptime() or builtin.os.tag != .windows) {
2150 // This function compares the strings code unit by code unit (aka u16-to-u16),2150 // This function compares the strings code unit by code unit (aka u16-to-u16),
2151 // so any length difference implies inequality. In other words, there's no possible2151 // 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 {...@@ -2222,19 +2222,19 @@ pub fn eqlIgnoreCaseWtf8(a: []const u8, b: []const u8) bool {
22222222
2223fn testEqlIgnoreCase(comptime expect_eql: bool, comptime a: []const u8, comptime b: []const u8) !void {2223fn testEqlIgnoreCase(comptime expect_eql: bool, comptime a: []const u8, comptime b: []const u8) !void {
2224 try std.testing.expectEqual(expect_eql, eqlIgnoreCaseWtf8(a, b));2224 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(
2226 std.unicode.utf8ToUtf16LeStringLiteral(a),2226 std.unicode.utf8ToUtf16LeStringLiteral(a),
2227 std.unicode.utf8ToUtf16LeStringLiteral(b),2227 std.unicode.utf8ToUtf16LeStringLiteral(b),
2228 ));2228 ));
22292229
2230 try comptime std.testing.expect(expect_eql == eqlIgnoreCaseWtf8(a, b));2230 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(
2232 std.unicode.utf8ToUtf16LeStringLiteral(a),2232 std.unicode.utf8ToUtf16LeStringLiteral(a),
2233 std.unicode.utf8ToUtf16LeStringLiteral(b),2233 std.unicode.utf8ToUtf16LeStringLiteral(b),
2234 ));2234 ));
2235}2235}
22362236
2237test "eqlIgnoreCaseWTF16/Wtf8" {2237test "eqlIgnoreCaseWtf16/Wtf8" {
2238 try testEqlIgnoreCase(true, "\x01 a B Λ ɐ", "\x01 A b λ Ɐ");2238 try testEqlIgnoreCase(true, "\x01 a B Λ ɐ", "\x01 A b λ Ɐ");
2239 // does not do case-insensitive comparison for codepoints >= U+100002239 // does not do case-insensitive comparison for codepoints >= U+10000
2240 try testEqlIgnoreCase(false, "𐓏", "𐓷");2240 try testEqlIgnoreCase(false, "𐓏", "𐓷");
...@@ -2365,271 +2365,339 @@ pub const Wtf16ToPrefixedFileWError = error{...@@ -2365,271 +2365,339 @@ pub const Wtf16ToPrefixedFileWError = error{
2365/// - . and space are not stripped from the end of relative paths (potential TODO)2365/// - . and space are not stripped from the end of relative paths (potential TODO)
2366pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {2366pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {
2367 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };2367 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
2368 switch (getNamespacePrefix(u16, path)) {2368 if (hasCommonNtPrefix(u16, path)) {
2369 // TODO: Figure out a way to design an API that can avoid the copy for .nt,2369 // TODO: Figure out a way to design an API that can avoid the copy for NT,
2370 // since it is always returned fully unmodified.2370 // since it is always returned fully unmodified.
2371 .nt, .verbatim => {2371 var path_space: PathSpace = undefined;
2372 var path_space: PathSpace = undefined;2372 path_space.data[0..nt_prefix.len].* = nt_prefix;
2373 path_space.data[0..nt_prefix.len].* = nt_prefix;2373 const len_after_prefix = path.len - nt_prefix.len;
2374 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 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);2375 path_space.len = path.len;
2376 path_space.len = path.len;2376 path_space.data[path_space.len] = 0;
2377 path_space.data[path_space.len] = 0;2377 return path_space;
2378 return path_space;2378 } else {
2379 },2379 const path_type = getWin32PathType(u16, path);
2380 .local_device, .fake_verbatim => {2380 var path_space: PathSpace = undefined;
2381 var path_space: PathSpace = undefined;2381 if (path_type == .local_device) {
2382 const path_byte_len = ntdll.RtlGetFullPathName_U(2382 switch (getLocalDevicePathType(u16, path)) {
2383 path.ptr,2383 .verbatim => {
2384 path_space.data.len * 2,2384 path_space.data[0..nt_prefix.len].* = nt_prefix;
2385 &path_space.data,2385 const len_after_prefix = path.len - nt_prefix.len;
2386 null,2386 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2387 );2387 path_space.len = path.len;
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 };
2421 path_space.data[path_space.len] = 0;2388 path_space.data[path_space.len] = 0;
2422 return path_space;2389 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 },
2424 }2410 }
2425 // We now know we are going to return an absolute NT path, so2411 }
2426 // we can unconditionally prefix it with the NT prefix.2412 relative: {
2427 path_space.data[0..nt_prefix.len].* = nt_prefix;2413 if (path_type == .relative) {
2428 if (path_type == .root_local_device) {2414 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
2429 // `\\.` and `\\?` always get converted to `\??\` exactly, so2415 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
2430 // we can just stop here2416
2431 path_space.len = nt_prefix.len;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 };
2432 path_space.data[path_space.len] = 0;2430 path_space.data[path_space.len] = 0;
2433 return path_space;2431 return path_space;
2434 }2432 }
2435 const path_buf_offset = switch (path_type) {2433 }
2436 // UNC paths will always start with `\\`. However, we want to2434 // We now know we are going to return an absolute NT path, so
2437 // end up with something like `\??\UNC\server\share`, so to get2435 // we can unconditionally prefix it with the NT prefix.
2438 // RtlGetFullPathName to write into the spot we want the `server`2436 path_space.data[0..nt_prefix.len].* = nt_prefix;
2439 // part to end up, we need to provide an offset such that2437 if (path_type == .root_local_device) {
2440 // the `\\` part gets written where the `C\` of `UNC\` will be2438 // `\\.` and `\\?` always get converted to `\??\` exactly, so
2441 // in the final NT path.2439 // we can just stop here
2442 .unc_absolute => nt_prefix.len + 2,2440 path_space.len = nt_prefix.len;
2443 else => nt_prefix.len,2441 path_space.data[path_space.len] = 0;
2444 };2442 return path_space;
2445 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);2443 }
2446 const path_to_get: [:0]const u16 = path_to_get: {2444 const path_buf_offset = switch (path_type) {
2447 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because2445 // UNC paths will always start with `\\`. However, we want to
2448 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.2446 // end up with something like `\??\UNC\server\share`, so to get
2449 if (path_type != .relative or dir == null) {2447 // RtlGetFullPathName to write into the spot we want the `server`
2450 break :path_to_get path;2448 // part to end up, we need to provide an offset such that
2451 }2449 // the `\\` part gets written where the `C\` of `UNC\` will be
2452 // We can also skip GetFinalPathNameByHandle if the handle matches2450 // in the final NT path.
2453 // the handle returned by fs.cwd()2451 .unc_absolute => nt_prefix.len + 2,
2454 if (dir.? == std.fs.cwd().fd) {2452 else => nt_prefix.len,
2455 break :path_to_get path;2453 };
2456 }2454 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
2457 // At this point, we know we have a relative path that had too many2455 const path_to_get: [:0]const u16 = path_to_get: {
2458 // `..` components to be resolved by normalizePath, so we need to2456 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
2459 // convert it into an absolute path and let RtlGetFullPathName_U2457 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
2460 // canonicalize it. We do this by getting the path of the `dir`2458 if (path_type != .relative or dir == null) {
2461 // and appending the relative path to it.2459 break :path_to_get path;
2462 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;2460 }
2463 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {2461 // We can also skip GetFinalPathNameByHandle if the handle matches
2464 // This mapping is not correct; it is actually expected2462 // the handle returned by fs.cwd()
2465 // that calling GetFinalPathNameByHandle might return2463 if (dir.? == std.fs.cwd().fd) {
2466 // error.UnrecognizedVolume, and in fact has been observed2464 break :path_to_get path;
2467 // in the wild. The problem is that wToPrefixedFileW was2465 }
2468 // never intended to make *any* OS syscall APIs. It's only2466 // At this point, we know we have a relative path that had too many
2469 // supposed to convert a string to one that is eligible to2467 // `..` components to be resolved by normalizePath, so we need to
2470 // be used in the ntdll syscalls.2468 // convert it into an absolute path and let RtlGetFullPathName_U
2471 //2469 // canonicalize it. We do this by getting the path of the `dir`
2472 // To solve this, this function needs to no longer call2470 // and appending the relative path to it.
2473 // GetFinalPathNameByHandle under any conditions, or the2471 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
2474 // calling function needs to get reworked to not need to2472 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
2475 // call this function.2473 // This mapping is not correct; it is actually expected
2476 //2474 // that calling GetFinalPathNameByHandle might return
2477 // This may involve making breaking API changes.2475 // error.UnrecognizedVolume, and in fact has been observed
2478 error.UnrecognizedVolume => return error.Unexpected,2476 // in the wild. The problem is that wToPrefixedFileW was
2479 else => |e| return e,2477 // never intended to make *any* OS syscall APIs. It's only
2480 };2478 // supposed to convert a string to one that is eligible to
2481 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {2479 // be used in the ntdll syscalls.
2482 return error.NameTooLong;2480 //
2483 }2481 // To solve this, this function needs to no longer call
2484 // We don't have to worry about potentially doubling up path separators2482 // GetFinalPathNameByHandle under any conditions, or the
2485 // here since RtlGetFullPathName_U will handle canonicalizing it.2483 // calling function needs to get reworked to not need to
2486 dir_path_buf[dir_path.len] = '\\';2484 // call this function.
2487 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);2485 //
2488 const full_len = dir_path.len + 1 + path.len;2486 // This may involve making breaking API changes.
2489 dir_path_buf[full_len] = 0;2487 error.UnrecognizedVolume => return error.Unexpected,
2490 break :path_to_get dir_path_buf[0..full_len :0];2488 else => |e| return e,
2491 };2489 };
2492 const path_byte_len = ntdll.RtlGetFullPathName_U(2490 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
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) {
2502 return error.NameTooLong;2491 return error.NameTooLong;
2503 }2492 }
2504 path_space.len = path_buf_offset + (path_byte_len / 2);2493 // We don't have to worry about potentially doubling up path separators
2505 if (path_type == .unc_absolute) {2494 // here since RtlGetFullPathName_U will handle canonicalizing it.
2506 // Now add in the UNC, the `C` should overwrite the first `\` of the2495 dir_path_buf[dir_path.len] = '\\';
2507 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`2496 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
2508 std.debug.assert(path_space.data[path_buf_offset] == '\\');2497 const full_len = dir_path.len + 1 + path.len;
2509 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');2498 dir_path_buf[full_len] = 0;
2510 const unc = [_]u16{ 'U', 'N', 'C' };2499 break :path_to_get dir_path_buf[0..full_len :0];
2511 path_space.data[nt_prefix.len..][0..unc.len].* = unc;2500 };
2512 }2501 const path_byte_len = ntdll.RtlGetFullPathName_U(
2513 return path_space;2502 path_to_get.ptr,
2514 },2503 buf_len * 2,
2515 }2504 path_space.data[path_buf_offset..].ptr,
2516}2505 null,
25172506 );
2518pub const NamespacePrefix = enum {2507 if (path_byte_len == 0) {
2519 none,2508 // TODO: This may not be the right error
2520 /// `\\.\` (path separators can be `\` or `/`)2509 return error.BadPathName;
2521 local_device,2510 } else if (path_byte_len / 2 > buf_len) {
2522 /// `\\?\`2511 return error.NameTooLong;
2523 /// When converted to an NT path, everything past the prefix is left2512 }
2524 /// untouched and `\\?\` is replaced by `\??\`.2513 path_space.len = path_buf_offset + (path_byte_len / 2);
2525 verbatim,2514 if (path_type == .unc_absolute) {
2526 /// `\\?\` without all path separators being `\`.2515 // Now add in the UNC, the `C` should overwrite the first `\` of the
2527 /// This seems to be recognized as a prefix, but the 'verbatim' aspect2516 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
2528 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,2517 std.debug.assert(path_space.data[path_buf_offset] == '\\');
2529 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't2518 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');
2530 /// be treated as part of the final path])2519 const unc = [_]u16{ 'U', 'N', 'C' };
2531 fake_verbatim,2520 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
2532 /// `\??\`2521 }
2533 nt,2522 return path_space;
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,
2554 }2523 }
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, "//?/"));
2577}2524}
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`
2580 unc_absolute,2529 unc_absolute,
2530 /// `C:\foo`
2581 drive_absolute,2531 drive_absolute,
2532 /// `C:foo`
2582 drive_relative,2533 drive_relative,
2534 /// `\foo`
2583 rooted,2535 rooted,
2536 /// `foo`
2584 relative,2537 relative,
2538 /// `\\.\foo`, `\\?\foo`
2539 local_device,
2540 /// `\\.`, `\\?`
2585 root_local_device,2541 root_local_device,
2586};2542};
25872543
2588/// Get the path type of a path that is known to not have any namespace prefixes2544/// Get the path type of a Win32 namespace path.
2589/// (`\\?\`, `\\.\`, `\??\`).2545/// Similar to `RtlDetermineDosPathNameType_U`.
2590/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.2546/// 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 {
2592 if (path.len < 1) return .relative;2548 if (path.len < 1) return .relative;
25932549
2594 if (std.debug.runtime_safety) {
2595 std.debug.assert(getNamespacePrefix(T, path) == .none);
2596 }
2597
2598 const windows_path = std.fs.path.PathType.windows;2550 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])) {
2600 // \x2552 // \x
2601 if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted;2553 if (path.len < 2 or !windows_path.isSep(T, path[1])) return .rooted;
2602 // exactly \\. or \\? with nothing trailing2554 // \\. or \\?
2603 if (path.len == 3 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) return .root_local_device;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 }
2604 // \\x2561 // \\x
2605 return .unc_absolute;2562 return .unc_absolute;
2606 } else {2563 } 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 };
2607 // x2597 // 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;
2609 // x:\2599 // 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;
2611 // x:2601 // x:
2612 return .drive_relative;2602 return .drive_relative;
2613 }2603 }
2614}2604}
26152605
2616test getUnprefixedPathType {2606test getWin32PathType {
2617 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, ""));2607 try std.testing.expectEqual(.relative, getWin32PathType(u8, ""));
2618 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x"));2608 try std.testing.expectEqual(.relative, getWin32PathType(u8, "x"));
2619 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x\\"));2609 try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\"));
2620 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "//."));2610
2621 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "/\\?"));2611 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//."));
2622 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "\\\\?"));2612 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?"));
2623 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "\\\\x"));2613 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?"));
2624 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "//x"));2614
2625 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "\\x"));2615 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x"));
2626 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "/"));2616 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x"));
2627 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:"));2617 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x"));
2628 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:abc"));2618 // local device paths require a path separator after the root, otherwise it is considered a UNC path
2629 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:a/b/c"));2619 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x"));
2630 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\"));2620 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x"));
2631 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\abc"));2621
2632 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));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 };
2633}2701}
26342702
2635/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.2703/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
...@@ -2646,30 +2714,25 @@ test getUnprefixedPathType {...@@ -2646,30 +2714,25 @@ test getUnprefixedPathType {
2646/// Supports in-place modification (`path` and `out` may refer to the same slice).2714/// Supports in-place modification (`path` and `out` may refer to the same slice).
2647pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {2715pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {
2648 if (path.len > PATH_MAX_WIDE) return error.NameTooLong;2716 if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
26492717 if (!hasCommonNtPrefix(u16, path)) return error.NotNtPath;
2650 const namespace_prefix = getNamespacePrefix(u16, path);2718
2651 switch (namespace_prefix) {2719 var dest_index: usize = 0;
2652 .nt => {2720 var after_prefix = path[4..]; // after the `\??\`
2653 var dest_index: usize = 0;2721 // The prefix \??\UNC\ means this is a UNC path, in which case the
2654 var after_prefix = path[4..]; // after the `\??\`2722 // `\??\UNC\` should be replaced by `\\` (two backslashes)
2655 // The prefix \??\UNC\ means this is a UNC path, in which case the2723 const is_unc = after_prefix.len >= 4 and
2656 // `\??\UNC\` should be replaced by `\\` (two backslashes)2724 eqlIgnoreCaseWtf16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
2657 const is_unc = after_prefix.len >= 4 and2725 std.fs.path.PathType.windows.isSep(u16, after_prefix[3]);
2658 eqlIgnoreCaseWTF16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and2726 const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
2659 std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));2727 if (out.len < win32_len) return error.NameTooLong;
2660 const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);2728 if (is_unc) {
2661 if (out.len < win32_len) return error.NameTooLong;2729 out[0] = comptime std.mem.nativeToLittle(u16, '\\');
2662 if (is_unc) {2730 dest_index += 1;
2663 out[0] = comptime std.mem.nativeToLittle(u16, '\\');2731 // We want to include the last `\` of `\??\UNC\`
2664 dest_index += 1;2732 after_prefix = path[7..];
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,
2672 }2733 }
2734 @memmove(out[dest_index..][0..after_prefix.len], after_prefix);
2735 return out[0..win32_len];
2673}2736}
26742737
2675test ntToWin32Namespace {2738test ntToWin32Namespace {
lib/std/os/windows/test.zig+102-2
...@@ -54,8 +54,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {...@@ -54,8 +54,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
54}54}
5555
56test "toPrefixedFileW" {56test "toPrefixedFileW" {
57 if (builtin.os.tag != .windows)57 if (builtin.os.tag != .windows) return error.SkipZigTest;
58 return;
5958
60 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html59 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
61 // Note that these tests do not actually touch the filesystem or care about whether or not60 // Note that these tests do not actually touch the filesystem or care about whether or not
...@@ -237,3 +236,104 @@ test "removeDotDirs" {...@@ -237,3 +236,104 @@ test "removeDotDirs" {
237 try testRemoveDotDirs("a\\b\\..\\", "a\\");236 try testRemoveDotDirs("a\\b\\..\\", "a\\");
238 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");237 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
239}238}
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;...@@ -22,16 +22,17 @@ pub const GetCwdError = posix.GetCwdError;
22/// The result is a slice of `out_buffer`, from index `0`.22/// The result is a slice of `out_buffer`, from index `0`.
23/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).23/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
24/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.24/// 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 {
26 return posix.getcwd(out_buffer);26 return posix.getcwd(out_buffer);
27}27}
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
31/// Caller must free the returned memory.32/// Caller must free the returned memory.
32/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).33/// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
33/// On other platforms, the result is an opaque sequence of bytes with no particular encoding.34/// 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 {
35 // The use of max_path_bytes here is just a heuristic: most paths will fit36 // The use of max_path_bytes here is just a heuristic: most paths will fit
36 // in stack_buf, avoiding an extra allocation in the common case.37 // in stack_buf, avoiding an extra allocation in the common case.
37 var stack_buf: [fs.max_path_bytes]u8 = undefined;38 var stack_buf: [fs.max_path_bytes]u8 = undefined;
...@@ -529,6 +530,7 @@ pub fn hasNonEmptyEnvVar(allocator: Allocator, key: []const u8) HasEnvVarError!b...@@ -529,6 +530,7 @@ pub fn hasNonEmptyEnvVar(allocator: Allocator, key: []const u8) HasEnvVarError!b
529}530}
530531
531/// Windows-only. Get an environment variable with a null-terminated, WTF-16 encoded name.532/// Windows-only. Get an environment variable with a null-terminated, WTF-16 encoded name.
533/// The returned slice points to memory in the PEB.
532///534///
533/// This function performs a Unicode-aware case-insensitive lookup using RtlEqualUnicodeString.535/// This function performs a Unicode-aware case-insensitive lookup using RtlEqualUnicodeString.
534///536///
...@@ -564,7 +566,7 @@ pub fn getenvW(key: [*:0]const u16) ?[:0]const u16 {...@@ -564,7 +566,7 @@ pub fn getenvW(key: [*:0]const u16) ?[:0]const u16 {
564 };566 };
565567
566 const this_key = key_value[0..equal_index];568 const this_key = key_value[0..equal_index];
567 if (windows.eqlIgnoreCaseWTF16(key_slice, this_key)) {569 if (windows.eqlIgnoreCaseWtf16(key_slice, this_key)) {
568 return key_value[equal_index + 1 ..];570 return key_value[equal_index + 1 ..];
569 }571 }
570572
lib/std/process/Child.zig+2-2
...@@ -1227,7 +1227,7 @@ fn windowsCreateProcessPathExt(...@@ -1227,7 +1227,7 @@ fn windowsCreateProcessPathExt(
1227 const app_name = app_buf.items[0..app_name_len];1227 const app_name = app_buf.items[0..app_name_len];
1228 const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;1228 const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err;
1229 const ext = app_name[ext_start..];1229 const ext = app_name[ext_start..];
1230 if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {1230 if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
1231 return error.UnrecoverableInvalidExe;1231 return error.UnrecoverableInvalidExe;
1232 }1232 }
1233 break :unappended err;1233 break :unappended err;
...@@ -1278,7 +1278,7 @@ fn windowsCreateProcessPathExt(...@@ -1278,7 +1278,7 @@ fn windowsCreateProcessPathExt(
1278 // On InvalidExe, if the extension of the app name is .exe then1278 // On InvalidExe, if the extension of the app name is .exe then
1279 // it's treated as an unrecoverable error. Otherwise, it'll be1279 // it's treated as an unrecoverable error. Otherwise, it'll be
1280 // skipped as normal.1280 // skipped as normal.
1281 if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {1281 if (windows.eqlIgnoreCaseWtf16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) {
1282 return error.UnrecoverableInvalidExe;1282 return error.UnrecoverableInvalidExe;
1283 }1283 }
1284 continue;1284 continue;
lib/std/zig/WindowsSdk.zig+1-1
...@@ -643,7 +643,7 @@ const MsvcLibDir = struct {...@@ -643,7 +643,7 @@ const MsvcLibDir = struct {
643643
644 if (!std.fs.path.isAbsolute(dll_path)) return error.PathNotFound;644 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);
647 // the .dll filename647 // the .dll filename
648 _ = path_it.last();648 _ = path_it.last();
649 const root_path = while (path_it.previous()) |dir_component| {649 const root_path = while (path_it.previous()) |dir_component| {
src/main.zig+1-1
...@@ -3883,7 +3883,7 @@ fn createModule(...@@ -3883,7 +3883,7 @@ fn createModule(
3883 if (create_module.sysroot) |root| {3883 if (create_module.sysroot) |root| {
3884 for (create_module.lib_dir_args.items) |lib_dir_arg| {3884 for (create_module.lib_dir_args.items) |lib_dir_arg| {
3885 if (fs.path.isAbsolute(lib_dir_arg)) {3885 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..];
3887 const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir });3887 const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir });
3888 addLibDirectoryWarn(&create_module.lib_directories, full_path);3888 addLibDirectoryWarn(&create_module.lib_directories, full_path);
3889 } else {3889 } else {
test/standalone/build.zig.zon+3
...@@ -126,6 +126,9 @@...@@ -126,6 +126,9 @@
126 .windows_bat_args = .{126 .windows_bat_args = .{
127 .path = "windows_bat_args",127 .path = "windows_bat_args",
128 },128 },
129 .windows_paths = .{
130 .path = "windows_paths",
131 },
129 .self_exe_symlink = .{132 .self_exe_symlink = .{
130 .path = "self_exe_symlink",133 .path = "self_exe_symlink",
131 },134 },
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}