authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-10-27 21:49:18-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-11-21 00:03:44-08:00
log59b8bed222137061d74002a40544b5ea30eab666
treeca7d9a54e931084354f79027393247265e7808d8
parent26afcdb7fe95b35a3246980d249bcf0dc17dbf8b

Teach fs.path about the wonderful world of Windows paths

Previously, fs.path handled a few of the Windows path types, but not all of them, and only a few of them correctly/consistently. This commit aims to make `std.fs.path` correct and consistent in handling all possible Win32 path types. This commit also slightly nudges the codebase towards a separation of Win32 paths and NT paths, as NT paths are not actually distinguishable from Win32 paths from looking at their contents alone (i.e. `\Device\Foo` could be an NT path or a Win32 rooted path, no way to tell without external context). This commit formalizes `std.fs.path` being fully concerned with Win32 paths, and having no special detection/handling of NT paths. Resources on Windows path types, and Win32 vs NT paths: - https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html - https://chrisdenton.github.io/omnipath/Overview.html - https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file API additions/changes/deprecations - `std.os.windows.getWin32PathType` was added (it is analogous to `RtlDetermineDosPathNameType_U`), while `std.os.windows.getNamespacePrefix` and `std.os.windows.getUnprefixedPathType` were deleted. `getWin32PathType` forms the basis on which the updated `std.fs.path` functions operate. - `std.fs.path.parsePath`, `std.fs.path.parsePathPosix`, and `std.fs.path.parsePathWindows` were added, while `std.fs.path.windowsParsePath` was deprecated. The new `parsePath` functions provide the "root" and the "kind" of a path, which is platform-specific. The now-deprecated `windowsParsePath` did not handle all possible path types, while the new `parsePathWindows` does. - `std.fs.path.diskDesignator` has been deprecated in favor of `std.fs.path.parsePath`, and same deal with `diskDesignatorWindows` -> `parsePathWindows` - `relativeWindows` is now a compile error when *not* targeting Windows, while `relativePosix` is now a compile error when targeting Windows. This is because those functions read/use the CWD path which will behave improperly when used from a system with different path semantics (e.g. calling `relativePosix` from a Windows system with a CWD like `C:\foo\bar` will give you a bogus result since that'd be treated as a single relative component when using POSIX semantics). This also allows `relativeWindows` to use Windows-specific APIs for getting the CWD and environment variables to cut down on allocations. - `componentIterator`/`ComponentIterator.init` have been made infallible. These functions used to be able to error on UNC paths with an empty server component, and on paths that were assumed to be NT paths, but now: + We follow the lead of `RtlDetermineDosPathNameType_U`/`RtlGetFullPathName_U` in how it treats a UNC path with an empty server name (e.g. `\\\share`) and allow it, even if it'll be invalid at the time of usage + Now that `std.fs.path` assumes paths are Win32 paths and not NT paths, we don't have to worry about NT paths Behavior changes - `std.fs.path` generally: any combinations of mixed path separators for UNC paths are universally supported, e.g. `\/server/share`, `/\server\share`, `/\server/\\//share` are all seen as equivalent UNC paths - `resolveWindows` handles all path types more appropriately/consistently. + `//` and `//foo` used to be treated as a relative path, but are now seen as UNC paths + If a rooted/drive-relative path cannot be resolved against anything more definite, the result will remain a rooted/drive-relative path. + I've created [a script to generate the results of a huge number of permutations of different path types](https://gist.github.com/squeek502/9eba7f19cad0d0d970ccafbc30f463bf) (the result of running the script is also included for anyone that'd like to vet the behavior). - `dirnameWindows` now treats the drive-relative root as the dirname of a drive-relative path with a component, e.g. `dirname("C:foo")` is now `C:`, whereas before it would return null. `dirnameWindows` also handles local device paths appropriately now. - `basenameWindows` now handles all path types more appropriately. The most notable change here is `//a` being treated as a partial UNC path now and therefore `basename` will return `""` for it, whereas before it would return `"a"` - `relativeWindows` will now do its best to resolve against the most appropriate CWD for each path, e.g. relative for `D:foo` will look at the CWD to check if the drive letter matches, and if not, look at the special environment variable `=D:` to get the shell-defined CWD for that drive, and if that doesn't exist, then it'll resolve against `D:\`. Implementation details - `resolveWindows` previously looped through the paths twice to build up the relevant info before doing the actual resolution. Now, `resolveWindows` iterates backwards once and keeps track of which paths are actually relevant using a bit set, which also allows it to break from the loop when it's no longer possible for earlier paths to matter. - A standalone test was added to test parts of `relativeWindows` since the CWD resolution logic depends on CWD information from the PEB and environment variables Edge cases worth noting - A strange piece of trivia that I found out while working on this is that it's technically possible to have a drive letter that it outside the intended A-Z range, or even outside the ASCII range entirely. Since we deal with both WTF-8 and WTF-16 paths, `path[0]`/`path[1]`/`path[2]` will not always refer to the same bits of information, so to get consistent behavior, some decision about how to deal with this edge case had to be made. I've made the choice to conform with how `RtlDetermineDosPathNameType_U` works, i.e. treat the first WTF-16 code unit as the drive letter. This means that when working with WTF-8, checking for drive-relative/drive-absolute paths is a bit more complicated. For more details, see the lengthy comment in `std.os.windows.getWin32PathType` - `relativeWindows` will now almost always be able to return either a fully-qualified absolute path or a relative path, but there's one scenario where it may return a rooted path: when the CWD gotten from the PEB is not a drive-absolute or UNC path (if that's actually feasible/possible?). An alternative approach to this scenario might be to resolve against the `HOMEDRIVE` env var if available, and/or default to `C:\` as a last resort in order to guarantee the result of `relative` is never a rooted path. - Partial UNC paths (e.g. `\\server` instead of `\\server\share`) are a bit awkward to handle, generally. Not entirely sure how best to handle them, so there may need to be another pass in the future to iron out any issues that arise. As of now the behavior is: + For `relative`, any part of a UNC disk designator is treated as the "root" and therefore isn't applicable for relative paths, e.g. calling `relative` with `\\server` and `\\server\share` will result in `\\server\share` rather than just `share` and if `relative` is called with `\\server\foo` and `\\server\bar` the result will be `\\server\bar` rather than `..\bar` + For `resolve`, any part of a UNC disk designator is also treated as the "root", but relative and rooted paths are still elligable for filling in missing portions of the disk designator, e.g. `resolve` with `\\server` and `foo` or `\foo` will result in `\\server\foo` Fixes #25703 Closes #25702

15 files changed, 1680 insertions(+), 756 deletions(-)

lib/compiler/resinator/compile.zig+1-1
......@@ -2914,7 +2914,7 @@ fn validateSearchPath(path: []const u8) error{BadPathName}!void {
29142914 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
29152915 // Those path types are something of an unavoidable way to
29162916 // still hit unreachable during the openDir call.
2917 var component_iterator = try std.fs.path.componentIterator(path);
2917 var component_iterator = std.fs.path.componentIterator(path);
29182918 while (component_iterator.next()) |component| {
29192919 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
29202920 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;
lib/std/Build/Cache.zig+1-3
......@@ -104,9 +104,7 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath {
104104fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {
105105 const relative = try fs.path.relative(allocator, prefix, path);
106106 errdefer allocator.free(relative);
107 var component_iterator = fs.path.NativeComponentIterator.init(relative) catch {
108 return error.NotASubPath;
109 };
107 var component_iterator = fs.path.NativeComponentIterator.init(relative);
110108 if (component_iterator.root() != null) {
111109 return error.NotASubPath;
112110 }
lib/std/Build/Watch/FsEvents.zig+1-1
......@@ -167,7 +167,7 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)
167167 }.lessThan);
168168 need_dirs.clearRetainingCapacity();
169169 for (old_dirs) |dir_path| {
170 var it: std.fs.path.ComponentIterator(.posix, u8) = try .init(dir_path);
170 var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path);
171171 while (it.next()) |component| {
172172 if (need_dirs.contains(component.path)) {
173173 // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added
lib/std/Io/Dir.zig+1-1
......@@ -318,7 +318,7 @@ pub const MakePathStatus = enum { existed, created };
318318/// Same as `makePath` except returns whether the path already existed or was
319319/// successfully created.
320320pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus {
321 var it = try std.fs.path.componentIterator(sub_path);
321 var it = std.fs.path.componentIterator(sub_path);
322322 var status: MakePathStatus = .existed;
323323 var component = it.last() orelse return error.BadPathName;
324324 while (true) {
lib/std/Io/Threaded.zig+1-1
......@@ -1154,7 +1154,7 @@ fn dirMakeOpenPathWindows(
11541154 w.SYNCHRONIZE | w.FILE_TRAVERSE |
11551155 (if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0));
11561156
1157 var it = try std.fs.path.componentIterator(sub_path);
1157 var it = std.fs.path.componentIterator(sub_path);
11581158 // If there are no components in the path, then create a dummy component with the full path.
11591159 var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
11601160 .name = "",
lib/std/fs/path.zig+1062-488
......@@ -20,10 +20,7 @@ const testing = std.testing;
2020const mem = std.mem;
2121const ascii = std.ascii;
2222const Allocator = mem.Allocator;
23const math = std.math;
2423const windows = std.os.windows;
25const os = std.os;
26const fs = std.fs;
2724const process = std.process;
2825const native_os = builtin.target.os.tag;
2926
......@@ -221,7 +218,7 @@ test join {
221218 try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);
222219 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
223220 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);
221 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b\\", "\\c" }, "c:\\a\\b\\c", zero);
225222
226223 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero);
227224 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero);
......@@ -283,26 +280,16 @@ pub fn isAbsolute(path: []const u8) bool {
283280}
284281
285282fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {
286 if (path.len < 1)
287 return false;
288
289 if (path[0] == '/')
290 return true;
291
292 if (path[0] == '\\')
293 return true;
294
295 if (path.len < 3)
296 return false;
297
298 if (path[1] == ':') {
299 if (path[2] == '/')
300 return true;
301 if (path[2] == '\\')
302 return true;
303 }
304
305 return false;
283 return switch (windows.getWin32PathType(T, path)) {
284 // Unambiguously absolute
285 .drive_absolute, .unc_absolute, .local_device, .root_local_device => true,
286 // Unambiguously relative
287 .relative => false,
288 // Ambiguous, more absolute than relative
289 .rooted => true,
290 // Ambiguous, more relative than absolute
291 .drive_relative => false,
292 };
306293}
307294
308295pub fn isAbsoluteWindows(path: []const u8) bool {
......@@ -347,6 +334,9 @@ test isAbsoluteWindows {
347334 try testIsAbsoluteWindows("C:\\Users\\", true);
348335 try testIsAbsoluteWindows("C:cwd/another", false);
349336 try testIsAbsoluteWindows("C:cwd\\another", false);
337 try testIsAbsoluteWindows("λ:\\", true);
338 try testIsAbsoluteWindows("λ:", false);
339 try testIsAbsoluteWindows("\u{10000}:\\", false);
350340 try testIsAbsoluteWindows("directory/directory", false);
351341 try testIsAbsoluteWindows("directory\\directory", false);
352342 try testIsAbsoluteWindows("/usr/local", true);
......@@ -362,12 +352,17 @@ test isAbsolutePosix {
362352
363353fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {
364354 try testing.expectEqual(expected_result, isAbsoluteWindows(path));
355 const path_w = try std.unicode.wtf8ToWtf16LeAllocZ(std.testing.allocator, path);
356 defer std.testing.allocator.free(path_w);
357 try testing.expectEqual(expected_result, isAbsoluteWindowsW(path_w));
358 try testing.expectEqual(expected_result, isAbsoluteWindowsWtf16(path_w));
365359}
366360
367361fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {
368362 try testing.expectEqual(expected_result, isAbsolutePosix(path));
369363}
370364
365/// Deprecated; see `WindowsPath2`
371366pub const WindowsPath = struct {
372367 is_abs: bool,
373368 kind: Kind,
......@@ -380,6 +375,7 @@ pub const WindowsPath = struct {
380375 };
381376};
382377
378/// Deprecated; see `parsePathWindows`
383379pub fn windowsParsePath(path: []const u8) WindowsPath {
384380 if (path.len >= 2 and path[1] == ':') {
385381 return WindowsPath{
......@@ -402,26 +398,18 @@ pub fn windowsParsePath(path: []const u8) WindowsPath {
402398 .disk_designator = &[_]u8{},
403399 .is_abs = false,
404400 };
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 }
415401
416 var it = mem.tokenizeAny(u8, path, "/\\");
417 _ = (it.next() orelse return relative_path);
418 _ = (it.next() orelse return relative_path);
419 return WindowsPath{
420 .is_abs = isAbsoluteWindows(path),
421 .kind = WindowsPath.Kind.NetworkShare,
422 .disk_designator = path[0..it.index],
423 };
424 }
402 if (path.len >= 2 and PathType.windows.isSep(u8, path[0]) and PathType.windows.isSep(u8, path[1])) {
403 const root_end = root_end: {
404 var server_end = mem.indexOfAnyPos(u8, path, 2, "/\\") orelse break :root_end path.len;
405 while (server_end < path.len and PathType.windows.isSep(u8, path[server_end])) server_end += 1;
406 break :root_end mem.indexOfAnyPos(u8, path, server_end, "/\\") orelse path.len;
407 };
408 return WindowsPath{
409 .is_abs = true,
410 .kind = WindowsPath.Kind.NetworkShare,
411 .disk_designator = path[0..root_end],
412 };
425413 }
426414 return relative_path;
427415}
......@@ -446,10 +434,22 @@ test windowsParsePath {
446434 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));
447435 }
448436 {
449 const parsed = windowsParsePath("\\\\a\\");
450 try testing.expect(!parsed.is_abs);
451 try testing.expect(parsed.kind == WindowsPath.Kind.None);
452 try testing.expect(mem.eql(u8, parsed.disk_designator, ""));
437 const parsed = windowsParsePath("\\/a\\");
438 try testing.expect(parsed.is_abs);
439 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
440 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\/a\\"));
441 }
442 {
443 const parsed = windowsParsePath("\\\\a\\\\b");
444 try testing.expect(parsed.is_abs);
445 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
446 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
447 }
448 {
449 const parsed = windowsParsePath("\\\\a\\\\b\\c");
450 try testing.expect(parsed.is_abs);
451 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
452 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a\\\\b"));
453453 }
454454 {
455455 const parsed = windowsParsePath("/usr/local");
......@@ -465,6 +465,229 @@ test windowsParsePath {
465465 }
466466}
467467
468/// On Windows, this calls `parsePathWindows` and on POSIX it calls `parsePathPosix`.
469///
470/// Returns a platform-specific struct with two fields: `root` and `kind`.
471/// The `root` will be a slice of `path` (`/` for POSIX absolute paths, and things
472/// like `C:\`, `\\server\share\`, etc for Windows paths).
473/// If the path is of kind `.relative`, then `root` will be zero-length.
474pub fn parsePath(path: []const u8) switch (native_os) {
475 .windows => WindowsPath2(u8),
476 else => PosixPath,
477} {
478 switch (native_os) {
479 .windows => return parsePathWindows(u8, path),
480 else => return parsePathPosix(path),
481 }
482}
483
484const PosixPath = struct {
485 kind: enum { relative, absolute },
486 root: []const u8,
487};
488
489pub fn parsePathPosix(path: []const u8) PosixPath {
490 const abs = isAbsolutePosix(path);
491 return .{
492 .kind = if (abs) .absolute else .relative,
493 .root = if (abs) path[0..1] else path[0..0],
494 };
495}
496
497test parsePathPosix {
498 {
499 const parsed = parsePathPosix("a/b");
500 try testing.expectEqual(.relative, parsed.kind);
501 try testing.expectEqualStrings("", parsed.root);
502 }
503 {
504 const parsed = parsePathPosix("/a/b");
505 try testing.expectEqual(.absolute, parsed.kind);
506 try testing.expectEqualStrings("/", parsed.root);
507 }
508 {
509 const parsed = parsePathPosix("///a/b");
510 try testing.expectEqual(.absolute, parsed.kind);
511 try testing.expectEqualStrings("/", parsed.root);
512 }
513}
514
515pub fn WindowsPath2(comptime T: type) type {
516 return struct {
517 kind: windows.Win32PathType,
518 root: []const T,
519 };
520}
521
522pub fn parsePathWindows(comptime T: type, path: []const T) WindowsPath2(T) {
523 const kind = windows.getWin32PathType(T, path);
524 const root = root: switch (kind) {
525 .drive_absolute, .drive_relative => {
526 const drive_letter_len = getDriveLetter(T, path).len;
527 break :root path[0 .. drive_letter_len + @as(usize, if (kind == .drive_absolute) 2 else 1)];
528 },
529 .relative => path[0..0],
530 .local_device => path[0..4],
531 .root_local_device => path,
532 .rooted => path[0..1],
533 .unc_absolute => {
534 const unc = parseUNC(T, path);
535 // There may be any number of path separators between the server and the share,
536 // so take that into account by using pointer math to get the difference.
537 var root_len = 2 + (unc.share.ptr - unc.server.ptr) + unc.share.len;
538 if (unc.sep_after_share) root_len += 1;
539 break :root path[0..root_len];
540 },
541 };
542 return .{
543 .kind = kind,
544 .root = root,
545 };
546}
547
548test parsePathWindows {
549 {
550 const path = "//a/b";
551 const parsed = parsePathWindows(u8, path);
552 try testing.expectEqual(.unc_absolute, parsed.kind);
553 try testing.expectEqualStrings("//a/b", parsed.root);
554 try testWindowsParsePathHarmony(path);
555 }
556 {
557 const path = "\\\\a\\b";
558 const parsed = parsePathWindows(u8, path);
559 try testing.expectEqual(.unc_absolute, parsed.kind);
560 try testing.expectEqualStrings("\\\\a\\b", parsed.root);
561 try testWindowsParsePathHarmony(path);
562 }
563 {
564 const path = "\\/a/b/c";
565 const parsed = parsePathWindows(u8, path);
566 try testing.expectEqual(.unc_absolute, parsed.kind);
567 try testing.expectEqualStrings("\\/a/b/", parsed.root);
568 try testWindowsParsePathHarmony(path);
569 }
570 {
571 const path = "\\\\a\\";
572 const parsed = parsePathWindows(u8, path);
573 try testing.expectEqual(.unc_absolute, parsed.kind);
574 try testing.expectEqualStrings("\\\\a\\", parsed.root);
575 try testWindowsParsePathHarmony(path);
576 }
577 {
578 const path = "\\\\a\\b\\";
579 const parsed = parsePathWindows(u8, path);
580 try testing.expectEqual(.unc_absolute, parsed.kind);
581 try testing.expectEqualStrings("\\\\a\\b\\", parsed.root);
582 try testWindowsParsePathHarmony(path);
583 }
584 {
585 const path = "\\\\a\\/b\\/";
586 const parsed = parsePathWindows(u8, path);
587 try testing.expectEqual(.unc_absolute, parsed.kind);
588 try testing.expectEqualStrings("\\\\a\\/b\\", parsed.root);
589 try testWindowsParsePathHarmony(path);
590 }
591 {
592 const path = "\\\\кириллица\\ελληνικά\\português";
593 const parsed = parsePathWindows(u8, path);
594 try testing.expectEqual(.unc_absolute, parsed.kind);
595 try testing.expectEqualStrings("\\\\кириллица\\ελληνικά\\", parsed.root);
596 try testWindowsParsePathHarmony(path);
597 }
598 {
599 const path = "/usr/local";
600 const parsed = parsePathWindows(u8, path);
601 try testing.expectEqual(.rooted, parsed.kind);
602 try testing.expectEqualStrings("/", parsed.root);
603 try testWindowsParsePathHarmony(path);
604 }
605 {
606 const path = "\\\\.";
607 const parsed = parsePathWindows(u8, path);
608 try testing.expectEqual(.root_local_device, parsed.kind);
609 try testing.expectEqualStrings("\\\\.", parsed.root);
610 try testWindowsParsePathHarmony(path);
611 }
612 {
613 const path = "\\\\.\\a";
614 const parsed = parsePathWindows(u8, path);
615 try testing.expectEqual(.local_device, parsed.kind);
616 try testing.expectEqualStrings("\\\\.\\", parsed.root);
617 try testWindowsParsePathHarmony(path);
618 }
619 {
620 const path = "c:../";
621 const parsed = parsePathWindows(u8, path);
622 try testing.expectEqual(.drive_relative, parsed.kind);
623 try testing.expectEqualStrings("c:", parsed.root);
624 try testWindowsParsePathHarmony(path);
625 }
626 {
627 const path = "C:\\../";
628 const parsed = parsePathWindows(u8, path);
629 try testing.expectEqual(.drive_absolute, parsed.kind);
630 try testing.expectEqualStrings("C:\\", parsed.root);
631 try testWindowsParsePathHarmony(path);
632 }
633 {
634 // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
635 const path = "€:\\";
636 const parsed = parsePathWindows(u8, path);
637 try testing.expectEqual(.drive_absolute, parsed.kind);
638 try testing.expectEqualStrings("€:\\", parsed.root);
639 try testWindowsParsePathHarmony(path);
640 }
641 {
642 const path = "€:";
643 const parsed = parsePathWindows(u8, path);
644 try testing.expectEqual(.drive_relative, parsed.kind);
645 try testing.expectEqualStrings("€:", parsed.root);
646 try testWindowsParsePathHarmony(path);
647 }
648 {
649 // But code points that are encoded as two WTF-16 code units are not
650 const path = "\u{10000}:\\";
651 const parsed = parsePathWindows(u8, path);
652 try testing.expectEqual(.relative, parsed.kind);
653 try testing.expectEqualStrings("", parsed.root);
654 try testWindowsParsePathHarmony(path);
655 }
656 {
657 const path = "\u{10000}:";
658 const parsed = parsePathWindows(u8, path);
659 try testing.expectEqual(.relative, parsed.kind);
660 try testing.expectEqualStrings("", parsed.root);
661 try testWindowsParsePathHarmony(path);
662 }
663 {
664 // Paths are assumed to be in the Win32 namespace, so while this is
665 // likely a NT namespace path, it's treated as a rooted path.
666 const path = "\\??\\foo";
667 const parsed = parsePathWindows(u8, path);
668 try testing.expectEqual(.rooted, parsed.kind);
669 try testing.expectEqualStrings("\\", parsed.root);
670 try testWindowsParsePathHarmony(path);
671 }
672}
673
674fn testWindowsParsePathHarmony(wtf8: []const u8) !void {
675 var wtf16_buf: [256]u16 = undefined;
676 const wtf16_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf, wtf8);
677 const wtf16 = wtf16_buf[0..wtf16_len];
678
679 const wtf8_parsed = parsePathWindows(u8, wtf8);
680 const wtf16_parsed = parsePathWindows(u16, wtf16);
681
682 var wtf8_buf: [256]u8 = undefined;
683 const wtf16_root_as_wtf8_len = std.unicode.wtf16LeToWtf8(&wtf8_buf, wtf16_parsed.root);
684 const wtf16_root_as_wtf8 = wtf8_buf[0..wtf16_root_as_wtf8_len];
685
686 try std.testing.expectEqual(wtf8_parsed.kind, wtf16_parsed.kind);
687 try std.testing.expectEqualStrings(wtf8_parsed.root, wtf16_root_as_wtf8);
688}
689
690/// Deprecated; use `parsePath`
468691pub fn diskDesignator(path: []const u8) []const u8 {
469692 if (native_os == .windows) {
470693 return diskDesignatorWindows(path);
......@@ -473,41 +696,172 @@ pub fn diskDesignator(path: []const u8) []const u8 {
473696 }
474697}
475698
699/// Deprecated; use `parsePathWindows`
476700pub fn diskDesignatorWindows(path: []const u8) []const u8 {
477701 return windowsParsePath(path).disk_designator;
478702}
479703
480fn networkShareServersEql(ns1: []const u8, ns2: []const u8) bool {
481 const sep1 = ns1[0];
482 const sep2 = ns2[0];
704fn WindowsUNC(comptime T: type) type {
705 return struct {
706 server: []const T,
707 sep_after_server: bool,
708 share: []const T,
709 sep_after_share: bool,
710 };
711}
483712
484 var it1 = mem.tokenizeScalar(u8, ns1, sep1);
485 var it2 = mem.tokenizeScalar(u8, ns2, sep2);
713/// Asserts that `path` starts with two path separators
714fn parseUNC(comptime T: type, path: []const T) WindowsUNC(T) {
715 assert(path.len >= 2 and PathType.windows.isSep(T, path[0]) and PathType.windows.isSep(T, path[1]));
716 const any_sep = switch (T) {
717 u8 => "/\\",
718 u16 => std.unicode.wtf8ToWtf16LeStringLiteral("/\\"),
719 else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) are supported"),
720 };
721 // For the server, the first path separator after the initial two is always
722 // the terminator of the server name, even if that means the server name is
723 // zero-length.
724 const server_end = mem.indexOfAnyPos(T, path, 2, any_sep) orelse return .{
725 .server = path[2..path.len],
726 .sep_after_server = false,
727 .share = path[path.len..path.len],
728 .sep_after_share = false,
729 };
730 // For the share, there can be any number of path separators between the server
731 // and the share, so we want to skip over all of them instead of just looking for
732 // the first one.
733 var it = std.mem.tokenizeAny(T, path[server_end + 1 ..], any_sep);
734 const share = it.next() orelse return .{
735 .server = path[2..server_end],
736 .sep_after_server = true,
737 .share = path[server_end + 1 .. server_end + 1],
738 .sep_after_share = false,
739 };
740 return .{
741 .server = path[2..server_end],
742 .sep_after_server = true,
743 .share = share,
744 .sep_after_share = it.index != it.buffer.len,
745 };
746}
486747
487 return windows.eqlIgnoreCaseWtf8(it1.next().?, it2.next().?);
748test parseUNC {
749 {
750 const unc = parseUNC(u8, "//");
751 try std.testing.expectEqualStrings("", unc.server);
752 try std.testing.expect(!unc.sep_after_server);
753 try std.testing.expectEqualStrings("", unc.share);
754 try std.testing.expect(!unc.sep_after_share);
755 }
756 {
757 const unc = parseUNC(u8, "\\\\s");
758 try std.testing.expectEqualStrings("s", unc.server);
759 try std.testing.expect(!unc.sep_after_server);
760 try std.testing.expectEqualStrings("", unc.share);
761 try std.testing.expect(!unc.sep_after_share);
762 }
763 {
764 const unc = parseUNC(u8, "\\\\s/");
765 try std.testing.expectEqualStrings("s", unc.server);
766 try std.testing.expect(unc.sep_after_server);
767 try std.testing.expectEqualStrings("", unc.share);
768 try std.testing.expect(!unc.sep_after_share);
769 }
770 {
771 const unc = parseUNC(u8, "\\/server\\share");
772 try std.testing.expectEqualStrings("server", unc.server);
773 try std.testing.expect(unc.sep_after_server);
774 try std.testing.expectEqualStrings("share", unc.share);
775 try std.testing.expect(!unc.sep_after_share);
776 }
777 {
778 const unc = parseUNC(u8, "/\\server\\share/");
779 try std.testing.expectEqualStrings("server", unc.server);
780 try std.testing.expect(unc.sep_after_server);
781 try std.testing.expectEqualStrings("share", unc.share);
782 try std.testing.expect(unc.sep_after_share);
783 }
784 {
785 const unc = parseUNC(u8, "\\\\server/\\share\\/");
786 try std.testing.expectEqualStrings("server", unc.server);
787 try std.testing.expect(unc.sep_after_server);
788 try std.testing.expectEqualStrings("share", unc.share);
789 try std.testing.expect(unc.sep_after_share);
790 }
791 {
792 const unc = parseUNC(u8, "\\\\server\\/\\\\");
793 try std.testing.expectEqualStrings("server", unc.server);
794 try std.testing.expect(unc.sep_after_server);
795 try std.testing.expectEqualStrings("", unc.share);
796 try std.testing.expect(!unc.sep_after_share);
797 }
488798}
489799
490fn compareDiskDesignators(kind: WindowsPath.Kind, p1: []const u8, p2: []const u8) bool {
800const DiskDesignatorKind = enum { drive, unc };
801
802/// `p1` and `p2` are both assumed to be the `kind` provided.
803fn compareDiskDesignators(comptime T: type, kind: DiskDesignatorKind, p1: []const T, p2: []const T) bool {
804 const eql = switch (T) {
805 u8 => windows.eqlIgnoreCaseWtf8,
806 u16 => windows.eqlIgnoreCaseWtf16,
807 else => @compileError("only u8 (WTF-8) and u16 (WTF-16LE) is supported"),
808 };
491809 switch (kind) {
492 WindowsPath.Kind.None => {
493 assert(p1.len == 0);
494 assert(p2.len == 0);
495 return true;
496 },
497 WindowsPath.Kind.Drive => {
498 return ascii.toUpper(p1[0]) == ascii.toUpper(p2[0]);
810 .drive => {
811 const drive_letter1 = getDriveLetter(T, p1);
812 const drive_letter2 = getDriveLetter(T, p2);
813
814 return eql(drive_letter1, drive_letter2);
499815 },
500 WindowsPath.Kind.NetworkShare => {
501 var it1 = mem.tokenizeAny(u8, p1, "/\\");
502 var it2 = mem.tokenizeAny(u8, p2, "/\\");
816 .unc => {
817 var unc1 = parseUNC(T, p1);
818 var unc2 = parseUNC(T, p2);
503819
504 return windows.eqlIgnoreCaseWtf8(it1.next().?, it2.next().?) and windows.eqlIgnoreCaseWtf8(it1.next().?, it2.next().?);
820 return eql(unc1.server, unc2.server) and
821 eql(unc1.share, unc2.share);
505822 },
506823 }
507824}
508825
826/// `path` is assumed to be drive-relative or drive-absolute.
827fn getDriveLetter(comptime T: type, path: []const T) []const T {
828 const len: usize = switch (T) {
829 // getWin32PathType will only return .drive_absolute/.drive_relative when there is
830 // (1) a valid code point, and (2) a code point < U+10000, so we only need to
831 // get the length determined by the first byte.
832 u8 => std.unicode.utf8ByteSequenceLength(path[0]) catch unreachable,
833 u16 => 1,
834 else => @compileError("unsupported type: " ++ @typeName(T)),
835 };
836 return path[0..len];
837}
838
839test compareDiskDesignators {
840 try testCompareDiskDesignators(true, .drive, "c:", "C:\\");
841 try testCompareDiskDesignators(true, .drive, "C:\\", "C:");
842 try testCompareDiskDesignators(false, .drive, "C:\\", "D:\\");
843 // Case-insensitivity technically applies to non-ASCII drive letters
844 try testCompareDiskDesignators(true, .drive, "λ:\\", "Λ:");
845
846 try testCompareDiskDesignators(true, .unc, "\\\\server", "//server//");
847 try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share");
848 try testCompareDiskDesignators(true, .unc, "\\\\server\\\\share", "/\\server/share\\\\foo");
849 try testCompareDiskDesignators(false, .unc, "\\\\server\\sharefoo", "/\\server/share\\foo");
850 try testCompareDiskDesignators(false, .unc, "\\\\serverfoo\\\\share", "//server/share");
851 try testCompareDiskDesignators(false, .unc, "\\\\server\\", "//server/share");
852}
853
854fn testCompareDiskDesignators(expected_result: bool, kind: DiskDesignatorKind, p1: []const u8, p2: []const u8) !void {
855 var wtf16_buf1: [256]u16 = undefined;
856 const w1_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf1, p1);
857 var wtf16_buf2: [256]u16 = undefined;
858 const w2_len = try std.unicode.wtf8ToWtf16Le(&wtf16_buf2, p2);
859 try std.testing.expectEqual(expected_result, compareDiskDesignators(u8, kind, p1, p2));
860 try std.testing.expectEqual(expected_result, compareDiskDesignators(u16, kind, wtf16_buf1[0..w1_len], wtf16_buf2[0..w2_len]));
861}
862
509863/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.
510pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
864pub fn resolve(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
511865 if (native_os == .windows) {
512866 return resolveWindows(allocator, paths);
513867 } else {
......@@ -516,184 +870,232 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
516870}
517871
518872/// This function is like a series of `cd` statements executed one after another.
519/// It resolves "." and "..", but will not convert relative path to absolute path, use std.fs.Dir.realpath instead.
520/// The result does not have a trailing path separator.
521/// Each drive has its own current working directory.
873/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
874/// an absolute path, use std.fs.Dir.realpath instead.
875/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.
522876/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.
877///
878/// The result will not have a trailing path separator, except for the following scenarios:
879/// - The resolved path is drive-absolute with no components (e.g. `C:\`).
880/// - The resolved path is a UNC path with only a server name, and the input path contained a trailing separator
881/// (e.g. `\\server\`).
882/// - The resolved path is a UNC path with no components after the share name, and the input path contained a
883/// trailing separator (e.g. `\\server\share\`).
884///
885/// Each drive has its own current working directory, which is only resolved via the paths provided.
886/// In the scenario that the resolved path contains a drive-relative path that can't be resolved using the paths alone,
887/// the result will be a drive-relative path.
888/// Similarly, in the scenario that the resolved path contains a rooted path that can't be resolved using the paths alone,
889/// the result will be a rooted path.
890///
523891/// Note: all usage of this function should be audited due to the existence of symlinks.
524892/// Without performing actual syscalls, resolving `..` could be incorrect.
525893/// This API may break in the future: https://github.com/ziglang/zig/issues/13613
526pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {
527 assert(paths.len > 0);
528
529 // determine which disk designator we will result with, if any
530 var result_drive_buf = "_:".*;
531 var disk_designator: []const u8 = "";
532 var drive_kind = WindowsPath.Kind.None;
533 var have_abs_path = false;
534 var first_index: usize = 0;
535 for (paths, 0..) |p, i| {
536 const parsed = windowsParsePath(p);
537 if (parsed.is_abs) {
538 have_abs_path = true;
539 first_index = i;
894pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
895 // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
896 // (we use `* 3` because stackFallback uses 1 usize as a length)
897 var bit_set_allocator_state = std.heap.stackFallback(@sizeOf(usize) * 3, allocator);
898 const bit_set_allocator = bit_set_allocator_state.get();
899 var relevant_paths = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(bit_set_allocator, paths.len);
900 defer relevant_paths.deinit(bit_set_allocator);
901
902 // Iterate the paths backwards, marking the relevant paths along the way.
903 // This also allows us to break from the loop whenever any earlier paths are known to be irrelevant.
904 var first_path_i: usize = paths.len;
905 const effective_root_path: WindowsPath2(u8) = root: {
906 var last_effective_root_path: WindowsPath2(u8) = .{ .kind = .relative, .root = "" };
907 var last_rooted_path_i: ?usize = null;
908 var last_drive_relative_path_i: usize = undefined;
909 while (first_path_i > 0) {
910 first_path_i -= 1;
911 const parsed = parsePathWindows(u8, paths[first_path_i]);
912 switch (parsed.kind) {
913 .unc_absolute, .root_local_device, .local_device => {
914 switch (last_effective_root_path.kind) {
915 .rooted => {},
916 .drive_relative => continue,
917 else => {
918 relevant_paths.set(first_path_i);
919 },
920 }
921 break :root parsed;
922 },
923 .drive_relative, .drive_absolute => {
924 switch (last_effective_root_path.kind) {
925 .drive_relative => if (!compareDiskDesignators(u8, .drive, parsed.root, last_effective_root_path.root)) {
926 continue;
927 } else if (last_rooted_path_i != null) {
928 break :root .{ .kind = .drive_absolute, .root = parsed.root };
929 },
930 .relative => last_effective_root_path = parsed,
931 .rooted => {
932 // This is the end of the line, since the rooted path will always be relative
933 // to this drive letter, and even if the current path is drive-relative, the
934 // rooted-ness makes that irrelevant.
935 //
936 // Therefore, force the kind of the effective root to be drive-absolute in order to
937 // properly resolve a rooted path against a drive-relative one, as the result should
938 // always be drive-absolute.
939 break :root .{ .kind = .drive_absolute, .root = parsed.root };
940 },
941 .drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
942 }
943 relevant_paths.set(first_path_i);
944 last_drive_relative_path_i = first_path_i;
945 if (parsed.kind == .drive_absolute) {
946 break :root parsed;
947 }
948 },
949 .relative => {
950 switch (last_effective_root_path.kind) {
951 .rooted => continue,
952 .relative => last_effective_root_path = parsed,
953 else => {},
954 }
955 relevant_paths.set(first_path_i);
956 },
957 .rooted => {
958 switch (last_effective_root_path.kind) {
959 .drive_relative => {},
960 .relative => last_effective_root_path = parsed,
961 .rooted => continue,
962 .drive_absolute, .unc_absolute, .root_local_device, .local_device => unreachable,
963 }
964 if (last_rooted_path_i == null) {
965 last_rooted_path_i = first_path_i;
966 relevant_paths.set(first_path_i);
967 }
968 },
969 }
540970 }
541 switch (parsed.kind) {
542 .Drive => {
543 result_drive_buf[0] = ascii.toUpper(parsed.disk_designator[0]);
544 disk_designator = result_drive_buf[0..];
545 drive_kind = WindowsPath.Kind.Drive;
546 },
547 .NetworkShare => {
548 disk_designator = parsed.disk_designator;
549 drive_kind = WindowsPath.Kind.NetworkShare;
550 },
551 .None => {},
971 // After iterating, if the pending effective root is drive-relative then that means
972 // nothing has led to forcing a drive-absolute root (a path that allows resolving the
973 // drive-specific CWD would cause an early break), so we now need to ignore all paths
974 // before the most recent drive-relative one. For example, if we're resolving
975 // { "\\rooted", "relative", "C:drive-relative" }
976 // then the `\rooted` and `relative` needs to be ignored since we can't
977 // know what the rooted path is rooted against as that'd require knowing the CWD.
978 if (last_effective_root_path.kind == .drive_relative) {
979 for (0..last_drive_relative_path_i) |i| {
980 relevant_paths.unset(i);
981 }
552982 }
553 }
983 break :root last_effective_root_path;
984 };
554985
555 // if we will result with a disk designator, loop again to determine
556 // which is the last time the disk designator is absolutely specified, if any
557 // and count up the max bytes for paths related to this disk designator
558 if (drive_kind != WindowsPath.Kind.None) {
559 have_abs_path = false;
560 first_index = 0;
561 var correct_disk_designator = false;
562
563 for (paths, 0..) |p, i| {
564 const parsed = windowsParsePath(p);
565 if (parsed.kind != WindowsPath.Kind.None) {
566 if (parsed.kind == drive_kind) {
567 correct_disk_designator = compareDiskDesignators(drive_kind, disk_designator, parsed.disk_designator);
568 } else {
569 continue;
570 }
986 var result: std.ArrayList(u8) = .empty;
987 defer result.deinit(allocator);
988
989 var want_path_sep_between_root_and_component = false;
990 switch (effective_root_path.kind) {
991 .root_local_device, .local_device => {
992 try result.ensureUnusedCapacity(allocator, 3);
993 result.appendSliceAssumeCapacity("\\\\");
994 result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
995 want_path_sep_between_root_and_component = true;
996 },
997 .drive_absolute, .drive_relative => {
998 try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);
999 result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
1000 result.appendAssumeCapacity(':');
1001 if (effective_root_path.kind == .drive_absolute) {
1002 result.appendAssumeCapacity('\\');
5711003 }
572 if (!correct_disk_designator) {
573 continue;
1004 },
1005 .unc_absolute => {
1006 const unc = parseUNC(u8, effective_root_path.root);
1007
1008 const root_len = len: {
1009 var len: usize = 2 + unc.server.len + unc.share.len;
1010 if (unc.sep_after_server) len += 1;
1011 if (unc.sep_after_share) len += 1;
1012 break :len len;
1013 };
1014 try result.ensureUnusedCapacity(allocator, root_len);
1015 result.appendSliceAssumeCapacity("\\\\");
1016 if (unc.server.len > 0 or unc.sep_after_server) {
1017 result.appendSliceAssumeCapacity(unc.server);
1018 if (unc.sep_after_server)
1019 result.appendAssumeCapacity('\\')
1020 else
1021 want_path_sep_between_root_and_component = true;
5741022 }
575 if (parsed.is_abs) {
576 first_index = i;
577 have_abs_path = true;
1023 if (unc.share.len > 0) {
1024 result.appendSliceAssumeCapacity(unc.share);
1025 if (unc.sep_after_share)
1026 result.appendAssumeCapacity('\\')
1027 else
1028 want_path_sep_between_root_and_component = true;
5781029 }
579 }
1030 },
1031 .rooted => {
1032 try result.append(allocator, '\\');
1033 },
1034 .relative => {},
5801035 }
5811036
582 // Allocate result and fill in the disk designator.
583 var result = std.array_list.Managed(u8).init(allocator);
584 defer result.deinit();
585
586 const disk_designator_len: usize = l: {
587 if (!have_abs_path) break :l 0;
588 switch (drive_kind) {
589 .Drive => {
590 try result.appendSlice(disk_designator);
591 break :l disk_designator.len;
592 },
593 .NetworkShare => {
594 var it = mem.tokenizeAny(u8, paths[first_index], "/\\");
595 const server_name = it.next().?;
596 const other_name = it.next().?;
597
598 try result.ensureUnusedCapacity(2 + 1 + server_name.len + other_name.len);
599 result.appendSliceAssumeCapacity("\\\\");
600 result.appendSliceAssumeCapacity(server_name);
601 result.appendAssumeCapacity('\\');
602 result.appendSliceAssumeCapacity(other_name);
603
604 break :l result.items.len;
605 },
606 .None => {
607 break :l 1;
608 },
609 }
610 };
611
612 var correct_disk_designator = true;
1037 const root_len = result.items.len;
6131038 var negative_count: usize = 0;
1039 for (paths[first_path_i..], first_path_i..) |path, i| {
1040 if (!relevant_paths.isSet(i)) continue;
6141041
615 for (paths[first_index..]) |p| {
616 const parsed = windowsParsePath(p);
617
618 if (parsed.kind != .None) {
619 if (parsed.kind == drive_kind) {
620 const dd = result.items[0..disk_designator_len];
621 correct_disk_designator = compareDiskDesignators(drive_kind, dd, parsed.disk_designator);
622 } else {
623 continue;
624 }
625 }
626 if (!correct_disk_designator) {
627 continue;
628 }
629 var it = mem.tokenizeAny(u8, p[parsed.disk_designator.len..], "/\\");
1042 const parsed = parsePathWindows(u8, path);
1043 const skip_len = parsed.root.len;
1044 var it = mem.tokenizeAny(u8, path[skip_len..], "/\\");
6301045 while (it.next()) |component| {
6311046 if (mem.eql(u8, component, ".")) {
6321047 continue;
6331048 } else if (mem.eql(u8, component, "..")) {
634 if (result.items.len == 0) {
1049 if (result.items.len == 0 or (result.items.len == root_len and effective_root_path.kind == .drive_relative)) {
6351050 negative_count += 1;
6361051 continue;
6371052 }
6381053 while (true) {
639 if (result.items.len == disk_designator_len) {
1054 if (result.items.len == root_len) {
6401055 break;
6411056 }
642 const end_with_sep = switch (result.items[result.items.len - 1]) {
643 '\\', '/' => true,
644 else => false,
645 };
1057 const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
6461058 result.items.len -= 1;
647 if (end_with_sep or result.items.len == 0) break;
1059 if (end_with_sep) break;
6481060 }
649 } else if (!have_abs_path and result.items.len == 0) {
650 try result.appendSlice(component);
1061 } else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {
1062 try result.appendSlice(allocator, component);
6511063 } else {
652 try result.ensureUnusedCapacity(1 + component.len);
1064 try result.ensureUnusedCapacity(allocator, 1 + component.len);
6531065 result.appendAssumeCapacity('\\');
6541066 result.appendSliceAssumeCapacity(component);
6551067 }
6561068 }
6571069 }
6581070
659 if (disk_designator_len != 0 and result.items.len == disk_designator_len) {
660 try result.append('\\');
661 return result.toOwnedSlice();
1071 if (root_len != 0 and result.items.len == root_len and negative_count == 0) {
1072 return result.toOwnedSlice(allocator);
6621073 }
6631074
664 if (result.items.len == 0) {
1075 if (result.items.len == root_len) {
6651076 if (negative_count == 0) {
6661077 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;
6771078 }
678 }
6791079
680 if (negative_count == 0) {
681 return result.toOwnedSlice();
1080 try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);
1081 for (0..negative_count - 1) |_| {
1082 result.appendSliceAssumeCapacity("..\\");
1083 }
1084 result.appendSliceAssumeCapacity("..");
6821085 } else {
683 const real_result = try allocator.alloc(u8, 3 * negative_count + result.items.len);
684 var count = negative_count;
685 var i: usize = 0;
686 while (count > 0) : (count -= 1) {
687 real_result[i..][0..3].* = "..\\".*;
688 i += 3;
1086 const dest = try result.addManyAt(allocator, root_len, 3 * negative_count);
1087 for (0..negative_count) |i| {
1088 dest[i * 3 ..][0..3].* = "..\\".*;
6891089 }
690 @memcpy(real_result[i..][0..result.items.len], result.items);
691 return real_result;
6921090 }
1091
1092 return result.toOwnedSlice(allocator);
6931093}
6941094
6951095/// 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.
1096/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
1097/// an absolute path, use std.fs.Dir.realpath instead.
1098/// ".." components may persist in the resolved path if the resolved path is relative.
6971099/// The result does not have a trailing path separator.
6981100/// This function does not perform any syscalls. Executing this series of path
6991101/// lookups on the actual filesystem may produce different results due to
......@@ -772,10 +1174,14 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
7721174}
7731175
7741176test resolve {
1177 try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");
1178 try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");
7751179 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
7761180 try testResolveWindows(&[_][]const u8{"."}, ".");
7771181 try testResolveWindows(&[_][]const u8{""}, ".");
7781182
1183 try testResolvePosix(&[_][]const u8{ "a", "../../.." }, "../..");
1184 try testResolvePosix(&[_][]const u8{ "..", "", "../../foo" }, "../../../foo");
7791185 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
7801186 try testResolvePosix(&[_][]const u8{"."}, ".");
7811187 try testResolvePosix(&[_][]const u8{""}, ".");
......@@ -792,22 +1198,81 @@ test resolveWindows {
7921198 );
7931199
7941200 try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok");
1201 try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c\\", ".\\..\\foo" }, "C:\\a\\b\\foo");
7951202 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");
7961203 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");
7971204 try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");
7981205 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");
1206 // The first path "sets" the CWD, so the drive-relative path is then relative to that.
1207 try testResolveWindows(&[_][]const u8{ "d:/foo", "d:some/dir//", "D:another" }, "D:\\foo\\some\\dir\\another");
8001208 try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
8011209 try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
802 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "C:\\");
803 try testResolveWindows(&[_][]const u8{ "c:/", "//dir" }, "C:\\dir");
804 try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share\\");
805 try testResolveWindows(&[_][]const u8{ "c:/", "//server//share" }, "\\\\server\\share\\");
806 try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "C:\\some\\dir");
1210 try testResolveWindows(&[_][]const u8{ "\\\\server/share/ignore", "//server/share/bar" }, "\\\\server\\share\\bar");
1211 try testResolveWindows(&[_][]const u8{ "\\/server\\share/", "..", "relative" }, "\\\\server\\share\\relative");
1212 try testResolveWindows(&[_][]const u8{ "\\\\server\\share", "C:drive-relative" }, "C:drive-relative");
1213 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "\\\\");
1214 try testResolveWindows(&[_][]const u8{ "c:/", "//server" }, "\\\\server");
1215 try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share");
1216 try testResolveWindows(&[_][]const u8{ "c:/", "//server//share////" }, "\\\\server\\share\\");
1217 try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "\\\\\\some\\dir");
1218 try testResolveWindows(&[_][]const u8{ "c:foo", "bar" }, "C:foo\\bar");
8071219 try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js");
1220 // Drive-relative stays drive-relative if there's nothing to provide the drive-specific CWD
1221 try testResolveWindows(&[_][]const u8{ "relative", "d:foo" }, "D:foo");
1222 try testResolveWindows(&[_][]const u8{ "../..\\..", "d:foo" }, "D:foo");
1223 try testResolveWindows(&[_][]const u8{ "../..\\..", "\\rooted", "d:foo" }, "D:foo");
1224 try testResolveWindows(&[_][]const u8{ "C:\\foo", "../..\\..", "\\rooted", "d:foo" }, "D:foo");
1225 try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "d:foo" }, "D:..\\..\\foo");
1226 try testResolveWindows(&[_][]const u8{ "D:relevant", "../..\\..", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:..\\..\\foo");
1227 try testResolveWindows(&[_][]const u8{ "ignored", "\\\\.\\ignored", "C:\\ignored", "C:ignored", "\\\\ignored", "d:foo" }, "D:foo");
1228 // Rooted paths remain rooted if there's no absolute path available to resolve the "root"
1229 try testResolveWindows(&[_][]const u8{ "/foo", "bar" }, "\\foo\\bar");
1230 // Rooted against a UNC path
1231 try testResolveWindows(&[_][]const u8{ "//server/share/ignore", "/foo", "bar" }, "\\\\server\\share\\foo\\bar");
1232 try testResolveWindows(&[_][]const u8{ "//server/share/", "/foo" }, "\\\\server\\share\\foo");
1233 try testResolveWindows(&[_][]const u8{ "//server/share", "/foo" }, "\\\\server\\share\\foo");
1234 try testResolveWindows(&[_][]const u8{ "//server/", "/foo" }, "\\\\server\\foo");
1235 try testResolveWindows(&[_][]const u8{ "//server", "/foo" }, "\\\\server\\foo");
1236 try testResolveWindows(&[_][]const u8{ "//", "/foo" }, "\\\\foo");
1237 // Rooted against a drive-relative path
1238 try testResolveWindows(&[_][]const u8{ "C:", "/foo", "bar" }, "C:\\foo\\bar");
1239 try testResolveWindows(&[_][]const u8{ "C:\\ignore", "C:", "/foo", "bar" }, "C:\\foo\\bar");
1240 try testResolveWindows(&[_][]const u8{ "C:\\ignore", "\\foo", "C:bar" }, "C:\\foo\\bar");
1241 // Only the last rooted path is relevant
1242 try testResolveWindows(&[_][]const u8{ "\\ignore", "\\foo" }, "\\foo");
1243 try testResolveWindows(&[_][]const u8{ "c:ignore", "ignore", "\\ignore", "\\foo" }, "C:\\foo");
1244 // Rooted is only relevant to a drive-relative if there's a previous drive-* path
1245 try testResolveWindows(&[_][]const u8{ "\\ignore", "C:foo" }, "C:foo");
1246 try testResolveWindows(&[_][]const u8{ "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
1247 try testResolveWindows(&[_][]const u8{ "c:ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
1248 try testResolveWindows(&[_][]const u8{ "c:\\ignore", "\\ignore", "\\rooted", "C:foo" }, "C:\\rooted\\foo");
1249 try testResolveWindows(&[_][]const u8{ "d:\\ignore", "\\ignore", "\\ignore2", "C:foo" }, "C:foo");
1250 // Root local device paths
1251 try testResolveWindows(&[_][]const u8{"\\/."}, "\\\\.");
1252 try testResolveWindows(&[_][]const u8{ "\\/.", "C:drive-relative" }, "C:drive-relative");
1253 try testResolveWindows(&[_][]const u8{"/\\?"}, "\\\\?");
1254 try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\.", "foo" }, "\\\\.\\foo");
1255 try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "foo" }, "\\\\?\\foo");
1256 try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "//.", "ignore", "\\foo" }, "\\\\.\\foo");
1257 try testResolveWindows(&[_][]const u8{ "ignore", "c:\\ignore", "\\\\?", "ignore", "\\foo" }, "\\\\?\\foo");
8081258
8091259 // Keep relative paths relative.
8101260 try testResolveWindows(&[_][]const u8{"a/b"}, "a\\b");
1261 try testResolveWindows(&[_][]const u8{".."}, "..");
1262 try testResolveWindows(&[_][]const u8{"../.."}, "..\\..");
1263 try testResolveWindows(&[_][]const u8{ "C:foo", "../.." }, "C:..");
1264 try testResolveWindows(&[_][]const u8{ "d:foo", "../..\\.." }, "D:..\\..");
1265
1266 // Local device paths treat the \\.\ or \\?\ as the "root", everything afterwards is treated as a regular component.
1267 try testResolveWindows(&[_][]const u8{ "\\\\?\\C:\\foo", "../bar", "baz" }, "\\\\?\\C:\\bar\\baz");
1268 try testResolveWindows(&[_][]const u8{ "\\\\.\\C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
1269 try testResolveWindows(&[_][]const u8{ "//./C:/foo", "../../../../bar", "baz" }, "\\\\.\\bar\\baz");
1270 try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", ".." }, "\\\\.");
1271 try testResolveWindows(&[_][]const u8{ "\\\\.\\foo", "..\\.." }, "\\\\.");
1272
1273 // Paths are assumed to be Win32, so paths that are likely NT paths are treated as a rooted path.
1274 try testResolveWindows(&[_][]const u8{ "\\??\\C:\\foo", "/bar", "baz" }, "\\bar\\baz");
1275 try testResolveWindows(&[_][]const u8{ "C:\\", "\\??\\C:\\foo", "bar" }, "C:\\??\\C:\\foo\\bar");
8111276}
8121277
8131278test resolvePosix {
......@@ -855,63 +1320,18 @@ pub fn dirname(path: []const u8) ?[]const u8 {
8551320}
8561321
8571322pub fn dirnameWindows(path: []const u8) ?[]const u8 {
858 if (path.len == 0)
859 return null;
860
861 const root_slice = diskDesignatorWindows(path);
862 if (path.len == root_slice.len)
863 return null;
864
865 const have_root_slash = path.len > root_slice.len and (path[root_slice.len] == '/' or path[root_slice.len] == '\\');
866
867 var end_index: usize = path.len - 1;
868
869 while (path[end_index] == '/' or path[end_index] == '\\') {
870 if (end_index == 0)
871 return null;
872 end_index -= 1;
873 }
874
875 while (path[end_index] != '/' and path[end_index] != '\\') {
876 if (end_index == 0)
877 return null;
878 end_index -= 1;
879 }
880
881 if (have_root_slash and end_index == root_slice.len) {
882 end_index += 1;
883 }
884
885 if (end_index == 0)
886 return null;
887
888 return path[0..end_index];
1323 return dirnameInner(.windows, path);
8891324}
8901325
8911326pub fn dirnamePosix(path: []const u8) ?[]const u8 {
892 if (path.len == 0)
893 return null;
894
895 var end_index: usize = path.len - 1;
896 while (path[end_index] == '/') {
897 if (end_index == 0)
898 return null;
899 end_index -= 1;
900 }
901
902 while (path[end_index] != '/') {
903 if (end_index == 0)
904 return null;
905 end_index -= 1;
906 }
907
908 if (end_index == 0 and path[0] == '/')
909 return path[0..1];
910
911 if (end_index == 0)
912 return null;
1327 return dirnameInner(.posix, path);
1328}
9131329
914 return path[0..end_index];
1330fn dirnameInner(comptime path_type: PathType, path: []const u8) ?[]const u8 {
1331 var it = ComponentIterator(path_type, u8).init(path);
1332 _ = it.last() orelse return null;
1333 const up = it.previous() orelse return it.root();
1334 return up.path;
9151335}
9161336
9171337test dirnamePosix {
......@@ -930,11 +1350,12 @@ test dirnamePosix {
9301350
9311351test dirnameWindows {
9321352 try testDirnameWindows("c:\\", null);
1353 try testDirnameWindows("c:\\\\", null);
9331354 try testDirnameWindows("c:\\foo", "c:\\");
934 try testDirnameWindows("c:\\foo\\", "c:\\");
1355 try testDirnameWindows("c:\\\\foo\\", "c:\\");
9351356 try testDirnameWindows("c:\\foo\\bar", "c:\\foo");
9361357 try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");
937 try testDirnameWindows("c:\\foo\\bar\\baz", "c:\\foo\\bar");
1358 try testDirnameWindows("c:\\\\foo\\bar\\baz", "c:\\\\foo\\bar");
9381359 try testDirnameWindows("\\", null);
9391360 try testDirnameWindows("\\foo", "\\");
9401361 try testDirnameWindows("\\foo\\", "\\");
......@@ -942,19 +1363,30 @@ test dirnameWindows {
9421363 try testDirnameWindows("\\foo\\bar\\", "\\foo");
9431364 try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
9441365 try testDirnameWindows("c:", null);
945 try testDirnameWindows("c:foo", null);
946 try testDirnameWindows("c:foo\\", null);
1366 try testDirnameWindows("c:foo", "c:");
1367 try testDirnameWindows("c:foo\\", "c:");
9471368 try testDirnameWindows("c:foo\\bar", "c:foo");
9481369 try testDirnameWindows("c:foo\\bar\\", "c:foo");
9491370 try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
9501371 try testDirnameWindows("file:stream", null);
9511372 try testDirnameWindows("dir\\file:stream", "dir");
9521373 try testDirnameWindows("\\\\unc\\share", null);
1374 try testDirnameWindows("\\\\unc\\share\\\\", null);
9531375 try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
9541376 try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
9551377 try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
9561378 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
9571379 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");
1380 try testDirnameWindows("\\\\.", null);
1381 try testDirnameWindows("\\\\.\\", null);
1382 try testDirnameWindows("\\\\.\\device", "\\\\.\\");
1383 try testDirnameWindows("\\\\.\\device\\", "\\\\.\\");
1384 try testDirnameWindows("\\\\.\\device\\foo", "\\\\.\\device");
1385 try testDirnameWindows("\\\\?", null);
1386 try testDirnameWindows("\\\\?\\", null);
1387 try testDirnameWindows("\\\\?\\device", "\\\\?\\");
1388 try testDirnameWindows("\\\\?\\device\\", "\\\\?\\");
1389 try testDirnameWindows("\\\\?\\device\\foo", "\\\\?\\device");
9581390 try testDirnameWindows("/a/b/", "/a");
9591391 try testDirnameWindows("/a/b", "/a");
9601392 try testDirnameWindows("/a", "/");
......@@ -974,7 +1406,7 @@ fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {
9741406
9751407fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {
9761408 if (dirnameWindows(input)) |output| {
977 try testing.expect(mem.eql(u8, output, expected_output.?));
1409 try testing.expectEqualStrings(expected_output.?, output);
9781410 } else {
9791411 try testing.expect(expected_output == null);
9801412 }
......@@ -989,56 +1421,17 @@ pub fn basename(path: []const u8) []const u8 {
9891421}
9901422
9911423pub fn basenamePosix(path: []const u8) []const u8 {
992 if (path.len == 0)
993 return &[_]u8{};
994
995 var end_index: usize = path.len - 1;
996 while (path[end_index] == '/') {
997 if (end_index == 0)
998 return &[_]u8{};
999 end_index -= 1;
1000 }
1001 var start_index: usize = end_index;
1002 end_index += 1;
1003 while (path[start_index] != '/') {
1004 if (start_index == 0)
1005 return path[0..end_index];
1006 start_index -= 1;
1007 }
1008
1009 return path[start_index + 1 .. end_index];
1424 return basenameInner(.posix, path);
10101425}
10111426
10121427pub fn basenameWindows(path: []const u8) []const u8 {
1013 if (path.len == 0)
1014 return &[_]u8{};
1015
1016 var end_index: usize = path.len - 1;
1017 while (true) {
1018 const byte = path[end_index];
1019 if (byte == '/' or byte == '\\') {
1020 if (end_index == 0)
1021 return &[_]u8{};
1022 end_index -= 1;
1023 continue;
1024 }
1025 if (byte == ':' and end_index == 1) {
1026 return &[_]u8{};
1027 }
1028 break;
1029 }
1030
1031 var start_index: usize = end_index;
1032 end_index += 1;
1033 while (path[start_index] != '/' and path[start_index] != '\\' and
1034 !(path[start_index] == ':' and start_index == 1))
1035 {
1036 if (start_index == 0)
1037 return path[0..end_index];
1038 start_index -= 1;
1039 }
1428 return basenameInner(.windows, path);
1429}
10401430
1041 return path[start_index + 1 .. end_index];
1431fn basenameInner(comptime path_type: PathType, path: []const u8) []const u8 {
1432 var it = ComponentIterator(path_type, u8).init(path);
1433 const last = it.last() orelse return &[_]u8{};
1434 return last.name;
10421435}
10431436
10441437test basename {
......@@ -1053,7 +1446,9 @@ test basename {
10531446 try testBasename("/aaa/", "aaa");
10541447 try testBasename("/aaa/b", "b");
10551448 try testBasename("/a/b", "b");
1056 try testBasename("//a", "a");
1449
1450 // For Windows, this is a UNC path that only has a server name component.
1451 try testBasename("//a", if (native_os == .windows) "" else "a");
10571452
10581453 try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
10591454 try testBasenamePosix("\\basename.ext", "\\basename.ext");
......@@ -1076,6 +1471,12 @@ test basename {
10761471 try testBasenameWindows("C:basename.ext", "basename.ext");
10771472 try testBasenameWindows("C:basename.ext\\", "basename.ext");
10781473 try testBasenameWindows("C:basename.ext\\\\", "basename.ext");
1474 try testBasenameWindows("\\\\.", "");
1475 try testBasenameWindows("\\\\.\\", "");
1476 try testBasenameWindows("\\\\.\\basename.ext", "basename.ext");
1477 try testBasenameWindows("\\\\?", "");
1478 try testBasenameWindows("\\\\?\\", "");
1479 try testBasenameWindows("\\\\?\\basename.ext", "basename.ext");
10791480 try testBasenameWindows("C:foo", "foo");
10801481 try testBasenameWindows("file:stream", "file:stream");
10811482}
......@@ -1092,11 +1493,15 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
10921493 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
10931494}
10941495
1496pub const RelativeError = std.process.GetCwdAllocError;
1497
10951498/// Returns the relative path from `from` to `to`. If `from` and `to` each
10961499/// resolve to the same path (after calling `resolve` on each), a zero-length
10971500/// string is returned.
1098/// On Windows this canonicalizes the drive to a capital letter and paths to `\\`.
1099pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1501/// On Windows, the result is not guaranteed to be relative, as the paths may be
1502/// on different volumes. In that case, the result will be the canonicalized absolute
1503/// path of `to`.
1504pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) RelativeError![]u8 {
11001505 if (native_os == .windows) {
11011506 return relativeWindows(allocator, from, to);
11021507 } else {
......@@ -1105,30 +1510,53 @@ pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
11051510}
11061511
11071512pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1108 const cwd = try process.getCwdAlloc(allocator);
1109 defer allocator.free(cwd);
1110 const resolved_from = try resolveWindows(allocator, &[_][]const u8{ cwd, from });
1111 defer allocator.free(resolved_from);
1513 if (native_os != .windows) @compileError("this function relies on Windows-specific semantics");
1514
1515 const parsed_from = parsePathWindows(u8, from);
1516 const parsed_to = parsePathWindows(u8, to);
11121517
1518 const result_is_always_to = x: {
1519 if (parsed_from.kind != parsed_to.kind) {
1520 break :x false;
1521 }
1522 switch (parsed_from.kind) {
1523 .drive_relative, .drive_absolute => {
1524 break :x !compareDiskDesignators(u8, .drive, parsed_from.root, parsed_to.root);
1525 },
1526 .unc_absolute => {
1527 break :x !compareDiskDesignators(u8, .unc, parsed_from.root, parsed_to.root);
1528 },
1529 .relative, .rooted, .local_device => break :x false,
1530 .root_local_device => break :x true,
1531 }
1532 };
1533
1534 if (result_is_always_to) {
1535 return windowsResolveAgainstCwd(allocator, to, parsed_to);
1536 }
1537
1538 const resolved_from = try windowsResolveAgainstCwd(allocator, from, parsed_from);
1539 defer allocator.free(resolved_from);
11131540 var clean_up_resolved_to = true;
1114 const resolved_to = try resolveWindows(allocator, &[_][]const u8{ cwd, to });
1541 const resolved_to = try windowsResolveAgainstCwd(allocator, to, parsed_to);
11151542 defer if (clean_up_resolved_to) allocator.free(resolved_to);
11161543
1117 const parsed_from = windowsParsePath(resolved_from);
1118 const parsed_to = windowsParsePath(resolved_to);
1544 const parsed_resolved_from = parsePathWindows(u8, resolved_from);
1545 const parsed_resolved_to = parsePathWindows(u8, resolved_to);
1546
11191547 const result_is_to = x: {
1120 if (parsed_from.kind != parsed_to.kind) {
1548 if (parsed_resolved_from.kind != parsed_resolved_to.kind) {
11211549 break :x true;
1122 } else switch (parsed_from.kind) {
1123 .NetworkShare => {
1124 break :x !networkShareServersEql(parsed_to.disk_designator, parsed_from.disk_designator);
1125 },
1126 .Drive => {
1127 break :x ascii.toUpper(parsed_from.disk_designator[0]) != ascii.toUpper(parsed_to.disk_designator[0]);
1550 }
1551 switch (parsed_resolved_from.kind) {
1552 .drive_absolute, .drive_relative => {
1553 break :x !compareDiskDesignators(u8, .drive, parsed_resolved_from.root, parsed_resolved_to.root);
11281554 },
1129 .None => {
1130 break :x false;
1555 .unc_absolute => {
1556 break :x !compareDiskDesignators(u8, .unc, parsed_resolved_from.root, parsed_resolved_to.root);
11311557 },
1558 .relative, .rooted, .local_device => break :x false,
1559 .root_local_device => break :x true,
11321560 }
11331561 };
11341562
......@@ -1137,8 +1565,8 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !
11371565 return resolved_to;
11381566 }
11391567
1140 var from_it = mem.tokenizeAny(u8, resolved_from, "/\\");
1141 var to_it = mem.tokenizeAny(u8, resolved_to, "/\\");
1568 var from_it = mem.tokenizeAny(u8, resolved_from[parsed_resolved_from.root.len..], "/\\");
1569 var to_it = mem.tokenizeAny(u8, resolved_to[parsed_resolved_to.root.len..], "/\\");
11421570 while (true) {
11431571 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
11441572 const to_rest = to_it.rest();
......@@ -1170,11 +1598,101 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !
11701598
11711599 return allocator.realloc(result, result_index);
11721600 }
1173
11741601 return [_]u8{};
11751602}
11761603
1604fn windowsResolveAgainstCwd(allocator: Allocator, path: []const u8, parsed: WindowsPath2(u8)) ![]u8 {
1605 // Space for 256 WTF-16 code units; potentially 3 WTF-8 bytes per WTF-16 code unit
1606 var temp_allocator_state = std.heap.stackFallback(256 * 3, allocator);
1607 return switch (parsed.kind) {
1608 .drive_absolute,
1609 .unc_absolute,
1610 .root_local_device,
1611 .local_device,
1612 => try resolveWindows(allocator, &.{path}),
1613 .relative => blk: {
1614 const temp_allocator = temp_allocator_state.get();
1615
1616 const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1617 const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1618
1619 const wtf8_len = std.unicode.calcWtf8Len(cwd_w);
1620 const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1621 defer temp_allocator.free(wtf8_buf);
1622 assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len);
1623
1624 break :blk try resolveWindows(allocator, &.{ wtf8_buf, path });
1625 },
1626 .rooted => blk: {
1627 const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1628 const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1629 const parsed_cwd = parsePathWindows(u16, cwd_w);
1630 switch (parsed_cwd.kind) {
1631 .drive_absolute => {
1632 var drive_buf = "_:\\".*;
1633 drive_buf[0] = @truncate(cwd_w[0]);
1634 break :blk try resolveWindows(allocator, &.{ &drive_buf, path });
1635 },
1636 .unc_absolute => {
1637 const temp_allocator = temp_allocator_state.get();
1638 var root_buf = try temp_allocator.alloc(u8, parsed_cwd.root.len * 3);
1639 defer temp_allocator.free(root_buf);
1640
1641 const wtf8_len = std.unicode.wtf16LeToWtf8(root_buf, parsed_cwd.root);
1642 const root = root_buf[0..wtf8_len];
1643 break :blk try resolveWindows(allocator, &.{ root, path });
1644 },
1645 // Effectively a malformed CWD, give up and just return a normalized path
1646 else => break :blk try resolveWindows(allocator, &.{path}),
1647 }
1648 },
1649 .drive_relative => blk: {
1650 const temp_allocator = temp_allocator_state.get();
1651 const drive_cwd = drive_cwd: {
1652 const peb_cwd = windows.peb().ProcessParameters.CurrentDirectory.DosPath;
1653 const cwd_w = (peb_cwd.Buffer.?)[0 .. peb_cwd.Length / 2];
1654 const parsed_cwd = parsePathWindows(u16, cwd_w);
1655
1656 if (parsed_cwd.kind == .drive_absolute) {
1657 const drive_letter_w = parsed_cwd.root[0];
1658 const drive_letters_match = drive_letter_w <= 0x7F and
1659 ascii.toUpper(@intCast(drive_letter_w)) == ascii.toUpper(parsed.root[0]);
1660 if (drive_letters_match) {
1661 const wtf8_len = std.unicode.calcWtf8Len(cwd_w);
1662 const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1663 assert(std.unicode.wtf16LeToWtf8(wtf8_buf, cwd_w) == wtf8_len);
1664 break :drive_cwd wtf8_buf[0..];
1665 }
1666
1667 // Per-drive CWD's are stored in special semi-hidden environment variables
1668 // of the format `=<drive-letter>:`, e.g. `=C:`. This type of CWD is
1669 // purely a shell concept, so there's no guarantee that it'll be set
1670 // or that it'll even be accurate.
1671 var key_buf = std.unicode.wtf8ToWtf16LeStringLiteral("=_:").*;
1672 key_buf[1] = parsed.root[0];
1673 if (std.process.getenvW(&key_buf)) |drive_cwd_w| {
1674 const wtf8_len = std.unicode.calcWtf8Len(drive_cwd_w);
1675 const wtf8_buf = try temp_allocator.alloc(u8, wtf8_len);
1676 assert(std.unicode.wtf16LeToWtf8(wtf8_buf, drive_cwd_w) == wtf8_len);
1677 break :drive_cwd wtf8_buf[0..];
1678 }
1679 }
1680
1681 const drive_buf = try temp_allocator.alloc(u8, 3);
1682 drive_buf[0] = parsed.root[0];
1683 drive_buf[1] = ':';
1684 drive_buf[2] = '\\';
1685 break :drive_cwd drive_buf;
1686 };
1687 defer temp_allocator.free(drive_cwd);
1688 break :blk try resolveWindows(allocator, &.{ drive_cwd, path });
1689 },
1690 };
1691}
1692
11771693pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1694 if (native_os == .windows) @compileError("this function relies on semantics that do not apply to Windows");
1695
11781696 const cwd = try process.getCwdAlloc(allocator);
11791697 defer allocator.free(cwd);
11801698 const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });
......@@ -1217,51 +1735,59 @@ pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]
12171735}
12181736
12191737test relative {
1220 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
1221 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
1222 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
1223 try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
1224 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
1225 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
1226 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
1227 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
1228 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
1229 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
1230 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
1231 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
1232 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
1233 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
1234 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
1235 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
1236 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
1237 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
1238 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
1239 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
1240 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz");
1241 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux");
1242 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
1243 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
1244
1245 try testRelativeWindows("a/b/c", "a\\b", "..");
1246 try testRelativeWindows("a/b/c", "a", "..\\..");
1247 try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");
1248
1249 try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
1250 // Unicode-aware case-insensitive path comparison
1251 try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");
1252
1253 try testRelativePosix("/var/lib", "/var", "..");
1254 try testRelativePosix("/var/lib", "/bin", "../../bin");
1255 try testRelativePosix("/var/lib", "/var/lib", "");
1256 try testRelativePosix("/var/lib", "/var/apache", "../apache");
1257 try testRelativePosix("/var/", "/var/lib", "lib");
1258 try testRelativePosix("/", "/var/lib", "var/lib");
1259 try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
1260 try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
1261 try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
1262 try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
1263 try testRelativePosix("/baz-quux", "/baz", "../baz");
1264 try testRelativePosix("/baz", "/baz-quux", "../baz-quux");
1738 if (native_os == .windows) {
1739 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
1740 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
1741 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
1742 try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
1743 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
1744 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
1745 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
1746 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
1747 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
1748 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
1749 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
1750 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
1751 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
1752 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
1753 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
1754 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
1755 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
1756 try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz");
1757 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
1758 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
1759 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
1760 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz");
1761 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux");
1762 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
1763 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
1764
1765 try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo");
1766 try testRelativeWindows("c:foo", "c:foo\\bar", "bar");
1767 try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo");
1768 try testRelativeWindows("\\foo", "\\foo\\bar", "bar");
1769
1770 try testRelativeWindows("a/b/c", "a\\b", "..");
1771 try testRelativeWindows("a/b/c", "a", "..\\..");
1772 try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");
1773
1774 try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
1775 // Unicode-aware case-insensitive path comparison
1776 try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");
1777 } else {
1778 try testRelativePosix("/var/lib", "/var", "..");
1779 try testRelativePosix("/var/lib", "/bin", "../../bin");
1780 try testRelativePosix("/var/lib", "/var/lib", "");
1781 try testRelativePosix("/var/lib", "/var/apache", "../apache");
1782 try testRelativePosix("/var/", "/var/lib", "lib");
1783 try testRelativePosix("/", "/var/lib", "var/lib");
1784 try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");
1785 try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");
1786 try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");
1787 try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");
1788 try testRelativePosix("/baz-quux", "/baz", "../baz");
1789 try testRelativePosix("/baz", "/baz-quux", "../baz-quux");
1790 }
12651791}
12661792
12671793fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
......@@ -1391,7 +1917,10 @@ test stem {
13911917pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
13921918 return struct {
13931919 path: []const T,
1394 root_end_index: usize = 0,
1920 /// Length of the root with at most one trailing path separator included (e.g. `C:/`).
1921 root_len: usize,
1922 /// Length of the root with all trailing path separators included (e.g. `C://///`).
1923 root_end_index: usize,
13951924 start_index: usize = 0,
13961925 end_index: usize = 0,
13971926
......@@ -1406,100 +1935,45 @@ pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
14061935 path: []const T,
14071936 };
14081937
1409 const InitError = switch (path_type) {
1410 .windows => error{BadPathName},
1411 else => error{},
1412 };
1413
14141938 /// After `init`, `next` will return the first component after the root
14151939 /// (there is no need to call `first` after `init`).
14161940 /// To iterate backwards (from the end of the path to the beginning), call `last`
14171941 /// after `init` and then iterate via `previous` calls.
1418 /// For Windows paths, `error.BadPathName` is returned if the `path` has an explicit
1419 /// namespace prefix (`\\.\`, `\\?\`, or `\??\`) or if it is a UNC path with more
1420 /// than two path separators at the beginning.
1421 pub fn init(path: []const T) InitError!Self {
1422 const root_end_index: usize = switch (path_type) {
1942 /// For Windows paths, paths are assumed to be in the Win32 namespace.
1943 pub fn init(path: []const T) Self {
1944 const root_len: usize = switch (path_type) {
14231945 .posix, .uefi => posix: {
14241946 // Root on UEFI and POSIX only differs by the path separator
1425 var root_end_index: usize = 0;
1426 while (true) : (root_end_index += 1) {
1427 if (root_end_index >= path.len or !path_type.isSep(T, path[root_end_index])) {
1428 break;
1429 }
1430 }
1431 break :posix root_end_index;
1947 break :posix if (path.len > 0 and path_type.isSep(T, path[0])) 1 else 0;
14321948 },
14331949 .windows => windows: {
1434 // Namespaces other than the Win32 file namespace are tricky
1435 // and basically impossible to determine a 'root' for, since it's
1436 // possible to construct an effectively arbitrarily long 'root',
1437 // e.g. `\\.\GLOBALROOT\??\UNC\localhost\C$\foo` is a
1438 // possible path that would be effectively equivalent to
1439 // `C:\foo`, and the `GLOBALROOT\??\` part can also be recursive,
1440 // so `GLOBALROOT\??\GLOBALROOT\??\...` would work for any number
1441 // of repetitions. Therefore, paths with an explicit namespace prefix
1442 // (\\.\, \??\, \\?\) are not allowed here.
1443 if (std.os.windows.getNamespacePrefix(T, path) != .none) {
1444 return error.BadPathName;
1445 }
1446 const windows_path_type = std.os.windows.getUnprefixedPathType(T, path);
1447 break :windows switch (windows_path_type) {
1448 .relative => 0,
1449 .root_local_device => path.len,
1450 .rooted => 1,
1451 .unc_absolute => unc: {
1452 var end_index: usize = 2;
1453 // Any extra separators between the first two and the server name are not allowed
1454 // and will always lead to STATUS_OBJECT_PATH_INVALID if it is attempted
1455 // to be used.
1456 if (end_index < path.len and path_type.isSep(T, path[end_index])) {
1457 return error.BadPathName;
1458 }
1459 // Server
1460 while (end_index < path.len and !path_type.isSep(T, path[end_index])) {
1461 end_index += 1;
1462 }
1463 // Slash(es) after server
1464 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1465 end_index += 1;
1466 }
1467 // Share
1468 while (end_index < path.len and !path_type.isSep(T, path[end_index])) {
1469 end_index += 1;
1470 }
1471 // Slash(es) after share
1472 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1473 end_index += 1;
1474 }
1475 break :unc end_index;
1476 },
1477 .drive_absolute => drive: {
1478 var end_index: usize = 3;
1479 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1480 end_index += 1;
1481 }
1482 break :drive end_index;
1483 },
1484 .drive_relative => 2,
1485 };
1950 break :windows parsePathWindows(T, path).root.len;
14861951 },
14871952 };
1953 // If there are repeated path separators directly after the root,
1954 // keep track of that info so that they don't have to be dealt with when
1955 // iterating components.
1956 var root_end_index = root_len;
1957 for (path[root_len..]) |c| {
1958 if (!path_type.isSep(T, c)) break;
1959 root_end_index += 1;
1960 }
14881961 return .{
14891962 .path = path,
1963 .root_len = root_len,
14901964 .root_end_index = root_end_index,
14911965 .start_index = root_end_index,
14921966 .end_index = root_end_index,
14931967 };
14941968 }
14951969
1496 /// Returns the root of the path if it is an absolute path, or null otherwise.
1970 /// Returns the root of the path if it is not a relative path, or null otherwise.
14971971 /// For POSIX paths, this will be `/`.
14981972 /// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.
14991973 /// For UEFI paths, this will be `\`.
15001974 pub fn root(self: Self) ?[]const T {
15011975 if (self.root_end_index == 0) return null;
1502 return self.path[0..self.root_end_index];
1976 return self.path[0..self.root_len];
15031977 }
15041978
15051979 /// Returns the first component (from the beginning of the path).
......@@ -1614,7 +2088,7 @@ pub const NativeComponentIterator = ComponentIterator(switch (native_os) {
16142088 else => .posix,
16152089}, u8);
16162090
1617pub fn componentIterator(path: []const u8) !NativeComponentIterator {
2091pub fn componentIterator(path: []const u8) NativeComponentIterator {
16182092 return NativeComponentIterator.init(path);
16192093}
16202094
......@@ -1622,8 +2096,9 @@ test "ComponentIterator posix" {
16222096 const PosixComponentIterator = ComponentIterator(.posix, u8);
16232097 {
16242098 const path = "a/b/c/";
1625 var it = try PosixComponentIterator.init(path);
1626 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);
2099 var it = PosixComponentIterator.init(path);
2100 try std.testing.expectEqual(0, it.root_len);
2101 try std.testing.expectEqual(0, it.root_end_index);
16272102 try std.testing.expect(null == it.root());
16282103 {
16292104 try std.testing.expect(null == it.previous());
......@@ -1669,8 +2144,9 @@ test "ComponentIterator posix" {
16692144
16702145 {
16712146 const path = "/a/b/c/";
1672 var it = try PosixComponentIterator.init(path);
1673 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);
2147 var it = PosixComponentIterator.init(path);
2148 try std.testing.expectEqual(1, it.root_len);
2149 try std.testing.expectEqual(1, it.root_end_index);
16742150 try std.testing.expectEqualStrings("/", it.root().?);
16752151 {
16762152 try std.testing.expect(null == it.previous());
......@@ -1714,10 +2190,59 @@ test "ComponentIterator posix" {
17142190 }
17152191 }
17162192
2193 {
2194 const path = "////a///b///c////";
2195 var it = PosixComponentIterator.init(path);
2196 try std.testing.expectEqual(1, it.root_len);
2197 try std.testing.expectEqual(4, it.root_end_index);
2198 try std.testing.expectEqualStrings("/", it.root().?);
2199 {
2200 try std.testing.expect(null == it.previous());
2201
2202 const first_via_next = it.next().?;
2203 try std.testing.expectEqualStrings("a", first_via_next.name);
2204 try std.testing.expectEqualStrings("////a", first_via_next.path);
2205
2206 const first = it.first().?;
2207 try std.testing.expectEqualStrings("a", first.name);
2208 try std.testing.expectEqualStrings("////a", first.path);
2209
2210 try std.testing.expect(null == it.previous());
2211
2212 const second = it.next().?;
2213 try std.testing.expectEqualStrings("b", second.name);
2214 try std.testing.expectEqualStrings("////a///b", second.path);
2215
2216 const third = it.next().?;
2217 try std.testing.expectEqualStrings("c", third.name);
2218 try std.testing.expectEqualStrings("////a///b///c", third.path);
2219
2220 try std.testing.expect(null == it.next());
2221 }
2222 {
2223 const last = it.last().?;
2224 try std.testing.expectEqualStrings("c", last.name);
2225 try std.testing.expectEqualStrings("////a///b///c", last.path);
2226
2227 try std.testing.expect(null == it.next());
2228
2229 const second_to_last = it.previous().?;
2230 try std.testing.expectEqualStrings("b", second_to_last.name);
2231 try std.testing.expectEqualStrings("////a///b", second_to_last.path);
2232
2233 const third_to_last = it.previous().?;
2234 try std.testing.expectEqualStrings("a", third_to_last.name);
2235 try std.testing.expectEqualStrings("////a", third_to_last.path);
2236
2237 try std.testing.expect(null == it.previous());
2238 }
2239 }
2240
17172241 {
17182242 const path = "/";
1719 var it = try PosixComponentIterator.init(path);
1720 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);
2243 var it = PosixComponentIterator.init(path);
2244 try std.testing.expectEqual(1, it.root_len);
2245 try std.testing.expectEqual(1, it.root_end_index);
17212246 try std.testing.expectEqualStrings("/", it.root().?);
17222247
17232248 try std.testing.expect(null == it.first());
......@@ -1733,8 +2258,9 @@ test "ComponentIterator posix" {
17332258
17342259 {
17352260 const path = "";
1736 var it = try PosixComponentIterator.init(path);
1737 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);
2261 var it = PosixComponentIterator.init(path);
2262 try std.testing.expectEqual(0, it.root_len);
2263 try std.testing.expectEqual(0, it.root_end_index);
17382264 try std.testing.expect(null == it.root());
17392265
17402266 try std.testing.expect(null == it.first());
......@@ -1753,8 +2279,9 @@ test "ComponentIterator windows" {
17532279 const WindowsComponentIterator = ComponentIterator(.windows, u8);
17542280 {
17552281 const path = "a/b\\c//";
1756 var it = try WindowsComponentIterator.init(path);
1757 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);
2282 var it = WindowsComponentIterator.init(path);
2283 try std.testing.expectEqual(0, it.root_len);
2284 try std.testing.expectEqual(0, it.root_end_index);
17582285 try std.testing.expect(null == it.root());
17592286 {
17602287 try std.testing.expect(null == it.previous());
......@@ -1800,8 +2327,9 @@ test "ComponentIterator windows" {
18002327
18012328 {
18022329 const path = "C:\\a/b/c/";
1803 var it = try WindowsComponentIterator.init(path);
1804 try std.testing.expectEqual(@as(usize, 3), it.root_end_index);
2330 var it = WindowsComponentIterator.init(path);
2331 try std.testing.expectEqual(3, it.root_len);
2332 try std.testing.expectEqual(3, it.root_end_index);
18052333 try std.testing.expectEqualStrings("C:\\", it.root().?);
18062334 {
18072335 const first = it.first().?;
......@@ -1835,10 +2363,49 @@ test "ComponentIterator windows" {
18352363 }
18362364 }
18372365
2366 {
2367 const path = "C:\\\\//a/\\/\\b///c////";
2368 var it = WindowsComponentIterator.init(path);
2369 try std.testing.expectEqual(3, it.root_len);
2370 try std.testing.expectEqual(6, it.root_end_index);
2371 try std.testing.expectEqualStrings("C:\\", it.root().?);
2372 {
2373 const first = it.first().?;
2374 try std.testing.expectEqualStrings("a", first.name);
2375 try std.testing.expectEqualStrings("C:\\\\//a", first.path);
2376
2377 const second = it.next().?;
2378 try std.testing.expectEqualStrings("b", second.name);
2379 try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second.path);
2380
2381 const third = it.next().?;
2382 try std.testing.expectEqualStrings("c", third.name);
2383 try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", third.path);
2384
2385 try std.testing.expect(null == it.next());
2386 }
2387 {
2388 const last = it.last().?;
2389 try std.testing.expectEqualStrings("c", last.name);
2390 try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b///c", last.path);
2391
2392 const second_to_last = it.previous().?;
2393 try std.testing.expectEqualStrings("b", second_to_last.name);
2394 try std.testing.expectEqualStrings("C:\\\\//a/\\/\\b", second_to_last.path);
2395
2396 const third_to_last = it.previous().?;
2397 try std.testing.expectEqualStrings("a", third_to_last.name);
2398 try std.testing.expectEqualStrings("C:\\\\//a", third_to_last.path);
2399
2400 try std.testing.expect(null == it.previous());
2401 }
2402 }
2403
18382404 {
18392405 const path = "/";
1840 var it = try WindowsComponentIterator.init(path);
1841 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);
2406 var it = WindowsComponentIterator.init(path);
2407 try std.testing.expectEqual(1, it.root_len);
2408 try std.testing.expectEqual(1, it.root_end_index);
18422409 try std.testing.expectEqualStrings("/", it.root().?);
18432410
18442411 try std.testing.expect(null == it.first());
......@@ -1854,8 +2421,9 @@ test "ComponentIterator windows" {
18542421
18552422 {
18562423 const path = "";
1857 var it = try WindowsComponentIterator.init(path);
1858 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);
2424 var it = WindowsComponentIterator.init(path);
2425 try std.testing.expectEqual(0, it.root_len);
2426 try std.testing.expectEqual(0, it.root_end_index);
18592427 try std.testing.expect(null == it.root());
18602428
18612429 try std.testing.expect(null == it.first());
......@@ -1880,8 +2448,9 @@ test "ComponentIterator windows WTF-16" {
18802448 const L = std.unicode.utf8ToUtf16LeStringLiteral;
18812449
18822450 const path = L("C:\\a/b/c/");
1883 var it = try WindowsComponentIterator.init(path);
1884 try std.testing.expectEqual(@as(usize, 3), it.root_end_index);
2451 var it = WindowsComponentIterator.init(path);
2452 try std.testing.expectEqual(3, it.root_len);
2453 try std.testing.expectEqual(3, it.root_end_index);
18852454 try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);
18862455 {
18872456 const first = it.first().?;
......@@ -1918,55 +2487,60 @@ test "ComponentIterator windows WTF-16" {
19182487test "ComponentIterator roots" {
19192488 // UEFI
19202489 {
1921 var it = try ComponentIterator(.uefi, u8).init("\\\\a");
1922 try std.testing.expectEqualStrings("\\\\", it.root().?);
2490 var it = ComponentIterator(.uefi, u8).init("\\\\a");
2491 try std.testing.expectEqualStrings("\\", it.root().?);
19232492
1924 it = try ComponentIterator(.uefi, u8).init("//a");
2493 it = ComponentIterator(.uefi, u8).init("//a");
19252494 try std.testing.expect(null == it.root());
19262495 }
19272496 // POSIX
19282497 {
1929 var it = try ComponentIterator(.posix, u8).init("//a");
1930 try std.testing.expectEqualStrings("//", it.root().?);
2498 var it = ComponentIterator(.posix, u8).init("//a");
2499 try std.testing.expectEqualStrings("/", it.root().?);
19312500
1932 it = try ComponentIterator(.posix, u8).init("\\\\a");
2501 it = ComponentIterator(.posix, u8).init("\\\\a");
19332502 try std.testing.expect(null == it.root());
19342503 }
19352504 // Windows
19362505 {
19372506 // Drive relative
1938 var it = try ComponentIterator(.windows, u8).init("C:a");
2507 var it = ComponentIterator(.windows, u8).init("C:a");
19392508 try std.testing.expectEqualStrings("C:", it.root().?);
19402509
19412510 // Drive absolute
1942 it = try ComponentIterator(.windows, u8).init("C://a");
1943 try std.testing.expectEqualStrings("C://", it.root().?);
1944 it = try ComponentIterator(.windows, u8).init("C:\\a");
2511 it = ComponentIterator(.windows, u8).init("C:/a");
2512 try std.testing.expectEqualStrings("C:/", it.root().?);
2513 it = ComponentIterator(.windows, u8).init("C:\\a");
19452514 try std.testing.expectEqualStrings("C:\\", it.root().?);
2515 it = ComponentIterator(.windows, u8).init("C:///a");
2516 try std.testing.expectEqualStrings("C:/", it.root().?);
19462517
19472518 // Rooted
1948 it = try ComponentIterator(.windows, u8).init("\\a");
2519 it = ComponentIterator(.windows, u8).init("\\a");
19492520 try std.testing.expectEqualStrings("\\", it.root().?);
1950 it = try ComponentIterator(.windows, u8).init("/a");
2521 it = ComponentIterator(.windows, u8).init("/a");
19512522 try std.testing.expectEqualStrings("/", it.root().?);
19522523
19532524 // Root local device
1954 it = try ComponentIterator(.windows, u8).init("\\\\.");
2525 it = ComponentIterator(.windows, u8).init("\\\\.");
19552526 try std.testing.expectEqualStrings("\\\\.", it.root().?);
1956 it = try ComponentIterator(.windows, u8).init("//?");
2527 it = ComponentIterator(.windows, u8).init("//?");
19572528 try std.testing.expectEqualStrings("//?", it.root().?);
19582529
19592530 // UNC absolute
1960 it = try ComponentIterator(.windows, u8).init("//");
2531 it = ComponentIterator(.windows, u8).init("//");
19612532 try std.testing.expectEqualStrings("//", it.root().?);
1962 it = try ComponentIterator(.windows, u8).init("\\\\a");
2533 it = ComponentIterator(.windows, u8).init("\\\\a");
19632534 try std.testing.expectEqualStrings("\\\\a", it.root().?);
1964 it = try ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
1965 try std.testing.expectEqualStrings("\\\\a\\b\\\\", it.root().?);
1966 it = try ComponentIterator(.windows, u8).init("//a");
2535 it = ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
2536 try std.testing.expectEqualStrings("\\\\a\\b\\", it.root().?);
2537 it = ComponentIterator(.windows, u8).init("//a");
19672538 try std.testing.expectEqualStrings("//a", it.root().?);
1968 it = try ComponentIterator(.windows, u8).init("//a/b//c");
1969 try std.testing.expectEqualStrings("//a/b//", it.root().?);
2539 it = ComponentIterator(.windows, u8).init("//a/b//c");
2540 try std.testing.expectEqualStrings("//a/b/", it.root().?);
2541 // Malformed UNC path with empty server name
2542 it = ComponentIterator(.windows, u8).init("\\\\\\a\\b\\c");
2543 try std.testing.expectEqualStrings("\\\\\\a\\", it.root().?);
19702544 }
19712545}
19722546
lib/std/fs/test.zig+1-1
......@@ -56,7 +56,7 @@ const PathType = enum {
5656 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
5757 var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
5858 const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);
59 const windows_path_type = windows.getUnprefixedPathType(u8, dir_path);
59 const windows_path_type = windows.getWin32PathType(u8, dir_path);
6060 switch (windows_path_type) {
6161 .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),
6262 .drive_absolute => {
lib/std/os/windows.zig+318-256
......@@ -836,8 +836,11 @@ pub fn CreateSymbolicLink(
836836 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
837837 var is_target_absolute = false;
838838 const final_target_path = target_path: {
839 switch (getNamespacePrefix(u16, target_path)) {
840 .none => switch (getUnprefixedPathType(u16, target_path)) {
839 if (hasCommonNtPrefix(u16, target_path)) {
840 // Already an NT path, no need to do anything to it
841 break :target_path target_path;
842 } else {
843 switch (getWin32PathType(u16, target_path)) {
841844 // Rooted paths need to avoid getting put through wToPrefixedFileW
842845 // (and they are treated as relative in this context)
843846 // Note: It seems that rooted paths in symbolic links are relative to
......@@ -849,10 +852,7 @@ pub fn CreateSymbolicLink(
849852 // Keep relative paths relative, but anything else needs to get NT-prefixed.
850853 else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
851854 break :target_path target_path,
852 },
853 // Already an NT path, no need to do anything to it
854 .nt => break :target_path target_path,
855 else => {},
855 }
856856 }
857857 var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
858858 // We do this after prefixing to ensure that drive-relative paths are treated as absolute
......@@ -2308,271 +2308,338 @@ pub const Wtf16ToPrefixedFileWError = error{
23082308/// - . and space are not stripped from the end of relative paths (potential TODO)
23092309pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {
23102310 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
2311 switch (getNamespacePrefix(u16, path)) {
2312 // TODO: Figure out a way to design an API that can avoid the copy for .nt,
2311 if (hasCommonNtPrefix(u16, path)) {
2312 // TODO: Figure out a way to design an API that can avoid the copy for NT,
23132313 // since it is always returned fully unmodified.
2314 .nt, .verbatim => {
2315 var path_space: PathSpace = undefined;
2316 path_space.data[0..nt_prefix.len].* = nt_prefix;
2317 const len_after_prefix = path.len - nt_prefix.len;
2318 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2319 path_space.len = path.len;
2320 path_space.data[path_space.len] = 0;
2321 return path_space;
2322 },
2323 .local_device, .fake_verbatim => {
2324 var path_space: PathSpace = undefined;
2325 const path_byte_len = ntdll.RtlGetFullPathName_U(
2326 path.ptr,
2327 path_space.data.len * 2,
2328 &path_space.data,
2329 null,
2330 );
2331 if (path_byte_len == 0) {
2332 // TODO: This may not be the right error
2333 return error.BadPathName;
2334 } else if (path_byte_len / 2 > path_space.data.len) {
2335 return error.NameTooLong;
2336 }
2337 path_space.len = path_byte_len / 2;
2338 // Both prefixes will be normalized but retained, so all
2339 // we need to do now is replace them with the NT prefix
2340 path_space.data[0..nt_prefix.len].* = nt_prefix;
2341 return path_space;
2342 },
2343 .none => {
2344 const path_type = getUnprefixedPathType(u16, path);
2345 var path_space: PathSpace = undefined;
2346 relative: {
2347 if (path_type == .relative) {
2348 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
2349 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
2350
2351 // TODO: Potentially strip all trailing . and space characters from the
2352 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
2353 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
2354 // are allowed, but such paths may not interact well with Windows (i.e.
2355 // files with these paths can't be deleted from explorer.exe, etc).
2356 // This could be something that normalizePath may want to do.
2357
2358 @memcpy(path_space.data[0..path.len], path);
2359 // Try to normalize, but if we get too many parent directories,
2360 // then we need to start over and use RtlGetFullPathName_U instead.
2361 path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
2362 error.TooManyParentDirs => break :relative,
2363 };
2314 var path_space: PathSpace = undefined;
2315 path_space.data[0..nt_prefix.len].* = nt_prefix;
2316 const len_after_prefix = path.len - nt_prefix.len;
2317 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2318 path_space.len = path.len;
2319 path_space.data[path_space.len] = 0;
2320 return path_space;
2321 } else {
2322 const path_type = getWin32PathType(u16, path);
2323 var path_space: PathSpace = undefined;
2324 if (path_type == .local_device) {
2325 switch (getLocalDevicePathType(u16, path)) {
2326 .verbatim => {
2327 path_space.data[0..nt_prefix.len].* = nt_prefix;
2328 const len_after_prefix = path.len - nt_prefix.len;
2329 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2330 path_space.len = path.len;
23642331 path_space.data[path_space.len] = 0;
23652332 return path_space;
2366 }
2333 },
2334 .local_device, .fake_verbatim => {
2335 const path_byte_len = ntdll.RtlGetFullPathName_U(
2336 path.ptr,
2337 path_space.data.len * 2,
2338 &path_space.data,
2339 null,
2340 );
2341 if (path_byte_len == 0) {
2342 // TODO: This may not be the right error
2343 return error.BadPathName;
2344 } else if (path_byte_len / 2 > path_space.data.len) {
2345 return error.NameTooLong;
2346 }
2347 path_space.len = path_byte_len / 2;
2348 // Both prefixes will be normalized but retained, so all
2349 // we need to do now is replace them with the NT prefix
2350 path_space.data[0..nt_prefix.len].* = nt_prefix;
2351 return path_space;
2352 },
23672353 }
2368 // We now know we are going to return an absolute NT path, so
2369 // we can unconditionally prefix it with the NT prefix.
2370 path_space.data[0..nt_prefix.len].* = nt_prefix;
2371 if (path_type == .root_local_device) {
2372 // `\\.` and `\\?` always get converted to `\??\` exactly, so
2373 // we can just stop here
2374 path_space.len = nt_prefix.len;
2354 }
2355 relative: {
2356 if (path_type == .relative) {
2357 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
2358 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
2359
2360 // TODO: Potentially strip all trailing . and space characters from the
2361 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
2362 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
2363 // are allowed, but such paths may not interact well with Windows (i.e.
2364 // files with these paths can't be deleted from explorer.exe, etc).
2365 // This could be something that normalizePath may want to do.
2366
2367 @memcpy(path_space.data[0..path.len], path);
2368 // Try to normalize, but if we get too many parent directories,
2369 // then we need to start over and use RtlGetFullPathName_U instead.
2370 path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
2371 error.TooManyParentDirs => break :relative,
2372 };
23752373 path_space.data[path_space.len] = 0;
23762374 return path_space;
23772375 }
2378 const path_buf_offset = switch (path_type) {
2379 // UNC paths will always start with `\\`. However, we want to
2380 // end up with something like `\??\UNC\server\share`, so to get
2381 // RtlGetFullPathName to write into the spot we want the `server`
2382 // part to end up, we need to provide an offset such that
2383 // the `\\` part gets written where the `C\` of `UNC\` will be
2384 // in the final NT path.
2385 .unc_absolute => nt_prefix.len + 2,
2386 else => nt_prefix.len,
2387 };
2388 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
2389 const path_to_get: [:0]const u16 = path_to_get: {
2390 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
2391 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
2392 if (path_type != .relative or dir == null) {
2393 break :path_to_get path;
2394 }
2395 // We can also skip GetFinalPathNameByHandle if the handle matches
2396 // the handle returned by fs.cwd()
2397 if (dir.? == std.fs.cwd().fd) {
2398 break :path_to_get path;
2399 }
2400 // At this point, we know we have a relative path that had too many
2401 // `..` components to be resolved by normalizePath, so we need to
2402 // convert it into an absolute path and let RtlGetFullPathName_U
2403 // canonicalize it. We do this by getting the path of the `dir`
2404 // and appending the relative path to it.
2405 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
2406 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
2407 // This mapping is not correct; it is actually expected
2408 // that calling GetFinalPathNameByHandle might return
2409 // error.UnrecognizedVolume, and in fact has been observed
2410 // in the wild. The problem is that wToPrefixedFileW was
2411 // never intended to make *any* OS syscall APIs. It's only
2412 // supposed to convert a string to one that is eligible to
2413 // be used in the ntdll syscalls.
2414 //
2415 // To solve this, this function needs to no longer call
2416 // GetFinalPathNameByHandle under any conditions, or the
2417 // calling function needs to get reworked to not need to
2418 // call this function.
2419 //
2420 // This may involve making breaking API changes.
2421 error.UnrecognizedVolume => return error.Unexpected,
2422 else => |e| return e,
2423 };
2424 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
2425 return error.NameTooLong;
2426 }
2427 // We don't have to worry about potentially doubling up path separators
2428 // here since RtlGetFullPathName_U will handle canonicalizing it.
2429 dir_path_buf[dir_path.len] = '\\';
2430 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
2431 const full_len = dir_path.len + 1 + path.len;
2432 dir_path_buf[full_len] = 0;
2433 break :path_to_get dir_path_buf[0..full_len :0];
2376 }
2377 // We now know we are going to return an absolute NT path, so
2378 // we can unconditionally prefix it with the NT prefix.
2379 path_space.data[0..nt_prefix.len].* = nt_prefix;
2380 if (path_type == .root_local_device) {
2381 // `\\.` and `\\?` always get converted to `\??\` exactly, so
2382 // we can just stop here
2383 path_space.len = nt_prefix.len;
2384 path_space.data[path_space.len] = 0;
2385 return path_space;
2386 }
2387 const path_buf_offset = switch (path_type) {
2388 // UNC paths will always start with `\\`. However, we want to
2389 // end up with something like `\??\UNC\server\share`, so to get
2390 // RtlGetFullPathName to write into the spot we want the `server`
2391 // part to end up, we need to provide an offset such that
2392 // the `\\` part gets written where the `C\` of `UNC\` will be
2393 // in the final NT path.
2394 .unc_absolute => nt_prefix.len + 2,
2395 else => nt_prefix.len,
2396 };
2397 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
2398 const path_to_get: [:0]const u16 = path_to_get: {
2399 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
2400 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
2401 if (path_type != .relative or dir == null) {
2402 break :path_to_get path;
2403 }
2404 // We can also skip GetFinalPathNameByHandle if the handle matches
2405 // the handle returned by fs.cwd()
2406 if (dir.? == std.fs.cwd().fd) {
2407 break :path_to_get path;
2408 }
2409 // At this point, we know we have a relative path that had too many
2410 // `..` components to be resolved by normalizePath, so we need to
2411 // convert it into an absolute path and let RtlGetFullPathName_U
2412 // canonicalize it. We do this by getting the path of the `dir`
2413 // and appending the relative path to it.
2414 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
2415 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
2416 // This mapping is not correct; it is actually expected
2417 // that calling GetFinalPathNameByHandle might return
2418 // error.UnrecognizedVolume, and in fact has been observed
2419 // in the wild. The problem is that wToPrefixedFileW was
2420 // never intended to make *any* OS syscall APIs. It's only
2421 // supposed to convert a string to one that is eligible to
2422 // be used in the ntdll syscalls.
2423 //
2424 // To solve this, this function needs to no longer call
2425 // GetFinalPathNameByHandle under any conditions, or the
2426 // calling function needs to get reworked to not need to
2427 // call this function.
2428 //
2429 // This may involve making breaking API changes.
2430 error.UnrecognizedVolume => return error.Unexpected,
2431 else => |e| return e,
24342432 };
2435 const path_byte_len = ntdll.RtlGetFullPathName_U(
2436 path_to_get.ptr,
2437 buf_len * 2,
2438 path_space.data[path_buf_offset..].ptr,
2439 null,
2440 );
2441 if (path_byte_len == 0) {
2442 // TODO: This may not be the right error
2443 return error.BadPathName;
2444 } else if (path_byte_len / 2 > buf_len) {
2433 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
24452434 return error.NameTooLong;
24462435 }
2447 path_space.len = path_buf_offset + (path_byte_len / 2);
2448 if (path_type == .unc_absolute) {
2449 // Now add in the UNC, the `C` should overwrite the first `\` of the
2450 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
2451 std.debug.assert(path_space.data[path_buf_offset] == '\\');
2452 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');
2453 const unc = [_]u16{ 'U', 'N', 'C' };
2454 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
2455 }
2456 return path_space;
2457 },
2458 }
2459}
2460
2461pub const NamespacePrefix = enum {
2462 none,
2463 /// `\\.\` (path separators can be `\` or `/`)
2464 local_device,
2465 /// `\\?\`
2466 /// When converted to an NT path, everything past the prefix is left
2467 /// untouched and `\\?\` is replaced by `\??\`.
2468 verbatim,
2469 /// `\\?\` without all path separators being `\`.
2470 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
2471 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
2472 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
2473 /// be treated as part of the final path])
2474 fake_verbatim,
2475 /// `\??\`
2476 nt,
2477};
2478
2479/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
2480pub fn getNamespacePrefix(comptime T: type, path: []const T) NamespacePrefix {
2481 if (path.len < 4) return .none;
2482 var all_backslash = switch (mem.littleToNative(T, path[0])) {
2483 '\\' => true,
2484 '/' => false,
2485 else => return .none,
2486 };
2487 all_backslash = all_backslash and switch (mem.littleToNative(T, path[3])) {
2488 '\\' => true,
2489 '/' => false,
2490 else => return .none,
2491 };
2492 switch (mem.littleToNative(T, path[1])) {
2493 '?' => if (mem.littleToNative(T, path[2]) == '?' and all_backslash) return .nt else return .none,
2494 '\\' => {},
2495 '/' => all_backslash = false,
2496 else => return .none,
2436 // We don't have to worry about potentially doubling up path separators
2437 // here since RtlGetFullPathName_U will handle canonicalizing it.
2438 dir_path_buf[dir_path.len] = '\\';
2439 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
2440 const full_len = dir_path.len + 1 + path.len;
2441 dir_path_buf[full_len] = 0;
2442 break :path_to_get dir_path_buf[0..full_len :0];
2443 };
2444 const path_byte_len = ntdll.RtlGetFullPathName_U(
2445 path_to_get.ptr,
2446 buf_len * 2,
2447 path_space.data[path_buf_offset..].ptr,
2448 null,
2449 );
2450 if (path_byte_len == 0) {
2451 // TODO: This may not be the right error
2452 return error.BadPathName;
2453 } else if (path_byte_len / 2 > buf_len) {
2454 return error.NameTooLong;
2455 }
2456 path_space.len = path_buf_offset + (path_byte_len / 2);
2457 if (path_type == .unc_absolute) {
2458 // Now add in the UNC, the `C` should overwrite the first `\` of the
2459 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
2460 std.debug.assert(path_space.data[path_buf_offset] == '\\');
2461 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');
2462 const unc = [_]u16{ 'U', 'N', 'C' };
2463 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
2464 }
2465 return path_space;
24972466 }
2498 return switch (mem.littleToNative(T, path[2])) {
2499 '?' => if (all_backslash) .verbatim else .fake_verbatim,
2500 '.' => .local_device,
2501 else => .none,
2502 };
2503}
2504
2505test getNamespacePrefix {
2506 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, ""));
2507 try std.testing.expectEqual(NamespacePrefix.nt, getNamespacePrefix(u8, "\\??\\"));
2508 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??/"));
2509 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/??\\"));
2510 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "\\?\\\\"));
2511 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\.\\"));
2512 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "\\\\./"));
2513 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "/\\./"));
2514 try std.testing.expectEqual(NamespacePrefix.local_device, getNamespacePrefix(u8, "//./"));
2515 try std.testing.expectEqual(NamespacePrefix.none, getNamespacePrefix(u8, "/.//"));
2516 try std.testing.expectEqual(NamespacePrefix.verbatim, getNamespacePrefix(u8, "\\\\?\\"));
2517 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?\\"));
2518 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "\\/?/"));
2519 try std.testing.expectEqual(NamespacePrefix.fake_verbatim, getNamespacePrefix(u8, "//?/"));
25202467}
25212468
2522pub const UnprefixedPathType = enum {
2469/// Similar to `RTL_PATH_TYPE`, but without the `UNKNOWN` path type.
2470pub const Win32PathType = enum {
2471 /// `\\server\share\foo`
25232472 unc_absolute,
2473 /// `C:\foo`
25242474 drive_absolute,
2475 /// `C:foo`
25252476 drive_relative,
2477 /// `\foo`
25262478 rooted,
2479 /// `foo`
25272480 relative,
2481 /// `\\.\foo`, `\\?\foo`
2482 local_device,
2483 /// `\\.`, `\\?`
25282484 root_local_device,
25292485};
25302486
2531/// Get the path type of a path that is known to not have any namespace prefixes
2532/// (`\\?\`, `\\.\`, `\??\`).
2487/// Get the path type of a Win32 namespace path.
2488/// Similar to `RtlDetermineDosPathNameType_U`.
25332489/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
2534pub fn getUnprefixedPathType(comptime T: type, path: []const T) UnprefixedPathType {
2490pub fn getWin32PathType(comptime T: type, path: []const T) Win32PathType {
25352491 if (path.len < 1) return .relative;
25362492
2537 if (std.debug.runtime_safety) {
2538 std.debug.assert(getNamespacePrefix(T, path) == .none);
2539 }
2540
25412493 const windows_path = std.fs.path.PathType.windows;
25422494 if (windows_path.isSep(T, mem.littleToNative(T, path[0]))) {
25432495 // \x
25442496 if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted;
2545 // exactly \\. or \\? with nothing trailing
2546 if (path.len == 3 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) return .root_local_device;
2497 // \\. or \\?
2498 if (path.len > 2 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) {
2499 // exactly \\. or \\? with nothing trailing
2500 if (path.len == 3) return .root_local_device;
2501 // \\.\x or \\?\x
2502 if (windows_path.isSep(T, mem.littleToNative(T, path[3]))) return .local_device;
2503 }
25472504 // \\x
25482505 return .unc_absolute;
25492506 } else {
2507 // Some choice has to be made about how non-ASCII code points as drive-letters are handled, since
2508 // path[0] is a different size for WTF-16 vs WTF-8, leading to a potential mismatch in classification
2509 // for a WTF-8 path and its WTF-16 equivalent. For example, `€:\` encoded in WTF-16 is three code
2510 // units `<0x20AC>:\` whereas `€:\` encoded as WTF-8 is 6 code units `<0xE2><0x82><0xAC>:\` so
2511 // checking path[0], path[1] and path[2] would not behave the same between WTF-8/WTF-16.
2512 //
2513 // `RtlDetermineDosPathNameType_U` exclusively deals with WTF-16 and considers
2514 // `€:\` a drive-absolute path, but code points that take two WTF-16 code units to encode get
2515 // classified as a relative path (e.g. with U+20000 as the drive-letter that'd be encoded
2516 // in WTF-16 as `<0xD840><0xDC00>:\` and be considered a relative path).
2517 //
2518 // The choice made here is to emulate the behavior of `RtlDetermineDosPathNameType_U` for both
2519 // WTF-16 and WTF-8. This is because, while unlikely and not supported by the Disk Manager GUI,
2520 // drive letters are not actually restricted to A-Z. Using `SetVolumeMountPointW` will allow you
2521 // to set any byte value as a drive letter, and going through `IOCTL_MOUNTMGR_CREATE_POINT` will
2522 // allow you to set any WTF-16 code unit as a drive letter.
2523 //
2524 // Non-A-Z drive letters don't interact well with most of Windows, but certain things do work, e.g.
2525 // `cd /D €:\` will work, filesystem functions still work, etc.
2526 //
2527 // The unfortunate part of this is that this makes handling WTF-8 more complicated as we can't
2528 // just check path[0], path[1], path[2].
2529 const colon_i: usize = switch (T) {
2530 u8 => i: {
2531 const code_point_len = std.unicode.utf8ByteSequenceLength(path[0]) catch return .relative;
2532 // Conveniently, 4-byte sequences in WTF-8 have the same starting code point
2533 // as 2-code-unit sequences in WTF-16.
2534 if (code_point_len > 3) return .relative;
2535 break :i code_point_len;
2536 },
2537 u16 => 1,
2538 else => @compileError("unsupported type: " ++ @typeName(T)),
2539 };
25502540 // x
2551 if (path.len < 2 or mem.littleToNative(T, path[1]) != ':') return .relative;
2541 if (path.len < colon_i + 1 or mem.littleToNative(T, path[colon_i]) != ':') return .relative;
25522542 // x:\
2553 if (path.len > 2 and windows_path.isSep(T, mem.littleToNative(T, path[2]))) return .drive_absolute;
2543 if (path.len > colon_i + 1 and windows_path.isSep(T, mem.littleToNative(T, path[colon_i + 1]))) return .drive_absolute;
25542544 // x:
25552545 return .drive_relative;
25562546 }
25572547}
25582548
2559test getUnprefixedPathType {
2560 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, ""));
2561 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x"));
2562 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x\\"));
2563 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "//."));
2564 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "/\\?"));
2565 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "\\\\?"));
2566 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "\\\\x"));
2567 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "//x"));
2568 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "\\x"));
2569 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "/"));
2570 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:"));
2571 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:abc"));
2572 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:a/b/c"));
2573 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\"));
2574 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\abc"));
2575 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
2549test getWin32PathType {
2550 try std.testing.expectEqual(.relative, getWin32PathType(u8, ""));
2551 try std.testing.expectEqual(.relative, getWin32PathType(u8, "x"));
2552 try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\"));
2553
2554 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//."));
2555 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?"));
2556 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?"));
2557
2558 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x"));
2559 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x"));
2560 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x"));
2561 // local device paths require a path separator after the root, otherwise it is considered a UNC path
2562 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x"));
2563 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x"));
2564
2565 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//"));
2566 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\x"));
2567 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//x"));
2568
2569 try std.testing.expectEqual(.rooted, getWin32PathType(u8, "\\x"));
2570 try std.testing.expectEqual(.rooted, getWin32PathType(u8, "/"));
2571
2572 try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:"));
2573 try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:abc"));
2574 try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "x:a/b/c"));
2575
2576 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\"));
2577 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:\\abc"));
2578 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "x:/a/b/c"));
2579
2580 // Non-ASCII code point that is encoded as one WTF-16 code unit is considered a valid drive letter
2581 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u8, "€:\\"));
2582 try std.testing.expectEqual(.drive_absolute, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:\\")));
2583 try std.testing.expectEqual(.drive_relative, getWin32PathType(u8, "€:"));
2584 try std.testing.expectEqual(.drive_relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("€:")));
2585 // But code points that are encoded as two WTF-16 code units are not
2586 try std.testing.expectEqual(.relative, getWin32PathType(u8, "\u{10000}:\\"));
2587 try std.testing.expectEqual(.relative, getWin32PathType(u16, std.unicode.wtf8ToWtf16LeStringLiteral("\u{10000}:\\")));
2588}
2589
2590/// Returns true if the path starts with `\??\`, which is indicative of an NT path
2591/// but is not enough to fully distinguish between NT paths and Win32 paths, as
2592/// `\??\` is not actually a distinct prefix but rather the path to a special virtual
2593/// folder in the Object Manager.
2594///
2595/// For example, `\Device\HarddiskVolume2` and `\DosDevices\C:` are also NT paths but
2596/// cannot be distinguished as such by their prefix.
2597///
2598/// So, inferring whether a path is an NT path or a Win32 path is usually a mistake;
2599/// that information should instead be known ahead-of-time.
2600///
2601/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.
2602pub fn hasCommonNtPrefix(comptime T: type, path: []const T) bool {
2603 // Must be exactly \??\, forward slashes are not allowed
2604 const expected_wtf8_prefix = "\\??\\";
2605 const expected_prefix = switch (T) {
2606 u8 => expected_wtf8_prefix,
2607 u16 => std.unicode.wtf8ToWtf16LeStringLiteral(expected_wtf8_prefix),
2608 else => @compileError("unsupported type: " ++ @typeName(T)),
2609 };
2610 return mem.startsWith(T, path, expected_prefix);
2611}
2612
2613const LocalDevicePathType = enum {
2614 /// `\\.\` (path separators can be `\` or `/`)
2615 local_device,
2616 /// `\\?\`
2617 /// When converted to an NT path, everything past the prefix is left
2618 /// untouched and `\\?\` is replaced by `\??\`.
2619 verbatim,
2620 /// `\\?\` without all path separators being `\`.
2621 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
2622 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
2623 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
2624 /// be treated as part of the final path])
2625 fake_verbatim,
2626};
2627
2628/// Only relevant for Win32 -> NT path conversion.
2629/// Asserts `path` is of type `Win32PathType.local_device`.
2630fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
2631 if (std.debug.runtime_safety) {
2632 std.debug.assert(getWin32PathType(T, path) == .local_device);
2633 }
2634
2635 const all_backslash = mem.littleToNative(T, path[0]) == '\\' and
2636 mem.littleToNative(T, path[1]) == '\\' and
2637 mem.littleToNative(T, path[3]) == '\\';
2638 return switch (mem.littleToNative(T, path[2])) {
2639 '?' => if (all_backslash) .verbatim else .fake_verbatim,
2640 '.' => .local_device,
2641 else => unreachable,
2642 };
25762643}
25772644
25782645/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
......@@ -2589,30 +2656,25 @@ test getUnprefixedPathType {
25892656/// Supports in-place modification (`path` and `out` may refer to the same slice).
25902657pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {
25912658 if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
2592
2593 const namespace_prefix = getNamespacePrefix(u16, path);
2594 switch (namespace_prefix) {
2595 .nt => {
2596 var dest_index: usize = 0;
2597 var after_prefix = path[4..]; // after the `\??\`
2598 // The prefix \??\UNC\ means this is a UNC path, in which case the
2599 // `\??\UNC\` should be replaced by `\\` (two backslashes)
2600 const is_unc = after_prefix.len >= 4 and
2601 eqlIgnoreCaseWTF16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
2602 std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));
2603 const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
2604 if (out.len < win32_len) return error.NameTooLong;
2605 if (is_unc) {
2606 out[0] = comptime std.mem.nativeToLittle(u16, '\\');
2607 dest_index += 1;
2608 // We want to include the last `\` of `\??\UNC\`
2609 after_prefix = path[7..];
2610 }
2611 @memmove(out[dest_index..][0..after_prefix.len], after_prefix);
2612 return out[0..win32_len];
2613 },
2614 else => return error.NotNtPath,
2659 if (!hasCommonNtPrefix(u16, path)) return error.NotNtPath;
2660
2661 var dest_index: usize = 0;
2662 var after_prefix = path[4..]; // after the `\??\`
2663 // The prefix \??\UNC\ means this is a UNC path, in which case the
2664 // `\??\UNC\` should be replaced by `\\` (two backslashes)
2665 const is_unc = after_prefix.len >= 4 and
2666 eqlIgnoreCaseWtf16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
2667 std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));
2668 const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
2669 if (out.len < win32_len) return error.NameTooLong;
2670 if (is_unc) {
2671 out[0] = comptime std.mem.nativeToLittle(u16, '\\');
2672 dest_index += 1;
2673 // We want to include the last `\` of `\??\UNC\`
2674 after_prefix = path[7..];
26152675 }
2676 @memmove(out[dest_index..][0..after_prefix.len], after_prefix);
2677 return out[0..win32_len];
26162678}
26172679
26182680test ntToWin32Namespace {
lib/std/os/windows/test.zig+102-2
......@@ -54,8 +54,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
5454}
5555
5656test "toPrefixedFileW" {
57 if (builtin.os.tag != .windows)
58 return;
57 if (builtin.os.tag != .windows) return error.SkipZigTest;
5958
6059 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
6160 // Note that these tests do not actually touch the filesystem or care about whether or not
......@@ -237,3 +236,104 @@ test "removeDotDirs" {
237236 try testRemoveDotDirs("a\\b\\..\\", "a\\");
238237 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
239238}
239
240const RTL_PATH_TYPE = enum(c_int) {
241 Unknown,
242 UncAbsolute,
243 DriveAbsolute,
244 DriveRelative,
245 Rooted,
246 Relative,
247 LocalDevice,
248 RootLocalDevice,
249};
250
251pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
252 Path: [*:0]const u16,
253) callconv(.winapi) RTL_PATH_TYPE;
254
255test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
256 if (builtin.os.tag != .windows) return error.SkipZigTest;
257
258 var buf: std.ArrayList(u16) = .empty;
259 defer buf.deinit(std.testing.allocator);
260
261 var wtf8_buf: std.ArrayList(u8) = .empty;
262 defer wtf8_buf.deinit(std.testing.allocator);
263
264 var random = std.Random.DefaultPrng.init(std.testing.random_seed);
265 const rand = random.random();
266
267 for (0..1000) |_| {
268 buf.clearRetainingCapacity();
269 const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
270 wtf8_buf.clearRetainingCapacity();
271 const wtf8_len = std.unicode.calcWtf8Len(path);
272 try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
273 wtf8_buf.items.len = wtf8_len;
274 std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
275
276 const windows_type = RtlDetermineDosPathNameType_U(path);
277 const wtf16_type = windows.getWin32PathType(u16, path);
278 const wtf8_type = windows.getWin32PathType(u8, wtf8_buf.items);
279
280 checkPathType(windows_type, wtf16_type) catch |err| {
281 std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
282 std.debug.print("path bytes:\n", .{});
283 std.debug.dumpHex(std.mem.sliceAsBytes(path));
284 return err;
285 };
286
287 if (wtf16_type != wtf8_type) {
288 std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
289 std.debug.print("wtf-16 path bytes:\n", .{});
290 std.debug.dumpHex(std.mem.sliceAsBytes(path));
291 std.debug.print("wtf-8 path bytes:\n", .{});
292 std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
293 return error.Wtf8Wtf16Mismatch;
294 }
295 }
296}
297
298fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: windows.Win32PathType) !void {
299 const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
300 .unc_absolute => .UncAbsolute,
301 .drive_absolute => .DriveAbsolute,
302 .drive_relative => .DriveRelative,
303 .rooted => .Rooted,
304 .relative => .Relative,
305 .local_device => .LocalDevice,
306 .root_local_device => .RootLocalDevice,
307 };
308 if (windows_type != expected_windows_type) return error.PathTypeMismatch;
309}
310
311fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
312 const Choice = enum {
313 backslash,
314 slash,
315 control,
316 printable,
317 non_ascii,
318 };
319
320 const choices = rand.uintAtMostBiased(u16, 32);
321
322 for (0..choices) |_| {
323 const choice = rand.enumValue(Choice);
324 const code_unit = switch (choice) {
325 .backslash => '\\',
326 .slash => '/',
327 .control => switch (rand.uintAtMostBiased(u8, 0x20)) {
328 0x20 => '\x7F',
329 else => |b| b + 1, // no NUL
330 },
331 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
332 .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
333 };
334 try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
335 }
336
337 try buf.append(allocator, 0);
338 return buf.items[0 .. buf.items.len - 1 :0];
339}
lib/std/zig/WindowsSdk.zig+1-1
......@@ -643,7 +643,7 @@ const MsvcLibDir = struct {
643643
644644 if (!std.fs.path.isAbsolute(dll_path)) return error.PathNotFound;
645645
646 var path_it = std.fs.path.componentIterator(dll_path) catch return error.PathNotFound;
646 var path_it = std.fs.path.componentIterator(dll_path);
647647 // the .dll filename
648648 _ = path_it.last();
649649 const root_path = while (path_it.previous()) |dir_component| {
src/main.zig+1-1
......@@ -3883,7 +3883,7 @@ fn createModule(
38833883 if (create_module.sysroot) |root| {
38843884 for (create_module.lib_dir_args.items) |lib_dir_arg| {
38853885 if (fs.path.isAbsolute(lib_dir_arg)) {
3886 const stripped_dir = lib_dir_arg[fs.path.diskDesignator(lib_dir_arg).len..];
3886 const stripped_dir = lib_dir_arg[fs.path.parsePath(lib_dir_arg).root.len..];
38873887 const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir });
38883888 addLibDirectoryWarn(&create_module.lib_directories, full_path);
38893889 } else {
test/standalone/build.zig.zon+3
......@@ -126,6 +126,9 @@
126126 .windows_bat_args = .{
127127 .path = "windows_bat_args",
128128 },
129 .windows_paths = .{
130 .path = "windows_paths",
131 },
129132 .self_exe_symlink = .{
130133 .path = "self_exe_symlink",
131134 },
test/standalone/windows_paths/build.zig created+37
......@@ -0,0 +1,37 @@
1const std = @import("std");
2const builtin = @import("builtin");
3
4pub fn build(b: *std.Build) void {
5 const test_step = b.step("test", "Test it");
6 b.default_step = test_step;
7
8 const optimize: std.builtin.OptimizeMode = .Debug;
9 const target = b.graph.host;
10
11 if (builtin.os.tag != .windows) return;
12
13 const relative = b.addExecutable(.{
14 .name = "relative",
15 .root_module = b.createModule(.{
16 .root_source_file = b.path("relative.zig"),
17 .optimize = optimize,
18 .target = target,
19 }),
20 });
21
22 const main = b.addExecutable(.{
23 .name = "test",
24 .root_module = b.createModule(.{
25 .root_source_file = b.path("test.zig"),
26 .optimize = optimize,
27 .target = target,
28 }),
29 });
30
31 const run = b.addRunArtifact(main);
32 run.addArtifactArg(relative);
33 run.expectExitCode(0);
34 run.skip_foreign_checks = true;
35
36 test_step.dependOn(&run.step);
37}
test/standalone/windows_paths/relative.zig created+19
......@@ -0,0 +1,19 @@
1const std = @import("std");
2
3pub fn main() !void {
4 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
5 defer std.debug.assert(gpa.deinit() == .ok);
6 const allocator = gpa.allocator();
7
8 const args = try std.process.argsAlloc(allocator);
9 defer std.process.argsFree(allocator, args);
10
11 if (args.len < 3) return error.MissingArgs;
12
13 const relative = try std.fs.path.relative(allocator, args[1], args[2]);
14 defer allocator.free(relative);
15
16 var stdout_writer = std.fs.File.stdout().writerStreaming(&.{});
17 const stdout = &stdout_writer.interface;
18 try stdout.writeAll(relative);
19}
test/standalone/windows_paths/test.zig created+131
......@@ -0,0 +1,131 @@
1const std = @import("std");
2
3pub fn main() anyerror!void {
4 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
5 defer arena_state.deinit();
6 const arena = arena_state.allocator();
7
8 const args = try std.process.argsAlloc(arena);
9
10 if (args.len < 2) return error.MissingArgs;
11
12 const exe_path = args[1];
13
14 const cwd_path = try std.process.getCwdAlloc(arena);
15 const parsed_cwd_path = std.fs.path.parsePathWindows(u8, cwd_path);
16
17 if (parsed_cwd_path.kind == .drive_absolute and !std.ascii.isAlphabetic(cwd_path[0])) {
18 // Technically possible, but not worth supporting here
19 return error.NonAlphabeticDriveLetter;
20 }
21
22 const alt_drive_letter = try getAltDriveLetter(cwd_path);
23 const alt_drive_cwd_key = try std.fmt.allocPrint(arena, "={c}:", .{alt_drive_letter});
24 const alt_drive_cwd = try std.fmt.allocPrint(arena, "{c}:\\baz", .{alt_drive_letter});
25 var alt_drive_env_map = std.process.EnvMap.init(arena);
26 try alt_drive_env_map.put(alt_drive_cwd_key, alt_drive_cwd);
27
28 const empty_env = std.process.EnvMap.init(arena);
29
30 {
31 const drive_rel = try std.fmt.allocPrint(arena, "{c}:foo", .{alt_drive_letter});
32 const drive_abs = try std.fmt.allocPrint(arena, "{c}:\\bar", .{alt_drive_letter});
33
34 // With the special =X: environment variable set, drive-relative paths that
35 // don't match the CWD's drive letter are resolved against that env var.
36 try checkRelative(arena, "..\\..\\bar", &.{ exe_path, drive_rel, drive_abs }, null, &alt_drive_env_map);
37 try checkRelative(arena, "..\\baz\\foo", &.{ exe_path, drive_abs, drive_rel }, null, &alt_drive_env_map);
38
39 // Without that environment variable set, drive-relative paths that don't match the
40 // CWD's drive letter are resolved against the root of the drive.
41 try checkRelative(arena, "..\\bar", &.{ exe_path, drive_rel, drive_abs }, null, &empty_env);
42 try checkRelative(arena, "..\\foo", &.{ exe_path, drive_abs, drive_rel }, null, &empty_env);
43
44 // Bare drive-relative path with no components
45 try checkRelative(arena, "bar", &.{ exe_path, drive_rel[0..2], drive_abs }, null, &empty_env);
46 try checkRelative(arena, "..", &.{ exe_path, drive_abs, drive_rel[0..2] }, null, &empty_env);
47
48 // Bare drive-relative path with no components, drive-CWD set
49 try checkRelative(arena, "..\\bar", &.{ exe_path, drive_rel[0..2], drive_abs }, null, &alt_drive_env_map);
50 try checkRelative(arena, "..\\baz", &.{ exe_path, drive_abs, drive_rel[0..2] }, null, &alt_drive_env_map);
51
52 // Bare drive-relative path relative to the CWD should be equivalent if drive-CWD is set
53 try checkRelative(arena, "", &.{ exe_path, alt_drive_cwd, drive_rel[0..2] }, null, &alt_drive_env_map);
54 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], alt_drive_cwd }, null, &alt_drive_env_map);
55
56 // Bare drive-relative should always be equivalent to itself
57 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &alt_drive_env_map);
58 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &alt_drive_env_map);
59 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &empty_env);
60 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &empty_env);
61 }
62
63 if (parsed_cwd_path.kind == .unc_absolute) {
64 const drive_abs_path = try std.fmt.allocPrint(arena, "{c}:\\foo\\bar", .{alt_drive_letter});
65
66 {
67 try checkRelative(arena, drive_abs_path, &.{ exe_path, cwd_path, drive_abs_path }, null, &empty_env);
68 try checkRelative(arena, cwd_path, &.{ exe_path, drive_abs_path, cwd_path }, null, &empty_env);
69 }
70 } else if (parsed_cwd_path.kind == .drive_absolute) {
71 const cur_drive_letter = parsed_cwd_path.root[0];
72 const path_beyond_root = cwd_path[3..];
73 const unc_cwd = try std.fmt.allocPrint(arena, "\\\\127.0.0.1\\{c}$\\{s}", .{ cur_drive_letter, path_beyond_root });
74
75 {
76 try checkRelative(arena, cwd_path, &.{ exe_path, unc_cwd, cwd_path }, null, &empty_env);
77 try checkRelative(arena, unc_cwd, &.{ exe_path, cwd_path, unc_cwd }, null, &empty_env);
78 }
79 {
80 const drive_abs = cwd_path;
81 const drive_rel = parsed_cwd_path.root[0..2];
82 try checkRelative(arena, "", &.{ exe_path, drive_abs, drive_rel }, null, &empty_env);
83 try checkRelative(arena, "", &.{ exe_path, drive_rel, drive_abs }, null, &empty_env);
84 }
85 } else {
86 return error.UnexpectedPathType;
87 }
88}
89
90fn checkRelative(
91 allocator: std.mem.Allocator,
92 expected_stdout: []const u8,
93 argv: []const []const u8,
94 cwd: ?[]const u8,
95 env_map: ?*const std.process.EnvMap,
96) !void {
97 const result = try std.process.Child.run(.{
98 .allocator = allocator,
99 .argv = argv,
100 .cwd = cwd,
101 .env_map = env_map,
102 });
103 defer allocator.free(result.stdout);
104 defer allocator.free(result.stderr);
105
106 try std.testing.expectEqualStrings("", result.stderr);
107 try std.testing.expectEqualStrings(expected_stdout, result.stdout);
108}
109
110fn getAltDriveLetter(path: []const u8) !u8 {
111 const parsed = std.fs.path.parsePathWindows(u8, path);
112 return switch (parsed.kind) {
113 .drive_absolute => {
114 const cur_drive_letter = parsed.root[0];
115 const next_drive_letter_index = (std.ascii.toUpper(cur_drive_letter) - 'A' + 1) % 26;
116 const next_drive_letter = next_drive_letter_index + 'A';
117 return next_drive_letter;
118 },
119 .unc_absolute => {
120 return 'C';
121 },
122 else => return error.UnexpectedPathType,
123 };
124}
125
126test getAltDriveLetter {
127 try std.testing.expectEqual('D', try getAltDriveLetter("C:\\"));
128 try std.testing.expectEqual('B', try getAltDriveLetter("a:\\"));
129 try std.testing.expectEqual('A', try getAltDriveLetter("Z:\\"));
130 try std.testing.expectEqual('C', try getAltDriveLetter("\\\\foo\\bar"));
131}