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 {...@@ -2914,7 +2914,7 @@ fn validateSearchPath(path: []const u8) error{BadPathName}!void {
2914 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).2914 // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
2915 // Those path types are something of an unavoidable way to2915 // Those path types are something of an unavoidable way to
2916 // still hit unreachable during the openDir call.2916 // still hit unreachable during the openDir call.
2917 var component_iterator = try std.fs.path.componentIterator(path);2917 var component_iterator = std.fs.path.componentIterator(path);
2918 while (component_iterator.next()) |component| {2918 while (component_iterator.next()) |component| {
2919 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file2919 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
2920 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;2920 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;
lib/std/Build/Cache.zig+1-3
...@@ -104,9 +104,7 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath {...@@ -104,9 +104,7 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath {
104fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {104fn getPrefixSubpath(allocator: Allocator, prefix: []const u8, path: []u8) ![]u8 {
105 const relative = try fs.path.relative(allocator, prefix, path);105 const relative = try fs.path.relative(allocator, prefix, path);
106 errdefer allocator.free(relative);106 errdefer allocator.free(relative);
107 var component_iterator = fs.path.NativeComponentIterator.init(relative) catch {107 var component_iterator = fs.path.NativeComponentIterator.init(relative);
108 return error.NotASubPath;
109 };
110 if (component_iterator.root() != null) {108 if (component_iterator.root() != null) {
111 return error.NotASubPath;109 return error.NotASubPath;
112 }110 }
lib/std/Build/Watch/FsEvents.zig+1-1
...@@ -167,7 +167,7 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)...@@ -167,7 +167,7 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)
167 }.lessThan);167 }.lessThan);
168 need_dirs.clearRetainingCapacity();168 need_dirs.clearRetainingCapacity();
169 for (old_dirs) |dir_path| {169 for (old_dirs) |dir_path| {
170 var it: std.fs.path.ComponentIterator(.posix, u8) = try .init(dir_path);170 var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path);
171 while (it.next()) |component| {171 while (it.next()) |component| {
172 if (need_dirs.contains(component.path)) {172 if (need_dirs.contains(component.path)) {
173 // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added173 // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added
lib/std/Io/Dir.zig+1-1
...@@ -318,7 +318,7 @@ pub const MakePathStatus = enum { existed, created };...@@ -318,7 +318,7 @@ pub const MakePathStatus = enum { existed, created };
318/// Same as `makePath` except returns whether the path already existed or was318/// Same as `makePath` except returns whether the path already existed or was
319/// successfully created.319/// successfully created.
320pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus {320pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus {
321 var it = try std.fs.path.componentIterator(sub_path);321 var it = std.fs.path.componentIterator(sub_path);
322 var status: MakePathStatus = .existed;322 var status: MakePathStatus = .existed;
323 var component = it.last() orelse return error.BadPathName;323 var component = it.last() orelse return error.BadPathName;
324 while (true) {324 while (true) {
lib/std/Io/Threaded.zig+1-1
...@@ -1154,7 +1154,7 @@ fn dirMakeOpenPathWindows(...@@ -1154,7 +1154,7 @@ fn dirMakeOpenPathWindows(
1154 w.SYNCHRONIZE | w.FILE_TRAVERSE |1154 w.SYNCHRONIZE | w.FILE_TRAVERSE |
1155 (if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0));1155 (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);
1158 // If there are no components in the path, then create a dummy component with the full path.1158 // If there are no components in the path, then create a dummy component with the full path.
1159 var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{1159 var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{
1160 .name = "",1160 .name = "",
lib/std/fs/path.zig+1062-488
...@@ -20,10 +20,7 @@ const testing = std.testing;...@@ -20,10 +20,7 @@ const testing = std.testing;
20const mem = std.mem;20const mem = std.mem;
21const ascii = std.ascii;21const ascii = std.ascii;
22const Allocator = mem.Allocator;22const Allocator = mem.Allocator;
23const math = std.math;
24const windows = std.os.windows;23const windows = std.os.windows;
25const os = std.os;
26const fs = std.fs;
27const process = std.process;24const process = std.process;
28const native_os = builtin.target.os.tag;25const native_os = builtin.target.os.tag;
2926
...@@ -221,7 +218,7 @@ test join {...@@ -221,7 +218,7 @@ test join {
221 try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);218 try testJoinMaybeZWindows(&[_][]const u8{}, "", zero);
222 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);219 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);
223 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\b", "c" }, "c:\\a\\b\\c", zero);220 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
226 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero);223 try testJoinMaybeZWindows(&[_][]const u8{ "c:\\", "a", "b\\", "c" }, "c:\\a\\b\\c", zero);
227 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);
...@@ -283,26 +280,16 @@ pub fn isAbsolute(path: []const u8) bool {...@@ -283,26 +280,16 @@ pub fn isAbsolute(path: []const u8) bool {
283}280}
284281
285fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {282fn isAbsoluteWindowsImpl(comptime T: type, path: []const T) bool {
286 if (path.len < 1)283 return switch (windows.getWin32PathType(T, path)) {
287 return false;284 // Unambiguously absolute
288285 .drive_absolute, .unc_absolute, .local_device, .root_local_device => true,
289 if (path[0] == '/')286 // Unambiguously relative
290 return true;287 .relative => false,
291288 // Ambiguous, more absolute than relative
292 if (path[0] == '\\')289 .rooted => true,
293 return true;290 // Ambiguous, more relative than absolute
294291 .drive_relative => false,
295 if (path.len < 3)292 };
296 return false;
297
298 if (path[1] == ':') {
299 if (path[2] == '/')
300 return true;
301 if (path[2] == '\\')
302 return true;
303 }
304
305 return false;
306}293}
307294
308pub fn isAbsoluteWindows(path: []const u8) bool {295pub fn isAbsoluteWindows(path: []const u8) bool {
...@@ -347,6 +334,9 @@ test isAbsoluteWindows {...@@ -347,6 +334,9 @@ test isAbsoluteWindows {
347 try testIsAbsoluteWindows("C:\\Users\\", true);334 try testIsAbsoluteWindows("C:\\Users\\", true);
348 try testIsAbsoluteWindows("C:cwd/another", false);335 try testIsAbsoluteWindows("C:cwd/another", false);
349 try testIsAbsoluteWindows("C:cwd\\another", false);336 try testIsAbsoluteWindows("C:cwd\\another", false);
337 try testIsAbsoluteWindows("λ:\\", true);
338 try testIsAbsoluteWindows("λ:", false);
339 try testIsAbsoluteWindows("\u{10000}:\\", false);
350 try testIsAbsoluteWindows("directory/directory", false);340 try testIsAbsoluteWindows("directory/directory", false);
351 try testIsAbsoluteWindows("directory\\directory", false);341 try testIsAbsoluteWindows("directory\\directory", false);
352 try testIsAbsoluteWindows("/usr/local", true);342 try testIsAbsoluteWindows("/usr/local", true);
...@@ -362,12 +352,17 @@ test isAbsolutePosix {...@@ -362,12 +352,17 @@ test isAbsolutePosix {
362352
363fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {353fn testIsAbsoluteWindows(path: []const u8, expected_result: bool) !void {
364 try testing.expectEqual(expected_result, isAbsoluteWindows(path));354 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));
365}359}
366360
367fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {361fn testIsAbsolutePosix(path: []const u8, expected_result: bool) !void {
368 try testing.expectEqual(expected_result, isAbsolutePosix(path));362 try testing.expectEqual(expected_result, isAbsolutePosix(path));
369}363}
370364
365/// Deprecated; see `WindowsPath2`
371pub const WindowsPath = struct {366pub const WindowsPath = struct {
372 is_abs: bool,367 is_abs: bool,
373 kind: Kind,368 kind: Kind,
...@@ -380,6 +375,7 @@ pub const WindowsPath = struct {...@@ -380,6 +375,7 @@ pub const WindowsPath = struct {
380 };375 };
381};376};
382377
378/// Deprecated; see `parsePathWindows`
383pub fn windowsParsePath(path: []const u8) WindowsPath {379pub fn windowsParsePath(path: []const u8) WindowsPath {
384 if (path.len >= 2 and path[1] == ':') {380 if (path.len >= 2 and path[1] == ':') {
385 return WindowsPath{381 return WindowsPath{
...@@ -402,26 +398,18 @@ pub fn windowsParsePath(path: []const u8) WindowsPath {...@@ -402,26 +398,18 @@ pub fn windowsParsePath(path: []const u8) WindowsPath {
402 .disk_designator = &[_]u8{},398 .disk_designator = &[_]u8{},
403 .is_abs = false,399 .is_abs = false,
404 };400 };
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, "/\\");402 if (path.len >= 2 and PathType.windows.isSep(u8, path[0]) and PathType.windows.isSep(u8, path[1])) {
417 _ = (it.next() orelse return relative_path);403 const root_end = root_end: {
418 _ = (it.next() orelse return relative_path);404 var server_end = mem.indexOfAnyPos(u8, path, 2, "/\\") orelse break :root_end path.len;
419 return WindowsPath{405 while (server_end < path.len and PathType.windows.isSep(u8, path[server_end])) server_end += 1;
420 .is_abs = isAbsoluteWindows(path),406 break :root_end mem.indexOfAnyPos(u8, path, server_end, "/\\") orelse path.len;
421 .kind = WindowsPath.Kind.NetworkShare,407 };
422 .disk_designator = path[0..it.index],408 return WindowsPath{
423 };409 .is_abs = true,
424 }410 .kind = WindowsPath.Kind.NetworkShare,
411 .disk_designator = path[0..root_end],
412 };
425 }413 }
426 return relative_path;414 return relative_path;
427}415}
...@@ -446,10 +434,22 @@ test windowsParsePath {...@@ -446,10 +434,22 @@ test windowsParsePath {
446 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));434 try testing.expect(mem.eql(u8, parsed.disk_designator, "\\\\a/b"));
447 }435 }
448 {436 {
449 const parsed = windowsParsePath("\\\\a\\");437 const parsed = windowsParsePath("\\/a\\");
450 try testing.expect(!parsed.is_abs);438 try testing.expect(parsed.is_abs);
451 try testing.expect(parsed.kind == WindowsPath.Kind.None);439 try testing.expect(parsed.kind == WindowsPath.Kind.NetworkShare);
452 try testing.expect(mem.eql(u8, parsed.disk_designator, ""));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"));
453 }453 }
454 {454 {
455 const parsed = windowsParsePath("/usr/local");455 const parsed = windowsParsePath("/usr/local");
...@@ -465,6 +465,229 @@ test windowsParsePath {...@@ -465,6 +465,229 @@ test windowsParsePath {
465 }465 }
466}466}
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`
468pub fn diskDesignator(path: []const u8) []const u8 {691pub fn diskDesignator(path: []const u8) []const u8 {
469 if (native_os == .windows) {692 if (native_os == .windows) {
470 return diskDesignatorWindows(path);693 return diskDesignatorWindows(path);
...@@ -473,41 +696,172 @@ pub fn diskDesignator(path: []const u8) []const u8 {...@@ -473,41 +696,172 @@ pub fn diskDesignator(path: []const u8) []const u8 {
473 }696 }
474}697}
475698
699/// Deprecated; use `parsePathWindows`
476pub fn diskDesignatorWindows(path: []const u8) []const u8 {700pub fn diskDesignatorWindows(path: []const u8) []const u8 {
477 return windowsParsePath(path).disk_designator;701 return windowsParsePath(path).disk_designator;
478}702}
479703
480fn networkShareServersEql(ns1: []const u8, ns2: []const u8) bool {704fn WindowsUNC(comptime T: type) type {
481 const sep1 = ns1[0];705 return struct {
482 const sep2 = ns2[0];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);713/// Asserts that `path` starts with two path separators
485 var it2 = mem.tokenizeScalar(u8, ns2, sep2);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 }
488}798}
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 };
491 switch (kind) {809 switch (kind) {
492 WindowsPath.Kind.None => {810 .drive => {
493 assert(p1.len == 0);811 const drive_letter1 = getDriveLetter(T, p1);
494 assert(p2.len == 0);812 const drive_letter2 = getDriveLetter(T, p2);
495 return true;813
496 },814 return eql(drive_letter1, drive_letter2);
497 WindowsPath.Kind.Drive => {
498 return ascii.toUpper(p1[0]) == ascii.toUpper(p2[0]);
499 },815 },
500 WindowsPath.Kind.NetworkShare => {816 .unc => {
501 var it1 = mem.tokenizeAny(u8, p1, "/\\");817 var unc1 = parseUNC(T, p1);
502 var it2 = mem.tokenizeAny(u8, p2, "/\\");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);
505 },822 },
506 }823 }
507}824}
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
509/// On Windows, this calls `resolveWindows` and on POSIX it calls `resolvePosix`.863/// 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 {
511 if (native_os == .windows) {865 if (native_os == .windows) {
512 return resolveWindows(allocator, paths);866 return resolveWindows(allocator, paths);
513 } else {867 } else {
...@@ -516,184 +870,232 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {...@@ -516,184 +870,232 @@ pub fn resolve(allocator: Allocator, paths: []const []const u8) ![]u8 {
516}870}
517871
518/// This function is like a series of `cd` statements executed one after another.872/// 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.873/// It resolves "." and ".." to the best of its ability, but will not convert relative paths to
520/// The result does not have a trailing path separator.874/// an absolute path, use std.fs.Dir.realpath instead.
521/// Each drive has its own current working directory.875/// ".." components may persist in the resolved path if the resolved path is relative or drive-relative.
522/// Path separators are canonicalized to '\\' and drives are canonicalized to capital letters.876/// 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///
523/// Note: all usage of this function should be audited due to the existence of symlinks.891/// Note: all usage of this function should be audited due to the existence of symlinks.
524/// Without performing actual syscalls, resolving `..` could be incorrect.892/// Without performing actual syscalls, resolving `..` could be incorrect.
525/// This API may break in the future: https://github.com/ziglang/zig/issues/13613893/// This API may break in the future: https://github.com/ziglang/zig/issues/13613
526pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) ![]u8 {894pub fn resolveWindows(allocator: Allocator, paths: []const []const u8) Allocator.Error![]u8 {
527 assert(paths.len > 0);895 // Avoid heap allocation when paths.len is <= @bitSizeOf(usize) * 2
528896 // (we use `* 3` because stackFallback uses 1 usize as a length)
529 // determine which disk designator we will result with, if any897 var bit_set_allocator_state = std.heap.stackFallback(@sizeOf(usize) * 3, allocator);
530 var result_drive_buf = "_:".*;898 const bit_set_allocator = bit_set_allocator_state.get();
531 var disk_designator: []const u8 = "";899 var relevant_paths = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(bit_set_allocator, paths.len);
532 var drive_kind = WindowsPath.Kind.None;900 defer relevant_paths.deinit(bit_set_allocator);
533 var have_abs_path = false;901
534 var first_index: usize = 0;902 // Iterate the paths backwards, marking the relevant paths along the way.
535 for (paths, 0..) |p, i| {903 // This also allows us to break from the loop whenever any earlier paths are known to be irrelevant.
536 const parsed = windowsParsePath(p);904 var first_path_i: usize = paths.len;
537 if (parsed.is_abs) {905 const effective_root_path: WindowsPath2(u8) = root: {
538 have_abs_path = true;906 var last_effective_root_path: WindowsPath2(u8) = .{ .kind = .relative, .root = "" };
539 first_index = i;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 }
540 }970 }
541 switch (parsed.kind) {971 // After iterating, if the pending effective root is drive-relative then that means
542 .Drive => {972 // nothing has led to forcing a drive-absolute root (a path that allows resolving the
543 result_drive_buf[0] = ascii.toUpper(parsed.disk_designator[0]);973 // drive-specific CWD would cause an early break), so we now need to ignore all paths
544 disk_designator = result_drive_buf[0..];974 // before the most recent drive-relative one. For example, if we're resolving
545 drive_kind = WindowsPath.Kind.Drive;975 // { "\\rooted", "relative", "C:drive-relative" }
546 },976 // then the `\rooted` and `relative` needs to be ignored since we can't
547 .NetworkShare => {977 // know what the rooted path is rooted against as that'd require knowing the CWD.
548 disk_designator = parsed.disk_designator;978 if (last_effective_root_path.kind == .drive_relative) {
549 drive_kind = WindowsPath.Kind.NetworkShare;979 for (0..last_drive_relative_path_i) |i| {
550 },980 relevant_paths.unset(i);
551 .None => {},981 }
552 }982 }
553 }983 break :root last_effective_root_path;
984 };
554985
555 // if we will result with a disk designator, loop again to determine986 var result: std.ArrayList(u8) = .empty;
556 // which is the last time the disk designator is absolutely specified, if any987 defer result.deinit(allocator);
557 // and count up the max bytes for paths related to this disk designator988
558 if (drive_kind != WindowsPath.Kind.None) {989 var want_path_sep_between_root_and_component = false;
559 have_abs_path = false;990 switch (effective_root_path.kind) {
560 first_index = 0;991 .root_local_device, .local_device => {
561 var correct_disk_designator = false;992 try result.ensureUnusedCapacity(allocator, 3);
562993 result.appendSliceAssumeCapacity("\\\\");
563 for (paths, 0..) |p, i| {994 result.appendAssumeCapacity(effective_root_path.root[2]); // . or ?
564 const parsed = windowsParsePath(p);995 want_path_sep_between_root_and_component = true;
565 if (parsed.kind != WindowsPath.Kind.None) {996 },
566 if (parsed.kind == drive_kind) {997 .drive_absolute, .drive_relative => {
567 correct_disk_designator = compareDiskDesignators(drive_kind, disk_designator, parsed.disk_designator);998 try result.ensureUnusedCapacity(allocator, effective_root_path.root.len);
568 } else {999 result.appendAssumeCapacity(std.ascii.toUpper(effective_root_path.root[0]));
569 continue;1000 result.appendAssumeCapacity(':');
570 }1001 if (effective_root_path.kind == .drive_absolute) {
1002 result.appendAssumeCapacity('\\');
571 }1003 }
572 if (!correct_disk_designator) {1004 },
573 continue;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;
574 }1022 }
575 if (parsed.is_abs) {1023 if (unc.share.len > 0) {
576 first_index = i;1024 result.appendSliceAssumeCapacity(unc.share);
577 have_abs_path = true;1025 if (unc.sep_after_share)
1026 result.appendAssumeCapacity('\\')
1027 else
1028 want_path_sep_between_root_and_component = true;
578 }1029 }
579 }1030 },
1031 .rooted => {
1032 try result.append(allocator, '\\');
1033 },
1034 .relative => {},
580 }1035 }
5811036
582 // Allocate result and fill in the disk designator.1037 const root_len = result.items.len;
583 var result = std.array_list.Managed(u8).init(allocator);
584 defer result.deinit();
585
586 const disk_designator_len: usize = l: {
587 if (!have_abs_path) break :l 0;
588 switch (drive_kind) {
589 .Drive => {
590 try result.appendSlice(disk_designator);
591 break :l disk_designator.len;
592 },
593 .NetworkShare => {
594 var it = mem.tokenizeAny(u8, paths[first_index], "/\\");
595 const server_name = it.next().?;
596 const other_name = it.next().?;
597
598 try result.ensureUnusedCapacity(2 + 1 + server_name.len + other_name.len);
599 result.appendSliceAssumeCapacity("\\\\");
600 result.appendSliceAssumeCapacity(server_name);
601 result.appendAssumeCapacity('\\');
602 result.appendSliceAssumeCapacity(other_name);
603
604 break :l result.items.len;
605 },
606 .None => {
607 break :l 1;
608 },
609 }
610 };
611
612 var correct_disk_designator = true;
613 var negative_count: usize = 0;1038 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| {1042 const parsed = parsePathWindows(u8, path);
616 const parsed = windowsParsePath(p);1043 const skip_len = parsed.root.len;
6171044 var it = mem.tokenizeAny(u8, path[skip_len..], "/\\");
618 if (parsed.kind != .None) {
619 if (parsed.kind == drive_kind) {
620 const dd = result.items[0..disk_designator_len];
621 correct_disk_designator = compareDiskDesignators(drive_kind, dd, parsed.disk_designator);
622 } else {
623 continue;
624 }
625 }
626 if (!correct_disk_designator) {
627 continue;
628 }
629 var it = mem.tokenizeAny(u8, p[parsed.disk_designator.len..], "/\\");
630 while (it.next()) |component| {1045 while (it.next()) |component| {
631 if (mem.eql(u8, component, ".")) {1046 if (mem.eql(u8, component, ".")) {
632 continue;1047 continue;
633 } else if (mem.eql(u8, component, "..")) {1048 } 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)) {
635 negative_count += 1;1050 negative_count += 1;
636 continue;1051 continue;
637 }1052 }
638 while (true) {1053 while (true) {
639 if (result.items.len == disk_designator_len) {1054 if (result.items.len == root_len) {
640 break;1055 break;
641 }1056 }
642 const end_with_sep = switch (result.items[result.items.len - 1]) {1057 const end_with_sep = PathType.windows.isSep(u8, result.items[result.items.len - 1]);
643 '\\', '/' => true,
644 else => false,
645 };
646 result.items.len -= 1;1058 result.items.len -= 1;
647 if (end_with_sep or result.items.len == 0) break;1059 if (end_with_sep) break;
648 }1060 }
649 } else if (!have_abs_path and result.items.len == 0) {1061 } else if (result.items.len == root_len and !want_path_sep_between_root_and_component) {
650 try result.appendSlice(component);1062 try result.appendSlice(allocator, component);
651 } else {1063 } else {
652 try result.ensureUnusedCapacity(1 + component.len);1064 try result.ensureUnusedCapacity(allocator, 1 + component.len);
653 result.appendAssumeCapacity('\\');1065 result.appendAssumeCapacity('\\');
654 result.appendSliceAssumeCapacity(component);1066 result.appendSliceAssumeCapacity(component);
655 }1067 }
656 }1068 }
657 }1069 }
6581070
659 if (disk_designator_len != 0 and result.items.len == disk_designator_len) {1071 if (root_len != 0 and result.items.len == root_len and negative_count == 0) {
660 try result.append('\\');1072 return result.toOwnedSlice(allocator);
661 return result.toOwnedSlice();
662 }1073 }
6631074
664 if (result.items.len == 0) {1075 if (result.items.len == root_len) {
665 if (negative_count == 0) {1076 if (negative_count == 0) {
666 return allocator.dupe(u8, ".");1077 return allocator.dupe(u8, ".");
667 } else {
668 const real_result = try allocator.alloc(u8, 3 * negative_count - 1);
669 var count = negative_count - 1;
670 var i: usize = 0;
671 while (count > 0) : (count -= 1) {
672 real_result[i..][0..3].* = "..\\".*;
673 i += 3;
674 }
675 real_result[i..][0..2].* = "..".*;
676 return real_result;
677 }1078 }
678 }
6791079
680 if (negative_count == 0) {1080 try result.ensureTotalCapacityPrecise(allocator, 3 * negative_count - 1);
681 return result.toOwnedSlice();1081 for (0..negative_count - 1) |_| {
1082 result.appendSliceAssumeCapacity("..\\");
1083 }
1084 result.appendSliceAssumeCapacity("..");
682 } else {1085 } else {
683 const real_result = try allocator.alloc(u8, 3 * negative_count + result.items.len);1086 const dest = try result.addManyAt(allocator, root_len, 3 * negative_count);
684 var count = negative_count;1087 for (0..negative_count) |i| {
685 var i: usize = 0;1088 dest[i * 3 ..][0..3].* = "..\\".*;
686 while (count > 0) : (count -= 1) {
687 real_result[i..][0..3].* = "..\\".*;
688 i += 3;
689 }1089 }
690 @memcpy(real_result[i..][0..result.items.len], result.items);
691 return real_result;
692 }1090 }
1091
1092 return result.toOwnedSlice(allocator);
693}1093}
6941094
695/// This function is like a series of `cd` statements executed one after another.1095/// 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.
697/// The result does not have a trailing path separator.1099/// The result does not have a trailing path separator.
698/// This function does not perform any syscalls. Executing this series of path1100/// This function does not perform any syscalls. Executing this series of path
699/// lookups on the actual filesystem may produce different results due to1101/// 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...@@ -772,10 +1174,14 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
772}1174}
7731175
774test resolve {1176test resolve {
1177 try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");
1178 try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");
775 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");1179 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
776 try testResolveWindows(&[_][]const u8{"."}, ".");1180 try testResolveWindows(&[_][]const u8{"."}, ".");
777 try testResolveWindows(&[_][]const u8{""}, ".");1181 try testResolveWindows(&[_][]const u8{""}, ".");
7781182
1183 try testResolvePosix(&[_][]const u8{ "a", "../../.." }, "../..");
1184 try testResolvePosix(&[_][]const u8{ "..", "", "../../foo" }, "../../../foo");
779 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");1185 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
780 try testResolvePosix(&[_][]const u8{"."}, ".");1186 try testResolvePosix(&[_][]const u8{"."}, ".");
781 try testResolvePosix(&[_][]const u8{""}, ".");1187 try testResolvePosix(&[_][]const u8{""}, ".");
...@@ -792,22 +1198,81 @@ test resolveWindows {...@@ -792,22 +1198,81 @@ test resolveWindows {
792 );1198 );
7931199
794 try testResolveWindows(&[_][]const u8{ "c:\\a\\b\\c", "/hi", "ok" }, "C:\\hi\\ok");1200 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");
795 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");1202 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "c:../a" }, "C:\\blah\\a");
796 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");1203 try testResolveWindows(&[_][]const u8{ "c:/blah\\blah", "d:/games", "C:../a" }, "C:\\blah\\a");
797 try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");1204 try testResolveWindows(&[_][]const u8{ "c:/ignore", "d:\\a/b\\c/d", "\\e.exe" }, "D:\\e.exe");
798 try testResolveWindows(&[_][]const u8{ "c:/ignore", "c:/some/file" }, "C:\\some\\file");1205 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");
800 try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");1208 try testResolveWindows(&[_][]const u8{ "//server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
801 try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");1209 try testResolveWindows(&[_][]const u8{ "\\\\server/share", "..", "relative\\" }, "\\\\server\\share\\relative");
802 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "C:\\");1210 try testResolveWindows(&[_][]const u8{ "\\\\server/share/ignore", "//server/share/bar" }, "\\\\server\\share\\bar");
803 try testResolveWindows(&[_][]const u8{ "c:/", "//dir" }, "C:\\dir");1211 try testResolveWindows(&[_][]const u8{ "\\/server\\share/", "..", "relative" }, "\\\\server\\share\\relative");
804 try testResolveWindows(&[_][]const u8{ "c:/", "//server/share" }, "\\\\server\\share\\");1212 try testResolveWindows(&[_][]const u8{ "\\\\server\\share", "C:drive-relative" }, "C:drive-relative");
805 try testResolveWindows(&[_][]const u8{ "c:/", "//server//share" }, "\\\\server\\share\\");1213 try testResolveWindows(&[_][]const u8{ "c:/", "//" }, "\\\\");
806 try testResolveWindows(&[_][]const u8{ "c:/", "///some//dir" }, "C:\\some\\dir");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");
807 try testResolveWindows(&[_][]const u8{ "C:\\foo\\tmp.3\\", "..\\tmp.3\\cycles\\root.js" }, "C:\\foo\\tmp.3\\cycles\\root.js");1219 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
809 // Keep relative paths relative.1259 // Keep relative paths relative.
810 try testResolveWindows(&[_][]const u8{"a/b"}, "a\\b");1260 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");
811}1276}
8121277
813test resolvePosix {1278test resolvePosix {
...@@ -855,63 +1320,18 @@ pub fn dirname(path: []const u8) ?[]const u8 {...@@ -855,63 +1320,18 @@ pub fn dirname(path: []const u8) ?[]const u8 {
855}1320}
8561321
857pub fn dirnameWindows(path: []const u8) ?[]const u8 {1322pub fn dirnameWindows(path: []const u8) ?[]const u8 {
858 if (path.len == 0)1323 return dirnameInner(.windows, path);
859 return null;
860
861 const root_slice = diskDesignatorWindows(path);
862 if (path.len == root_slice.len)
863 return null;
864
865 const have_root_slash = path.len > root_slice.len and (path[root_slice.len] == '/' or path[root_slice.len] == '\\');
866
867 var end_index: usize = path.len - 1;
868
869 while (path[end_index] == '/' or path[end_index] == '\\') {
870 if (end_index == 0)
871 return null;
872 end_index -= 1;
873 }
874
875 while (path[end_index] != '/' and path[end_index] != '\\') {
876 if (end_index == 0)
877 return null;
878 end_index -= 1;
879 }
880
881 if (have_root_slash and end_index == root_slice.len) {
882 end_index += 1;
883 }
884
885 if (end_index == 0)
886 return null;
887
888 return path[0..end_index];
889}1324}
8901325
891pub fn dirnamePosix(path: []const u8) ?[]const u8 {1326pub fn dirnamePosix(path: []const u8) ?[]const u8 {
892 if (path.len == 0)1327 return dirnameInner(.posix, path);
893 return null;1328}
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;
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;
915}1335}
9161336
917test dirnamePosix {1337test dirnamePosix {
...@@ -930,11 +1350,12 @@ test dirnamePosix {...@@ -930,11 +1350,12 @@ test dirnamePosix {
9301350
931test dirnameWindows {1351test dirnameWindows {
932 try testDirnameWindows("c:\\", null);1352 try testDirnameWindows("c:\\", null);
1353 try testDirnameWindows("c:\\\\", null);
933 try testDirnameWindows("c:\\foo", "c:\\");1354 try testDirnameWindows("c:\\foo", "c:\\");
934 try testDirnameWindows("c:\\foo\\", "c:\\");1355 try testDirnameWindows("c:\\\\foo\\", "c:\\");
935 try testDirnameWindows("c:\\foo\\bar", "c:\\foo");1356 try testDirnameWindows("c:\\foo\\bar", "c:\\foo");
936 try testDirnameWindows("c:\\foo\\bar\\", "c:\\foo");1357 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");
938 try testDirnameWindows("\\", null);1359 try testDirnameWindows("\\", null);
939 try testDirnameWindows("\\foo", "\\");1360 try testDirnameWindows("\\foo", "\\");
940 try testDirnameWindows("\\foo\\", "\\");1361 try testDirnameWindows("\\foo\\", "\\");
...@@ -942,19 +1363,30 @@ test dirnameWindows {...@@ -942,19 +1363,30 @@ test dirnameWindows {
942 try testDirnameWindows("\\foo\\bar\\", "\\foo");1363 try testDirnameWindows("\\foo\\bar\\", "\\foo");
943 try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");1364 try testDirnameWindows("\\foo\\bar\\baz", "\\foo\\bar");
944 try testDirnameWindows("c:", null);1365 try testDirnameWindows("c:", null);
945 try testDirnameWindows("c:foo", null);1366 try testDirnameWindows("c:foo", "c:");
946 try testDirnameWindows("c:foo\\", null);1367 try testDirnameWindows("c:foo\\", "c:");
947 try testDirnameWindows("c:foo\\bar", "c:foo");1368 try testDirnameWindows("c:foo\\bar", "c:foo");
948 try testDirnameWindows("c:foo\\bar\\", "c:foo");1369 try testDirnameWindows("c:foo\\bar\\", "c:foo");
949 try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");1370 try testDirnameWindows("c:foo\\bar\\baz", "c:foo\\bar");
950 try testDirnameWindows("file:stream", null);1371 try testDirnameWindows("file:stream", null);
951 try testDirnameWindows("dir\\file:stream", "dir");1372 try testDirnameWindows("dir\\file:stream", "dir");
952 try testDirnameWindows("\\\\unc\\share", null);1373 try testDirnameWindows("\\\\unc\\share", null);
1374 try testDirnameWindows("\\\\unc\\share\\\\", null);
953 try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");1375 try testDirnameWindows("\\\\unc\\share\\foo", "\\\\unc\\share\\");
954 try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");1376 try testDirnameWindows("\\\\unc\\share\\foo\\", "\\\\unc\\share\\");
955 try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");1377 try testDirnameWindows("\\\\unc\\share\\foo\\bar", "\\\\unc\\share\\foo");
956 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");1378 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\", "\\\\unc\\share\\foo");
957 try testDirnameWindows("\\\\unc\\share\\foo\\bar\\baz", "\\\\unc\\share\\foo\\bar");1379 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");
958 try testDirnameWindows("/a/b/", "/a");1390 try testDirnameWindows("/a/b/", "/a");
959 try testDirnameWindows("/a/b", "/a");1391 try testDirnameWindows("/a/b", "/a");
960 try testDirnameWindows("/a", "/");1392 try testDirnameWindows("/a", "/");
...@@ -974,7 +1406,7 @@ fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {...@@ -974,7 +1406,7 @@ fn testDirnamePosix(input: []const u8, expected_output: ?[]const u8) !void {
9741406
975fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {1407fn testDirnameWindows(input: []const u8, expected_output: ?[]const u8) !void {
976 if (dirnameWindows(input)) |output| {1408 if (dirnameWindows(input)) |output| {
977 try testing.expect(mem.eql(u8, output, expected_output.?));1409 try testing.expectEqualStrings(expected_output.?, output);
978 } else {1410 } else {
979 try testing.expect(expected_output == null);1411 try testing.expect(expected_output == null);
980 }1412 }
...@@ -989,56 +1421,17 @@ pub fn basename(path: []const u8) []const u8 {...@@ -989,56 +1421,17 @@ pub fn basename(path: []const u8) []const u8 {
989}1421}
9901422
991pub fn basenamePosix(path: []const u8) []const u8 {1423pub fn basenamePosix(path: []const u8) []const u8 {
992 if (path.len == 0)1424 return basenameInner(.posix, path);
993 return &[_]u8{};
994
995 var end_index: usize = path.len - 1;
996 while (path[end_index] == '/') {
997 if (end_index == 0)
998 return &[_]u8{};
999 end_index -= 1;
1000 }
1001 var start_index: usize = end_index;
1002 end_index += 1;
1003 while (path[start_index] != '/') {
1004 if (start_index == 0)
1005 return path[0..end_index];
1006 start_index -= 1;
1007 }
1008
1009 return path[start_index + 1 .. end_index];
1010}1425}
10111426
1012pub fn basenameWindows(path: []const u8) []const u8 {1427pub fn basenameWindows(path: []const u8) []const u8 {
1013 if (path.len == 0)1428 return basenameInner(.windows, path);
1014 return &[_]u8{};1429}
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 }
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;
1042}1435}
10431436
1044test basename {1437test basename {
...@@ -1053,7 +1446,9 @@ test basename {...@@ -1053,7 +1446,9 @@ test basename {
1053 try testBasename("/aaa/", "aaa");1446 try testBasename("/aaa/", "aaa");
1054 try testBasename("/aaa/b", "b");1447 try testBasename("/aaa/b", "b");
1055 try testBasename("/a/b", "b");1448 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
1058 try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");1453 try testBasenamePosix("\\dir\\basename.ext", "\\dir\\basename.ext");
1059 try testBasenamePosix("\\basename.ext", "\\basename.ext");1454 try testBasenamePosix("\\basename.ext", "\\basename.ext");
...@@ -1076,6 +1471,12 @@ test basename {...@@ -1076,6 +1471,12 @@ test basename {
1076 try testBasenameWindows("C:basename.ext", "basename.ext");1471 try testBasenameWindows("C:basename.ext", "basename.ext");
1077 try testBasenameWindows("C:basename.ext\\", "basename.ext");1472 try testBasenameWindows("C:basename.ext\\", "basename.ext");
1078 try testBasenameWindows("C:basename.ext\\\\", "basename.ext");1473 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");
1079 try testBasenameWindows("C:foo", "foo");1480 try testBasenameWindows("C:foo", "foo");
1080 try testBasenameWindows("file:stream", "file:stream");1481 try testBasenameWindows("file:stream", "file:stream");
1081}1482}
...@@ -1092,11 +1493,15 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {...@@ -1092,11 +1493,15 @@ fn testBasenameWindows(input: []const u8, expected_output: []const u8) !void {
1092 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));1493 try testing.expectEqualSlices(u8, expected_output, basenameWindows(input));
1093}1494}
10941495
1496pub const RelativeError = std.process.GetCwdAllocError;
1497
1095/// Returns the relative path from `from` to `to`. If `from` and `to` each1498/// Returns the relative path from `from` to `to`. If `from` and `to` each
1096/// resolve to the same path (after calling `resolve` on each), a zero-length1499/// resolve to the same path (after calling `resolve` on each), a zero-length
1097/// string is returned.1500/// string is returned.
1098/// On Windows this canonicalizes the drive to a capital letter and paths to `\\`.1501/// On Windows, the result is not guaranteed to be relative, as the paths may be
1099pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {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 {
1100 if (native_os == .windows) {1505 if (native_os == .windows) {
1101 return relativeWindows(allocator, from, to);1506 return relativeWindows(allocator, from, to);
1102 } else {1507 } else {
...@@ -1105,30 +1510,53 @@ pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {...@@ -1105,30 +1510,53 @@ pub fn relative(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1105}1510}
11061511
1107pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {1512pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {
1108 const cwd = try process.getCwdAlloc(allocator);1513 if (native_os != .windows) @compileError("this function relies on Windows-specific semantics");
1109 defer allocator.free(cwd);1514
1110 const resolved_from = try resolveWindows(allocator, &[_][]const u8{ cwd, from });1515 const parsed_from = parsePathWindows(u8, from);
1111 defer allocator.free(resolved_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);
1113 var clean_up_resolved_to = true;1540 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);
1115 defer if (clean_up_resolved_to) allocator.free(resolved_to);1542 defer if (clean_up_resolved_to) allocator.free(resolved_to);
11161543
1117 const parsed_from = windowsParsePath(resolved_from);1544 const parsed_resolved_from = parsePathWindows(u8, resolved_from);
1118 const parsed_to = windowsParsePath(resolved_to);1545 const parsed_resolved_to = parsePathWindows(u8, resolved_to);
1546
1119 const result_is_to = x: {1547 const result_is_to = x: {
1120 if (parsed_from.kind != parsed_to.kind) {1548 if (parsed_resolved_from.kind != parsed_resolved_to.kind) {
1121 break :x true;1549 break :x true;
1122 } else switch (parsed_from.kind) {1550 }
1123 .NetworkShare => {1551 switch (parsed_resolved_from.kind) {
1124 break :x !networkShareServersEql(parsed_to.disk_designator, parsed_from.disk_designator);1552 .drive_absolute, .drive_relative => {
1125 },1553 break :x !compareDiskDesignators(u8, .drive, parsed_resolved_from.root, parsed_resolved_to.root);
1126 .Drive => {
1127 break :x ascii.toUpper(parsed_from.disk_designator[0]) != ascii.toUpper(parsed_to.disk_designator[0]);
1128 },1554 },
1129 .None => {1555 .unc_absolute => {
1130 break :x false;1556 break :x !compareDiskDesignators(u8, .unc, parsed_resolved_from.root, parsed_resolved_to.root);
1131 },1557 },
1558 .relative, .rooted, .local_device => break :x false,
1559 .root_local_device => break :x true,
1132 }1560 }
1133 };1561 };
11341562
...@@ -1137,8 +1565,8 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !...@@ -1137,8 +1565,8 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !
1137 return resolved_to;1565 return resolved_to;
1138 }1566 }
11391567
1140 var from_it = mem.tokenizeAny(u8, resolved_from, "/\\");1568 var from_it = mem.tokenizeAny(u8, resolved_from[parsed_resolved_from.root.len..], "/\\");
1141 var to_it = mem.tokenizeAny(u8, resolved_to, "/\\");1569 var to_it = mem.tokenizeAny(u8, resolved_to[parsed_resolved_to.root.len..], "/\\");
1142 while (true) {1570 while (true) {
1143 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());1571 const from_component = from_it.next() orelse return allocator.dupe(u8, to_it.rest());
1144 const to_rest = to_it.rest();1572 const to_rest = to_it.rest();
...@@ -1170,11 +1598,101 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !...@@ -1170,11 +1598,101 @@ pub fn relativeWindows(allocator: Allocator, from: []const u8, to: []const u8) !
11701598
1171 return allocator.realloc(result, result_index);1599 return allocator.realloc(result, result_index);
1172 }1600 }
1173
1174 return [_]u8{};1601 return [_]u8{};
1175}1602}
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
1177pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]u8 {1693pub 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
1178 const cwd = try process.getCwdAlloc(allocator);1696 const cwd = try process.getCwdAlloc(allocator);
1179 defer allocator.free(cwd);1697 defer allocator.free(cwd);
1180 const resolved_from = try resolvePosix(allocator, &[_][]const u8{ cwd, from });1698 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) ![]...@@ -1217,51 +1735,59 @@ pub fn relativePosix(allocator: Allocator, from: []const u8, to: []const u8) ![]
1217}1735}
12181736
1219test relative {1737test relative {
1220 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");1738 if (native_os == .windows) {
1221 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");1739 try testRelativeWindows("c:/blah\\blah", "d:/games", "D:\\games");
1222 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");1740 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa", "..");
1223 try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");1741 try testRelativeWindows("c:/aaaa/bbbb", "c:/cccc", "..\\..\\cccc");
1224 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");1742 try testRelativeWindows("c:/aaaa/bbbb", "C:/aaaa/bbbb", "");
1225 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");1743 try testRelativeWindows("c:/aaaa/bbbb", "c:/aaaa/cccc", "..\\cccc");
1226 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");1744 try testRelativeWindows("c:/aaaa/", "c:/aaaa/cccc", "cccc");
1227 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");1745 try testRelativeWindows("c:/", "c:\\aaaa\\bbbb", "aaaa\\bbbb");
1228 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");1746 try testRelativeWindows("c:/aaaa/bbbb", "d:\\", "D:\\");
1229 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");1747 try testRelativeWindows("c:/AaAa/bbbb", "c:/aaaa/bbbb", "");
1230 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");1748 try testRelativeWindows("c:/aaaaa/", "c:/aaaa/cccc", "..\\aaaa\\cccc");
1231 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");1749 try testRelativeWindows("C:\\foo\\bar\\baz\\quux", "C:\\", "..\\..\\..\\..");
1232 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");1750 try testRelativeWindows("C:\\foo\\test", "C:\\foo\\test\\bar\\package.json", "bar\\package.json");
1233 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");1751 try testRelativeWindows("C:\\foo\\bar\\baz-quux", "C:\\foo\\bar\\baz", "..\\baz");
1234 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");1752 try testRelativeWindows("C:\\foo\\bar\\baz", "C:\\foo\\bar\\baz-quux", "..\\baz-quux");
1235 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");1753 try testRelativeWindows("\\\\foo\\bar", "\\\\foo\\bar\\baz", "baz");
1236 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");1754 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar", "..");
1237 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");1755 try testRelativeWindows("\\\\foo\\bar\\baz-quux", "\\\\foo\\bar\\baz", "..\\baz");
1238 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");1756 try testRelativeWindows("\\\\foo/bar\\baz-quux", "//foo\\bar/baz", "..\\baz");
1239 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");1757 try testRelativeWindows("\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz-quux", "..\\baz-quux");
1240 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "..\\baz");1758 try testRelativeWindows("C:\\baz-quux", "C:\\baz", "..\\baz");
1241 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "..\\baz-quux");1759 try testRelativeWindows("C:\\baz", "C:\\baz-quux", "..\\baz-quux");
1242 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");1760 try testRelativeWindows("\\\\foo\\baz-quux", "\\\\foo\\baz", "\\\\foo\\baz");
1243 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");1761 try testRelativeWindows("\\\\foo\\baz", "\\\\foo\\baz-quux", "\\\\foo\\baz-quux");
12441762 try testRelativeWindows("C:\\baz", "\\\\foo\\bar\\baz", "\\\\foo\\bar\\baz");
1245 try testRelativeWindows("a/b/c", "a\\b", "..");1763 try testRelativeWindows("\\\\foo\\bar\\baz", "C:\\baz", "C:\\baz");
1246 try testRelativeWindows("a/b/c", "a", "..\\..");1764
1247 try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");1765 try testRelativeWindows("c:blah\\blah", "c:foo", "..\\..\\foo");
12481766 try testRelativeWindows("c:foo", "c:foo\\bar", "bar");
1249 try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");1767 try testRelativeWindows("\\blah\\blah", "\\foo", "..\\..\\foo");
1250 // Unicode-aware case-insensitive path comparison1768 try testRelativeWindows("\\foo", "\\foo\\bar", "bar");
1251 try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");1769
12521770 try testRelativeWindows("a/b/c", "a\\b", "..");
1253 try testRelativePosix("/var/lib", "/var", "..");1771 try testRelativeWindows("a/b/c", "a", "..\\..");
1254 try testRelativePosix("/var/lib", "/bin", "../../bin");1772 try testRelativeWindows("a/b/c", "a\\b\\c\\d", "d");
1255 try testRelativePosix("/var/lib", "/var/lib", "");1773
1256 try testRelativePosix("/var/lib", "/var/apache", "../apache");1774 try testRelativeWindows("\\\\FOO\\bar\\baz", "\\\\foo\\BAR\\BAZ", "");
1257 try testRelativePosix("/var/", "/var/lib", "lib");1775 // Unicode-aware case-insensitive path comparison
1258 try testRelativePosix("/", "/var/lib", "var/lib");1776 try testRelativeWindows("\\\\кириллица\\ελληνικά\\português", "\\\\КИРИЛЛИЦА\\ΕΛΛΗΝΙΚΆ\\PORTUGUÊS", "");
1259 try testRelativePosix("/foo/test", "/foo/test/bar/package.json", "bar/package.json");1777 } else {
1260 try testRelativePosix("/Users/a/web/b/test/mails", "/Users/a/web/b", "../..");1778 try testRelativePosix("/var/lib", "/var", "..");
1261 try testRelativePosix("/foo/bar/baz-quux", "/foo/bar/baz", "../baz");1779 try testRelativePosix("/var/lib", "/bin", "../../bin");
1262 try testRelativePosix("/foo/bar/baz", "/foo/bar/baz-quux", "../baz-quux");1780 try testRelativePosix("/var/lib", "/var/lib", "");
1263 try testRelativePosix("/baz-quux", "/baz", "../baz");1781 try testRelativePosix("/var/lib", "/var/apache", "../apache");
1264 try testRelativePosix("/baz", "/baz-quux", "../baz-quux");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 }
1265}1791}
12661792
1267fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {1793fn testRelativePosix(from: []const u8, to: []const u8, expected_output: []const u8) !void {
...@@ -1391,7 +1917,10 @@ test stem {...@@ -1391,7 +1917,10 @@ test stem {
1391pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {1917pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
1392 return struct {1918 return struct {
1393 path: []const T,1919 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,
1395 start_index: usize = 0,1924 start_index: usize = 0,
1396 end_index: usize = 0,1925 end_index: usize = 0,
13971926
...@@ -1406,100 +1935,45 @@ pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {...@@ -1406,100 +1935,45 @@ pub fn ComponentIterator(comptime path_type: PathType, comptime T: type) type {
1406 path: []const T,1935 path: []const T,
1407 };1936 };
14081937
1409 const InitError = switch (path_type) {
1410 .windows => error{BadPathName},
1411 else => error{},
1412 };
1413
1414 /// After `init`, `next` will return the first component after the root1938 /// After `init`, `next` will return the first component after the root
1415 /// (there is no need to call `first` after `init`).1939 /// (there is no need to call `first` after `init`).
1416 /// To iterate backwards (from the end of the path to the beginning), call `last`1940 /// To iterate backwards (from the end of the path to the beginning), call `last`
1417 /// after `init` and then iterate via `previous` calls.1941 /// after `init` and then iterate via `previous` calls.
1418 /// For Windows paths, `error.BadPathName` is returned if the `path` has an explicit1942 /// For Windows paths, paths are assumed to be in the Win32 namespace.
1419 /// namespace prefix (`\\.\`, `\\?\`, or `\??\`) or if it is a UNC path with more1943 pub fn init(path: []const T) Self {
1420 /// than two path separators at the beginning.1944 const root_len: usize = switch (path_type) {
1421 pub fn init(path: []const T) InitError!Self {
1422 const root_end_index: usize = switch (path_type) {
1423 .posix, .uefi => posix: {1945 .posix, .uefi => posix: {
1424 // Root on UEFI and POSIX only differs by the path separator1946 // Root on UEFI and POSIX only differs by the path separator
1425 var root_end_index: usize = 0;1947 break :posix if (path.len > 0 and path_type.isSep(T, path[0])) 1 else 0;
1426 while (true) : (root_end_index += 1) {
1427 if (root_end_index >= path.len or !path_type.isSep(T, path[root_end_index])) {
1428 break;
1429 }
1430 }
1431 break :posix root_end_index;
1432 },1948 },
1433 .windows => windows: {1949 .windows => windows: {
1434 // Namespaces other than the Win32 file namespace are tricky1950 break :windows parsePathWindows(T, path).root.len;
1435 // and basically impossible to determine a 'root' for, since it's
1436 // possible to construct an effectively arbitrarily long 'root',
1437 // e.g. `\\.\GLOBALROOT\??\UNC\localhost\C$\foo` is a
1438 // possible path that would be effectively equivalent to
1439 // `C:\foo`, and the `GLOBALROOT\??\` part can also be recursive,
1440 // so `GLOBALROOT\??\GLOBALROOT\??\...` would work for any number
1441 // of repetitions. Therefore, paths with an explicit namespace prefix
1442 // (\\.\, \??\, \\?\) are not allowed here.
1443 if (std.os.windows.getNamespacePrefix(T, path) != .none) {
1444 return error.BadPathName;
1445 }
1446 const windows_path_type = std.os.windows.getUnprefixedPathType(T, path);
1447 break :windows switch (windows_path_type) {
1448 .relative => 0,
1449 .root_local_device => path.len,
1450 .rooted => 1,
1451 .unc_absolute => unc: {
1452 var end_index: usize = 2;
1453 // Any extra separators between the first two and the server name are not allowed
1454 // and will always lead to STATUS_OBJECT_PATH_INVALID if it is attempted
1455 // to be used.
1456 if (end_index < path.len and path_type.isSep(T, path[end_index])) {
1457 return error.BadPathName;
1458 }
1459 // Server
1460 while (end_index < path.len and !path_type.isSep(T, path[end_index])) {
1461 end_index += 1;
1462 }
1463 // Slash(es) after server
1464 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1465 end_index += 1;
1466 }
1467 // Share
1468 while (end_index < path.len and !path_type.isSep(T, path[end_index])) {
1469 end_index += 1;
1470 }
1471 // Slash(es) after share
1472 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1473 end_index += 1;
1474 }
1475 break :unc end_index;
1476 },
1477 .drive_absolute => drive: {
1478 var end_index: usize = 3;
1479 while (end_index < path.len and path_type.isSep(T, path[end_index])) {
1480 end_index += 1;
1481 }
1482 break :drive end_index;
1483 },
1484 .drive_relative => 2,
1485 };
1486 },1951 },
1487 };1952 };
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 }
1488 return .{1961 return .{
1489 .path = path,1962 .path = path,
1963 .root_len = root_len,
1490 .root_end_index = root_end_index,1964 .root_end_index = root_end_index,
1491 .start_index = root_end_index,1965 .start_index = root_end_index,
1492 .end_index = root_end_index,1966 .end_index = root_end_index,
1493 };1967 };
1494 }1968 }
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.
1497 /// For POSIX paths, this will be `/`.1971 /// For POSIX paths, this will be `/`.
1498 /// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.1972 /// For Windows paths, this will be something like `C:\`, `\\server\share\`, etc.
1499 /// For UEFI paths, this will be `\`.1973 /// For UEFI paths, this will be `\`.
1500 pub fn root(self: Self) ?[]const T {1974 pub fn root(self: Self) ?[]const T {
1501 if (self.root_end_index == 0) return null;1975 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];
1503 }1977 }
15041978
1505 /// Returns the first component (from the beginning of the path).1979 /// Returns the first component (from the beginning of the path).
...@@ -1614,7 +2088,7 @@ pub const NativeComponentIterator = ComponentIterator(switch (native_os) {...@@ -1614,7 +2088,7 @@ pub const NativeComponentIterator = ComponentIterator(switch (native_os) {
1614 else => .posix,2088 else => .posix,
1615}, u8);2089}, u8);
16162090
1617pub fn componentIterator(path: []const u8) !NativeComponentIterator {2091pub fn componentIterator(path: []const u8) NativeComponentIterator {
1618 return NativeComponentIterator.init(path);2092 return NativeComponentIterator.init(path);
1619}2093}
16202094
...@@ -1622,8 +2096,9 @@ test "ComponentIterator posix" {...@@ -1622,8 +2096,9 @@ test "ComponentIterator posix" {
1622 const PosixComponentIterator = ComponentIterator(.posix, u8);2096 const PosixComponentIterator = ComponentIterator(.posix, u8);
1623 {2097 {
1624 const path = "a/b/c/";2098 const path = "a/b/c/";
1625 var it = try PosixComponentIterator.init(path);2099 var it = PosixComponentIterator.init(path);
1626 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);2100 try std.testing.expectEqual(0, it.root_len);
2101 try std.testing.expectEqual(0, it.root_end_index);
1627 try std.testing.expect(null == it.root());2102 try std.testing.expect(null == it.root());
1628 {2103 {
1629 try std.testing.expect(null == it.previous());2104 try std.testing.expect(null == it.previous());
...@@ -1669,8 +2144,9 @@ test "ComponentIterator posix" {...@@ -1669,8 +2144,9 @@ test "ComponentIterator posix" {
16692144
1670 {2145 {
1671 const path = "/a/b/c/";2146 const path = "/a/b/c/";
1672 var it = try PosixComponentIterator.init(path);2147 var it = PosixComponentIterator.init(path);
1673 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);2148 try std.testing.expectEqual(1, it.root_len);
2149 try std.testing.expectEqual(1, it.root_end_index);
1674 try std.testing.expectEqualStrings("/", it.root().?);2150 try std.testing.expectEqualStrings("/", it.root().?);
1675 {2151 {
1676 try std.testing.expect(null == it.previous());2152 try std.testing.expect(null == it.previous());
...@@ -1714,10 +2190,59 @@ test "ComponentIterator posix" {...@@ -1714,10 +2190,59 @@ test "ComponentIterator posix" {
1714 }2190 }
1715 }2191 }
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
1717 {2241 {
1718 const path = "/";2242 const path = "/";
1719 var it = try PosixComponentIterator.init(path);2243 var it = PosixComponentIterator.init(path);
1720 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);2244 try std.testing.expectEqual(1, it.root_len);
2245 try std.testing.expectEqual(1, it.root_end_index);
1721 try std.testing.expectEqualStrings("/", it.root().?);2246 try std.testing.expectEqualStrings("/", it.root().?);
17222247
1723 try std.testing.expect(null == it.first());2248 try std.testing.expect(null == it.first());
...@@ -1733,8 +2258,9 @@ test "ComponentIterator posix" {...@@ -1733,8 +2258,9 @@ test "ComponentIterator posix" {
17332258
1734 {2259 {
1735 const path = "";2260 const path = "";
1736 var it = try PosixComponentIterator.init(path);2261 var it = PosixComponentIterator.init(path);
1737 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);2262 try std.testing.expectEqual(0, it.root_len);
2263 try std.testing.expectEqual(0, it.root_end_index);
1738 try std.testing.expect(null == it.root());2264 try std.testing.expect(null == it.root());
17392265
1740 try std.testing.expect(null == it.first());2266 try std.testing.expect(null == it.first());
...@@ -1753,8 +2279,9 @@ test "ComponentIterator windows" {...@@ -1753,8 +2279,9 @@ test "ComponentIterator windows" {
1753 const WindowsComponentIterator = ComponentIterator(.windows, u8);2279 const WindowsComponentIterator = ComponentIterator(.windows, u8);
1754 {2280 {
1755 const path = "a/b\\c//";2281 const path = "a/b\\c//";
1756 var it = try WindowsComponentIterator.init(path);2282 var it = WindowsComponentIterator.init(path);
1757 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);2283 try std.testing.expectEqual(0, it.root_len);
2284 try std.testing.expectEqual(0, it.root_end_index);
1758 try std.testing.expect(null == it.root());2285 try std.testing.expect(null == it.root());
1759 {2286 {
1760 try std.testing.expect(null == it.previous());2287 try std.testing.expect(null == it.previous());
...@@ -1800,8 +2327,9 @@ test "ComponentIterator windows" {...@@ -1800,8 +2327,9 @@ test "ComponentIterator windows" {
18002327
1801 {2328 {
1802 const path = "C:\\a/b/c/";2329 const path = "C:\\a/b/c/";
1803 var it = try WindowsComponentIterator.init(path);2330 var it = WindowsComponentIterator.init(path);
1804 try std.testing.expectEqual(@as(usize, 3), it.root_end_index);2331 try std.testing.expectEqual(3, it.root_len);
2332 try std.testing.expectEqual(3, it.root_end_index);
1805 try std.testing.expectEqualStrings("C:\\", it.root().?);2333 try std.testing.expectEqualStrings("C:\\", it.root().?);
1806 {2334 {
1807 const first = it.first().?;2335 const first = it.first().?;
...@@ -1835,10 +2363,49 @@ test "ComponentIterator windows" {...@@ -1835,10 +2363,49 @@ test "ComponentIterator windows" {
1835 }2363 }
1836 }2364 }
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
1838 {2404 {
1839 const path = "/";2405 const path = "/";
1840 var it = try WindowsComponentIterator.init(path);2406 var it = WindowsComponentIterator.init(path);
1841 try std.testing.expectEqual(@as(usize, 1), it.root_end_index);2407 try std.testing.expectEqual(1, it.root_len);
2408 try std.testing.expectEqual(1, it.root_end_index);
1842 try std.testing.expectEqualStrings("/", it.root().?);2409 try std.testing.expectEqualStrings("/", it.root().?);
18432410
1844 try std.testing.expect(null == it.first());2411 try std.testing.expect(null == it.first());
...@@ -1854,8 +2421,9 @@ test "ComponentIterator windows" {...@@ -1854,8 +2421,9 @@ test "ComponentIterator windows" {
18542421
1855 {2422 {
1856 const path = "";2423 const path = "";
1857 var it = try WindowsComponentIterator.init(path);2424 var it = WindowsComponentIterator.init(path);
1858 try std.testing.expectEqual(@as(usize, 0), it.root_end_index);2425 try std.testing.expectEqual(0, it.root_len);
2426 try std.testing.expectEqual(0, it.root_end_index);
1859 try std.testing.expect(null == it.root());2427 try std.testing.expect(null == it.root());
18602428
1861 try std.testing.expect(null == it.first());2429 try std.testing.expect(null == it.first());
...@@ -1880,8 +2448,9 @@ test "ComponentIterator windows WTF-16" {...@@ -1880,8 +2448,9 @@ test "ComponentIterator windows WTF-16" {
1880 const L = std.unicode.utf8ToUtf16LeStringLiteral;2448 const L = std.unicode.utf8ToUtf16LeStringLiteral;
18812449
1882 const path = L("C:\\a/b/c/");2450 const path = L("C:\\a/b/c/");
1883 var it = try WindowsComponentIterator.init(path);2451 var it = WindowsComponentIterator.init(path);
1884 try std.testing.expectEqual(@as(usize, 3), it.root_end_index);2452 try std.testing.expectEqual(3, it.root_len);
2453 try std.testing.expectEqual(3, it.root_end_index);
1885 try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);2454 try std.testing.expectEqualSlices(u16, L("C:\\"), it.root().?);
1886 {2455 {
1887 const first = it.first().?;2456 const first = it.first().?;
...@@ -1918,55 +2487,60 @@ test "ComponentIterator windows WTF-16" {...@@ -1918,55 +2487,60 @@ test "ComponentIterator windows WTF-16" {
1918test "ComponentIterator roots" {2487test "ComponentIterator roots" {
1919 // UEFI2488 // UEFI
1920 {2489 {
1921 var it = try ComponentIterator(.uefi, u8).init("\\\\a");2490 var it = ComponentIterator(.uefi, u8).init("\\\\a");
1922 try std.testing.expectEqualStrings("\\\\", it.root().?);2491 try std.testing.expectEqualStrings("\\", it.root().?);
19232492
1924 it = try ComponentIterator(.uefi, u8).init("//a");2493 it = ComponentIterator(.uefi, u8).init("//a");
1925 try std.testing.expect(null == it.root());2494 try std.testing.expect(null == it.root());
1926 }2495 }
1927 // POSIX2496 // POSIX
1928 {2497 {
1929 var it = try ComponentIterator(.posix, u8).init("//a");2498 var it = ComponentIterator(.posix, u8).init("//a");
1930 try std.testing.expectEqualStrings("//", it.root().?);2499 try std.testing.expectEqualStrings("/", it.root().?);
19312500
1932 it = try ComponentIterator(.posix, u8).init("\\\\a");2501 it = ComponentIterator(.posix, u8).init("\\\\a");
1933 try std.testing.expect(null == it.root());2502 try std.testing.expect(null == it.root());
1934 }2503 }
1935 // Windows2504 // Windows
1936 {2505 {
1937 // Drive relative2506 // Drive relative
1938 var it = try ComponentIterator(.windows, u8).init("C:a");2507 var it = ComponentIterator(.windows, u8).init("C:a");
1939 try std.testing.expectEqualStrings("C:", it.root().?);2508 try std.testing.expectEqualStrings("C:", it.root().?);
19402509
1941 // Drive absolute2510 // Drive absolute
1942 it = try ComponentIterator(.windows, u8).init("C://a");2511 it = ComponentIterator(.windows, u8).init("C:/a");
1943 try std.testing.expectEqualStrings("C://", it.root().?);2512 try std.testing.expectEqualStrings("C:/", it.root().?);
1944 it = try ComponentIterator(.windows, u8).init("C:\\a");2513 it = ComponentIterator(.windows, u8).init("C:\\a");
1945 try std.testing.expectEqualStrings("C:\\", it.root().?);2514 try std.testing.expectEqualStrings("C:\\", it.root().?);
2515 it = ComponentIterator(.windows, u8).init("C:///a");
2516 try std.testing.expectEqualStrings("C:/", it.root().?);
19462517
1947 // Rooted2518 // Rooted
1948 it = try ComponentIterator(.windows, u8).init("\\a");2519 it = ComponentIterator(.windows, u8).init("\\a");
1949 try std.testing.expectEqualStrings("\\", it.root().?);2520 try std.testing.expectEqualStrings("\\", it.root().?);
1950 it = try ComponentIterator(.windows, u8).init("/a");2521 it = ComponentIterator(.windows, u8).init("/a");
1951 try std.testing.expectEqualStrings("/", it.root().?);2522 try std.testing.expectEqualStrings("/", it.root().?);
19522523
1953 // Root local device2524 // Root local device
1954 it = try ComponentIterator(.windows, u8).init("\\\\.");2525 it = ComponentIterator(.windows, u8).init("\\\\.");
1955 try std.testing.expectEqualStrings("\\\\.", it.root().?);2526 try std.testing.expectEqualStrings("\\\\.", it.root().?);
1956 it = try ComponentIterator(.windows, u8).init("//?");2527 it = ComponentIterator(.windows, u8).init("//?");
1957 try std.testing.expectEqualStrings("//?", it.root().?);2528 try std.testing.expectEqualStrings("//?", it.root().?);
19582529
1959 // UNC absolute2530 // UNC absolute
1960 it = try ComponentIterator(.windows, u8).init("//");2531 it = ComponentIterator(.windows, u8).init("//");
1961 try std.testing.expectEqualStrings("//", it.root().?);2532 try std.testing.expectEqualStrings("//", it.root().?);
1962 it = try ComponentIterator(.windows, u8).init("\\\\a");2533 it = ComponentIterator(.windows, u8).init("\\\\a");
1963 try std.testing.expectEqualStrings("\\\\a", it.root().?);2534 try std.testing.expectEqualStrings("\\\\a", it.root().?);
1964 it = try ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");2535 it = ComponentIterator(.windows, u8).init("\\\\a\\b\\\\c");
1965 try std.testing.expectEqualStrings("\\\\a\\b\\\\", it.root().?);2536 try std.testing.expectEqualStrings("\\\\a\\b\\", it.root().?);
1966 it = try ComponentIterator(.windows, u8).init("//a");2537 it = ComponentIterator(.windows, u8).init("//a");
1967 try std.testing.expectEqualStrings("//a", it.root().?);2538 try std.testing.expectEqualStrings("//a", it.root().?);
1968 it = try ComponentIterator(.windows, u8).init("//a/b//c");2539 it = ComponentIterator(.windows, u8).init("//a/b//c");
1969 try std.testing.expectEqualStrings("//a/b//", it.root().?);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().?);
1970 }2544 }
1971}2545}
19722546
lib/std/fs/test.zig+1-1
...@@ -56,7 +56,7 @@ const PathType = enum {...@@ -56,7 +56,7 @@ const PathType = enum {
56 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.56 // using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
57 var fd_path_buf: [fs.max_path_bytes]u8 = undefined;57 var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
58 const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);58 const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);
59 const windows_path_type = windows.getUnprefixedPathType(u8, dir_path);59 const windows_path_type = windows.getWin32PathType(u8, dir_path);
60 switch (windows_path_type) {60 switch (windows_path_type) {
61 .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),61 .unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),
62 .drive_absolute => {62 .drive_absolute => {
lib/std/os/windows.zig+318-256
...@@ -836,8 +836,11 @@ pub fn CreateSymbolicLink(...@@ -836,8 +836,11 @@ pub fn CreateSymbolicLink(
836 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw836 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
837 var is_target_absolute = false;837 var is_target_absolute = false;
838 const final_target_path = target_path: {838 const final_target_path = target_path: {
839 switch (getNamespacePrefix(u16, target_path)) {839 if (hasCommonNtPrefix(u16, target_path)) {
840 .none => switch (getUnprefixedPathType(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)) {
841 // Rooted paths need to avoid getting put through wToPrefixedFileW844 // Rooted paths need to avoid getting put through wToPrefixedFileW
842 // (and they are treated as relative in this context)845 // (and they are treated as relative in this context)
843 // Note: It seems that rooted paths in symbolic links are relative to846 // Note: It seems that rooted paths in symbolic links are relative to
...@@ -849,10 +852,7 @@ pub fn CreateSymbolicLink(...@@ -849,10 +852,7 @@ pub fn CreateSymbolicLink(
849 // Keep relative paths relative, but anything else needs to get NT-prefixed.852 // Keep relative paths relative, but anything else needs to get NT-prefixed.
850 else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))853 else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path))
851 break :target_path target_path,854 break :target_path target_path,
852 },855 }
853 // Already an NT path, no need to do anything to it
854 .nt => break :target_path target_path,
855 else => {},
856 }856 }
857 var prefixed_target_path = try wToPrefixedFileW(dir, target_path);857 var prefixed_target_path = try wToPrefixedFileW(dir, target_path);
858 // We do this after prefixing to ensure that drive-relative paths are treated as absolute858 // We do this after prefixing to ensure that drive-relative paths are treated as absolute
...@@ -2308,271 +2308,338 @@ pub const Wtf16ToPrefixedFileWError = error{...@@ -2308,271 +2308,338 @@ pub const Wtf16ToPrefixedFileWError = error{
2308/// - . and space are not stripped from the end of relative paths (potential TODO)2308/// - . and space are not stripped from the end of relative paths (potential TODO)
2309pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {2309pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {
2310 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };2310 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
2311 switch (getNamespacePrefix(u16, path)) {2311 if (hasCommonNtPrefix(u16, path)) {
2312 // TODO: Figure out a way to design an API that can avoid the copy for .nt,2312 // TODO: Figure out a way to design an API that can avoid the copy for NT,
2313 // since it is always returned fully unmodified.2313 // since it is always returned fully unmodified.
2314 .nt, .verbatim => {2314 var path_space: PathSpace = undefined;
2315 var path_space: PathSpace = undefined;2315 path_space.data[0..nt_prefix.len].* = nt_prefix;
2316 path_space.data[0..nt_prefix.len].* = nt_prefix;2316 const len_after_prefix = path.len - nt_prefix.len;
2317 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 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);2318 path_space.len = path.len;
2319 path_space.len = path.len;2319 path_space.data[path_space.len] = 0;
2320 path_space.data[path_space.len] = 0;2320 return path_space;
2321 return path_space;2321 } else {
2322 },2322 const path_type = getWin32PathType(u16, path);
2323 .local_device, .fake_verbatim => {2323 var path_space: PathSpace = undefined;
2324 var path_space: PathSpace = undefined;2324 if (path_type == .local_device) {
2325 const path_byte_len = ntdll.RtlGetFullPathName_U(2325 switch (getLocalDevicePathType(u16, path)) {
2326 path.ptr,2326 .verbatim => {
2327 path_space.data.len * 2,2327 path_space.data[0..nt_prefix.len].* = nt_prefix;
2328 &path_space.data,2328 const len_after_prefix = path.len - nt_prefix.len;
2329 null,2329 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
2330 );2330 path_space.len = path.len;
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 };
2364 path_space.data[path_space.len] = 0;2331 path_space.data[path_space.len] = 0;
2365 return path_space;2332 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 },
2367 }2353 }
2368 // We now know we are going to return an absolute NT path, so2354 }
2369 // we can unconditionally prefix it with the NT prefix.2355 relative: {
2370 path_space.data[0..nt_prefix.len].* = nt_prefix;2356 if (path_type == .relative) {
2371 if (path_type == .root_local_device) {2357 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
2372 // `\\.` and `\\?` always get converted to `\??\` exactly, so2358 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
2373 // we can just stop here2359
2374 path_space.len = nt_prefix.len;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 };
2375 path_space.data[path_space.len] = 0;2373 path_space.data[path_space.len] = 0;
2376 return path_space;2374 return path_space;
2377 }2375 }
2378 const path_buf_offset = switch (path_type) {2376 }
2379 // UNC paths will always start with `\\`. However, we want to2377 // We now know we are going to return an absolute NT path, so
2380 // end up with something like `\??\UNC\server\share`, so to get2378 // we can unconditionally prefix it with the NT prefix.
2381 // RtlGetFullPathName to write into the spot we want the `server`2379 path_space.data[0..nt_prefix.len].* = nt_prefix;
2382 // part to end up, we need to provide an offset such that2380 if (path_type == .root_local_device) {
2383 // the `\\` part gets written where the `C\` of `UNC\` will be2381 // `\\.` and `\\?` always get converted to `\??\` exactly, so
2384 // in the final NT path.2382 // we can just stop here
2385 .unc_absolute => nt_prefix.len + 2,2383 path_space.len = nt_prefix.len;
2386 else => nt_prefix.len,2384 path_space.data[path_space.len] = 0;
2387 };2385 return path_space;
2388 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);2386 }
2389 const path_to_get: [:0]const u16 = path_to_get: {2387 const path_buf_offset = switch (path_type) {
2390 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because2388 // UNC paths will always start with `\\`. However, we want to
2391 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.2389 // end up with something like `\??\UNC\server\share`, so to get
2392 if (path_type != .relative or dir == null) {2390 // RtlGetFullPathName to write into the spot we want the `server`
2393 break :path_to_get path;2391 // part to end up, we need to provide an offset such that
2394 }2392 // the `\\` part gets written where the `C\` of `UNC\` will be
2395 // We can also skip GetFinalPathNameByHandle if the handle matches2393 // in the final NT path.
2396 // the handle returned by fs.cwd()2394 .unc_absolute => nt_prefix.len + 2,
2397 if (dir.? == std.fs.cwd().fd) {2395 else => nt_prefix.len,
2398 break :path_to_get path;2396 };
2399 }2397 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
2400 // At this point, we know we have a relative path that had too many2398 const path_to_get: [:0]const u16 = path_to_get: {
2401 // `..` components to be resolved by normalizePath, so we need to2399 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
2402 // convert it into an absolute path and let RtlGetFullPathName_U2400 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
2403 // canonicalize it. We do this by getting the path of the `dir`2401 if (path_type != .relative or dir == null) {
2404 // and appending the relative path to it.2402 break :path_to_get path;
2405 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;2403 }
2406 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {2404 // We can also skip GetFinalPathNameByHandle if the handle matches
2407 // This mapping is not correct; it is actually expected2405 // the handle returned by fs.cwd()
2408 // that calling GetFinalPathNameByHandle might return2406 if (dir.? == std.fs.cwd().fd) {
2409 // error.UnrecognizedVolume, and in fact has been observed2407 break :path_to_get path;
2410 // in the wild. The problem is that wToPrefixedFileW was2408 }
2411 // never intended to make *any* OS syscall APIs. It's only2409 // At this point, we know we have a relative path that had too many
2412 // supposed to convert a string to one that is eligible to2410 // `..` components to be resolved by normalizePath, so we need to
2413 // be used in the ntdll syscalls.2411 // convert it into an absolute path and let RtlGetFullPathName_U
2414 //2412 // canonicalize it. We do this by getting the path of the `dir`
2415 // To solve this, this function needs to no longer call2413 // and appending the relative path to it.
2416 // GetFinalPathNameByHandle under any conditions, or the2414 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
2417 // calling function needs to get reworked to not need to2415 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
2418 // call this function.2416 // This mapping is not correct; it is actually expected
2419 //2417 // that calling GetFinalPathNameByHandle might return
2420 // This may involve making breaking API changes.2418 // error.UnrecognizedVolume, and in fact has been observed
2421 error.UnrecognizedVolume => return error.Unexpected,2419 // in the wild. The problem is that wToPrefixedFileW was
2422 else => |e| return e,2420 // never intended to make *any* OS syscall APIs. It's only
2423 };2421 // supposed to convert a string to one that is eligible to
2424 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {2422 // be used in the ntdll syscalls.
2425 return error.NameTooLong;2423 //
2426 }2424 // To solve this, this function needs to no longer call
2427 // We don't have to worry about potentially doubling up path separators2425 // GetFinalPathNameByHandle under any conditions, or the
2428 // here since RtlGetFullPathName_U will handle canonicalizing it.2426 // calling function needs to get reworked to not need to
2429 dir_path_buf[dir_path.len] = '\\';2427 // call this function.
2430 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);2428 //
2431 const full_len = dir_path.len + 1 + path.len;2429 // This may involve making breaking API changes.
2432 dir_path_buf[full_len] = 0;2430 error.UnrecognizedVolume => return error.Unexpected,
2433 break :path_to_get dir_path_buf[0..full_len :0];2431 else => |e| return e,
2434 };2432 };
2435 const path_byte_len = ntdll.RtlGetFullPathName_U(2433 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
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) {
2445 return error.NameTooLong;2434 return error.NameTooLong;
2446 }2435 }
2447 path_space.len = path_buf_offset + (path_byte_len / 2);2436 // We don't have to worry about potentially doubling up path separators
2448 if (path_type == .unc_absolute) {2437 // here since RtlGetFullPathName_U will handle canonicalizing it.
2449 // Now add in the UNC, the `C` should overwrite the first `\` of the2438 dir_path_buf[dir_path.len] = '\\';
2450 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`2439 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
2451 std.debug.assert(path_space.data[path_buf_offset] == '\\');2440 const full_len = dir_path.len + 1 + path.len;
2452 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');2441 dir_path_buf[full_len] = 0;
2453 const unc = [_]u16{ 'U', 'N', 'C' };2442 break :path_to_get dir_path_buf[0..full_len :0];
2454 path_space.data[nt_prefix.len..][0..unc.len].* = unc;2443 };
2455 }2444 const path_byte_len = ntdll.RtlGetFullPathName_U(
2456 return path_space;2445 path_to_get.ptr,
2457 },2446 buf_len * 2,
2458 }2447 path_space.data[path_buf_offset..].ptr,
2459}2448 null,
24602449 );
2461pub const NamespacePrefix = enum {2450 if (path_byte_len == 0) {
2462 none,2451 // TODO: This may not be the right error
2463 /// `\\.\` (path separators can be `\` or `/`)2452 return error.BadPathName;
2464 local_device,2453 } else if (path_byte_len / 2 > buf_len) {
2465 /// `\\?\`2454 return error.NameTooLong;
2466 /// When converted to an NT path, everything past the prefix is left2455 }
2467 /// untouched and `\\?\` is replaced by `\??\`.2456 path_space.len = path_buf_offset + (path_byte_len / 2);
2468 verbatim,2457 if (path_type == .unc_absolute) {
2469 /// `\\?\` without all path separators being `\`.2458 // Now add in the UNC, the `C` should overwrite the first `\` of the
2470 /// This seems to be recognized as a prefix, but the 'verbatim' aspect2459 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
2471 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,2460 std.debug.assert(path_space.data[path_buf_offset] == '\\');
2472 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't2461 std.debug.assert(path_space.data[path_buf_offset + 1] == '\\');
2473 /// be treated as part of the final path])2462 const unc = [_]u16{ 'U', 'N', 'C' };
2474 fake_verbatim,2463 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
2475 /// `\??\`2464 }
2476 nt,2465 return path_space;
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,
2497 }2466 }
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, "//?/"));
2520}2467}
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`
2523 unc_absolute,2472 unc_absolute,
2473 /// `C:\foo`
2524 drive_absolute,2474 drive_absolute,
2475 /// `C:foo`
2525 drive_relative,2476 drive_relative,
2477 /// `\foo`
2526 rooted,2478 rooted,
2479 /// `foo`
2527 relative,2480 relative,
2481 /// `\\.\foo`, `\\?\foo`
2482 local_device,
2483 /// `\\.`, `\\?`
2528 root_local_device,2484 root_local_device,
2529};2485};
25302486
2531/// Get the path type of a path that is known to not have any namespace prefixes2487/// Get the path type of a Win32 namespace path.
2532/// (`\\?\`, `\\.\`, `\??\`).2488/// Similar to `RtlDetermineDosPathNameType_U`.
2533/// If `T` is `u16`, then `path` should be encoded as WTF-16LE.2489/// 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 {
2535 if (path.len < 1) return .relative;2491 if (path.len < 1) return .relative;
25362492
2537 if (std.debug.runtime_safety) {
2538 std.debug.assert(getNamespacePrefix(T, path) == .none);
2539 }
2540
2541 const windows_path = std.fs.path.PathType.windows;2493 const windows_path = std.fs.path.PathType.windows;
2542 if (windows_path.isSep(T, mem.littleToNative(T, path[0]))) {2494 if (windows_path.isSep(T, mem.littleToNative(T, path[0]))) {
2543 // \x2495 // \x
2544 if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted;2496 if (path.len < 2 or !windows_path.isSep(T, mem.littleToNative(T, path[1]))) return .rooted;
2545 // exactly \\. or \\? with nothing trailing2497 // \\. or \\?
2546 if (path.len == 3 and (mem.littleToNative(T, path[2]) == '.' or mem.littleToNative(T, path[2]) == '?')) return .root_local_device;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 }
2547 // \\x2504 // \\x
2548 return .unc_absolute;2505 return .unc_absolute;
2549 } else {2506 } 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 };
2550 // x2540 // 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;
2552 // x:\2542 // 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;
2554 // x:2544 // x:
2555 return .drive_relative;2545 return .drive_relative;
2556 }2546 }
2557}2547}
25582548
2559test getUnprefixedPathType {2549test getWin32PathType {
2560 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, ""));2550 try std.testing.expectEqual(.relative, getWin32PathType(u8, ""));
2561 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x"));2551 try std.testing.expectEqual(.relative, getWin32PathType(u8, "x"));
2562 try std.testing.expectEqual(UnprefixedPathType.relative, getUnprefixedPathType(u8, "x\\"));2552 try std.testing.expectEqual(.relative, getWin32PathType(u8, "x\\"));
2563 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "//."));2553
2564 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "/\\?"));2554 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "//."));
2565 try std.testing.expectEqual(UnprefixedPathType.root_local_device, getUnprefixedPathType(u8, "\\\\?"));2555 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "/\\?"));
2566 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "\\\\x"));2556 try std.testing.expectEqual(.root_local_device, getWin32PathType(u8, "\\\\?"));
2567 try std.testing.expectEqual(UnprefixedPathType.unc_absolute, getUnprefixedPathType(u8, "//x"));2557
2568 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "\\x"));2558 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "//./x"));
2569 try std.testing.expectEqual(UnprefixedPathType.rooted, getUnprefixedPathType(u8, "/"));2559 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "/\\?\\x"));
2570 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:"));2560 try std.testing.expectEqual(.local_device, getWin32PathType(u8, "\\\\?\\x"));
2571 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:abc"));2561 // local device paths require a path separator after the root, otherwise it is considered a UNC path
2572 try std.testing.expectEqual(UnprefixedPathType.drive_relative, getUnprefixedPathType(u8, "x:a/b/c"));2562 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "\\\\?x"));
2573 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\"));2563 try std.testing.expectEqual(.unc_absolute, getWin32PathType(u8, "//.x"));
2574 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:\\abc"));2564
2575 try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));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 };
2576}2643}
25772644
2578/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.2645/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
...@@ -2589,30 +2656,25 @@ test getUnprefixedPathType {...@@ -2589,30 +2656,25 @@ test getUnprefixedPathType {
2589/// Supports in-place modification (`path` and `out` may refer to the same slice).2656/// Supports in-place modification (`path` and `out` may refer to the same slice).
2590pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {2657pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {
2591 if (path.len > PATH_MAX_WIDE) return error.NameTooLong;2658 if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
25922659 if (!hasCommonNtPrefix(u16, path)) return error.NotNtPath;
2593 const namespace_prefix = getNamespacePrefix(u16, path);2660
2594 switch (namespace_prefix) {2661 var dest_index: usize = 0;
2595 .nt => {2662 var after_prefix = path[4..]; // after the `\??\`
2596 var dest_index: usize = 0;2663 // The prefix \??\UNC\ means this is a UNC path, in which case the
2597 var after_prefix = path[4..]; // after the `\??\`2664 // `\??\UNC\` should be replaced by `\\` (two backslashes)
2598 // The prefix \??\UNC\ means this is a UNC path, in which case the2665 const is_unc = after_prefix.len >= 4 and
2599 // `\??\UNC\` should be replaced by `\\` (two backslashes)2666 eqlIgnoreCaseWtf16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
2600 const is_unc = after_prefix.len >= 4 and2667 std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));
2601 eqlIgnoreCaseWTF16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and2668 const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
2602 std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));2669 if (out.len < win32_len) return error.NameTooLong;
2603 const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);2670 if (is_unc) {
2604 if (out.len < win32_len) return error.NameTooLong;2671 out[0] = comptime std.mem.nativeToLittle(u16, '\\');
2605 if (is_unc) {2672 dest_index += 1;
2606 out[0] = comptime std.mem.nativeToLittle(u16, '\\');2673 // We want to include the last `\` of `\??\UNC\`
2607 dest_index += 1;2674 after_prefix = path[7..];
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,
2615 }2675 }
2676 @memmove(out[dest_index..][0..after_prefix.len], after_prefix);
2677 return out[0..win32_len];
2616}2678}
26172679
2618test ntToWin32Namespace {2680test ntToWin32Namespace {
lib/std/os/windows/test.zig+102-2
...@@ -54,8 +54,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {...@@ -54,8 +54,7 @@ fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
54}54}
5555
56test "toPrefixedFileW" {56test "toPrefixedFileW" {
57 if (builtin.os.tag != .windows)57 if (builtin.os.tag != .windows) return error.SkipZigTest;
58 return;
5958
60 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html59 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
61 // Note that these tests do not actually touch the filesystem or care about whether or not60 // Note that these tests do not actually touch the filesystem or care about whether or not
...@@ -237,3 +236,104 @@ test "removeDotDirs" {...@@ -237,3 +236,104 @@ test "removeDotDirs" {
237 try testRemoveDotDirs("a\\b\\..\\", "a\\");236 try testRemoveDotDirs("a\\b\\..\\", "a\\");
238 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");237 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
239}238}
239
240const RTL_PATH_TYPE = enum(c_int) {
241 Unknown,
242 UncAbsolute,
243 DriveAbsolute,
244 DriveRelative,
245 Rooted,
246 Relative,
247 LocalDevice,
248 RootLocalDevice,
249};
250
251pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
252 Path: [*:0]const u16,
253) callconv(.winapi) RTL_PATH_TYPE;
254
255test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
256 if (builtin.os.tag != .windows) return error.SkipZigTest;
257
258 var buf: std.ArrayList(u16) = .empty;
259 defer buf.deinit(std.testing.allocator);
260
261 var wtf8_buf: std.ArrayList(u8) = .empty;
262 defer wtf8_buf.deinit(std.testing.allocator);
263
264 var random = std.Random.DefaultPrng.init(std.testing.random_seed);
265 const rand = random.random();
266
267 for (0..1000) |_| {
268 buf.clearRetainingCapacity();
269 const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
270 wtf8_buf.clearRetainingCapacity();
271 const wtf8_len = std.unicode.calcWtf8Len(path);
272 try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
273 wtf8_buf.items.len = wtf8_len;
274 std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
275
276 const windows_type = RtlDetermineDosPathNameType_U(path);
277 const wtf16_type = windows.getWin32PathType(u16, path);
278 const wtf8_type = windows.getWin32PathType(u8, wtf8_buf.items);
279
280 checkPathType(windows_type, wtf16_type) catch |err| {
281 std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
282 std.debug.print("path bytes:\n", .{});
283 std.debug.dumpHex(std.mem.sliceAsBytes(path));
284 return err;
285 };
286
287 if (wtf16_type != wtf8_type) {
288 std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
289 std.debug.print("wtf-16 path bytes:\n", .{});
290 std.debug.dumpHex(std.mem.sliceAsBytes(path));
291 std.debug.print("wtf-8 path bytes:\n", .{});
292 std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
293 return error.Wtf8Wtf16Mismatch;
294 }
295 }
296}
297
298fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: windows.Win32PathType) !void {
299 const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
300 .unc_absolute => .UncAbsolute,
301 .drive_absolute => .DriveAbsolute,
302 .drive_relative => .DriveRelative,
303 .rooted => .Rooted,
304 .relative => .Relative,
305 .local_device => .LocalDevice,
306 .root_local_device => .RootLocalDevice,
307 };
308 if (windows_type != expected_windows_type) return error.PathTypeMismatch;
309}
310
311fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
312 const Choice = enum {
313 backslash,
314 slash,
315 control,
316 printable,
317 non_ascii,
318 };
319
320 const choices = rand.uintAtMostBiased(u16, 32);
321
322 for (0..choices) |_| {
323 const choice = rand.enumValue(Choice);
324 const code_unit = switch (choice) {
325 .backslash => '\\',
326 .slash => '/',
327 .control => switch (rand.uintAtMostBiased(u8, 0x20)) {
328 0x20 => '\x7F',
329 else => |b| b + 1, // no NUL
330 },
331 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
332 .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
333 };
334 try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
335 }
336
337 try buf.append(allocator, 0);
338 return buf.items[0 .. buf.items.len - 1 :0];
339}
lib/std/zig/WindowsSdk.zig+1-1
...@@ -643,7 +643,7 @@ const MsvcLibDir = struct {...@@ -643,7 +643,7 @@ const MsvcLibDir = struct {
643643
644 if (!std.fs.path.isAbsolute(dll_path)) return error.PathNotFound;644 if (!std.fs.path.isAbsolute(dll_path)) return error.PathNotFound;
645645
646 var path_it = std.fs.path.componentIterator(dll_path) catch return error.PathNotFound;646 var path_it = std.fs.path.componentIterator(dll_path);
647 // the .dll filename647 // the .dll filename
648 _ = path_it.last();648 _ = path_it.last();
649 const root_path = while (path_it.previous()) |dir_component| {649 const root_path = while (path_it.previous()) |dir_component| {
src/main.zig+1-1
...@@ -3883,7 +3883,7 @@ fn createModule(...@@ -3883,7 +3883,7 @@ fn createModule(
3883 if (create_module.sysroot) |root| {3883 if (create_module.sysroot) |root| {
3884 for (create_module.lib_dir_args.items) |lib_dir_arg| {3884 for (create_module.lib_dir_args.items) |lib_dir_arg| {
3885 if (fs.path.isAbsolute(lib_dir_arg)) {3885 if (fs.path.isAbsolute(lib_dir_arg)) {
3886 const stripped_dir = lib_dir_arg[fs.path.diskDesignator(lib_dir_arg).len..];3886 const stripped_dir = lib_dir_arg[fs.path.parsePath(lib_dir_arg).root.len..];
3887 const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir });3887 const full_path = try fs.path.join(arena, &[_][]const u8{ root, stripped_dir });
3888 addLibDirectoryWarn(&create_module.lib_directories, full_path);3888 addLibDirectoryWarn(&create_module.lib_directories, full_path);
3889 } else {3889 } else {
test/standalone/build.zig.zon+3
...@@ -126,6 +126,9 @@...@@ -126,6 +126,9 @@
126 .windows_bat_args = .{126 .windows_bat_args = .{
127 .path = "windows_bat_args",127 .path = "windows_bat_args",
128 },128 },
129 .windows_paths = .{
130 .path = "windows_paths",
131 },
129 .self_exe_symlink = .{132 .self_exe_symlink = .{
130 .path = "self_exe_symlink",133 .path = "self_exe_symlink",
131 },134 },
test/standalone/windows_paths/build.zig created+37
...@@ -0,0 +1,37 @@
1const std = @import("std");
2const builtin = @import("builtin");
3
4pub fn build(b: *std.Build) void {
5 const test_step = b.step("test", "Test it");
6 b.default_step = test_step;
7
8 const optimize: std.builtin.OptimizeMode = .Debug;
9 const target = b.graph.host;
10
11 if (builtin.os.tag != .windows) return;
12
13 const relative = b.addExecutable(.{
14 .name = "relative",
15 .root_module = b.createModule(.{
16 .root_source_file = b.path("relative.zig"),
17 .optimize = optimize,
18 .target = target,
19 }),
20 });
21
22 const main = b.addExecutable(.{
23 .name = "test",
24 .root_module = b.createModule(.{
25 .root_source_file = b.path("test.zig"),
26 .optimize = optimize,
27 .target = target,
28 }),
29 });
30
31 const run = b.addRunArtifact(main);
32 run.addArtifactArg(relative);
33 run.expectExitCode(0);
34 run.skip_foreign_checks = true;
35
36 test_step.dependOn(&run.step);
37}
test/standalone/windows_paths/relative.zig created+19
...@@ -0,0 +1,19 @@
1const std = @import("std");
2
3pub fn main() !void {
4 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
5 defer std.debug.assert(gpa.deinit() == .ok);
6 const allocator = gpa.allocator();
7
8 const args = try std.process.argsAlloc(allocator);
9 defer std.process.argsFree(allocator, args);
10
11 if (args.len < 3) return error.MissingArgs;
12
13 const relative = try std.fs.path.relative(allocator, args[1], args[2]);
14 defer allocator.free(relative);
15
16 var stdout_writer = std.fs.File.stdout().writerStreaming(&.{});
17 const stdout = &stdout_writer.interface;
18 try stdout.writeAll(relative);
19}
test/standalone/windows_paths/test.zig created+131
...@@ -0,0 +1,131 @@
1const std = @import("std");
2
3pub fn main() anyerror!void {
4 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
5 defer arena_state.deinit();
6 const arena = arena_state.allocator();
7
8 const args = try std.process.argsAlloc(arena);
9
10 if (args.len < 2) return error.MissingArgs;
11
12 const exe_path = args[1];
13
14 const cwd_path = try std.process.getCwdAlloc(arena);
15 const parsed_cwd_path = std.fs.path.parsePathWindows(u8, cwd_path);
16
17 if (parsed_cwd_path.kind == .drive_absolute and !std.ascii.isAlphabetic(cwd_path[0])) {
18 // Technically possible, but not worth supporting here
19 return error.NonAlphabeticDriveLetter;
20 }
21
22 const alt_drive_letter = try getAltDriveLetter(cwd_path);
23 const alt_drive_cwd_key = try std.fmt.allocPrint(arena, "={c}:", .{alt_drive_letter});
24 const alt_drive_cwd = try std.fmt.allocPrint(arena, "{c}:\\baz", .{alt_drive_letter});
25 var alt_drive_env_map = std.process.EnvMap.init(arena);
26 try alt_drive_env_map.put(alt_drive_cwd_key, alt_drive_cwd);
27
28 const empty_env = std.process.EnvMap.init(arena);
29
30 {
31 const drive_rel = try std.fmt.allocPrint(arena, "{c}:foo", .{alt_drive_letter});
32 const drive_abs = try std.fmt.allocPrint(arena, "{c}:\\bar", .{alt_drive_letter});
33
34 // With the special =X: environment variable set, drive-relative paths that
35 // don't match the CWD's drive letter are resolved against that env var.
36 try checkRelative(arena, "..\\..\\bar", &.{ exe_path, drive_rel, drive_abs }, null, &alt_drive_env_map);
37 try checkRelative(arena, "..\\baz\\foo", &.{ exe_path, drive_abs, drive_rel }, null, &alt_drive_env_map);
38
39 // Without that environment variable set, drive-relative paths that don't match the
40 // CWD's drive letter are resolved against the root of the drive.
41 try checkRelative(arena, "..\\bar", &.{ exe_path, drive_rel, drive_abs }, null, &empty_env);
42 try checkRelative(arena, "..\\foo", &.{ exe_path, drive_abs, drive_rel }, null, &empty_env);
43
44 // Bare drive-relative path with no components
45 try checkRelative(arena, "bar", &.{ exe_path, drive_rel[0..2], drive_abs }, null, &empty_env);
46 try checkRelative(arena, "..", &.{ exe_path, drive_abs, drive_rel[0..2] }, null, &empty_env);
47
48 // Bare drive-relative path with no components, drive-CWD set
49 try checkRelative(arena, "..\\bar", &.{ exe_path, drive_rel[0..2], drive_abs }, null, &alt_drive_env_map);
50 try checkRelative(arena, "..\\baz", &.{ exe_path, drive_abs, drive_rel[0..2] }, null, &alt_drive_env_map);
51
52 // Bare drive-relative path relative to the CWD should be equivalent if drive-CWD is set
53 try checkRelative(arena, "", &.{ exe_path, alt_drive_cwd, drive_rel[0..2] }, null, &alt_drive_env_map);
54 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], alt_drive_cwd }, null, &alt_drive_env_map);
55
56 // Bare drive-relative should always be equivalent to itself
57 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &alt_drive_env_map);
58 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &alt_drive_env_map);
59 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &empty_env);
60 try checkRelative(arena, "", &.{ exe_path, drive_rel[0..2], drive_rel[0..2] }, null, &empty_env);
61 }
62
63 if (parsed_cwd_path.kind == .unc_absolute) {
64 const drive_abs_path = try std.fmt.allocPrint(arena, "{c}:\\foo\\bar", .{alt_drive_letter});
65
66 {
67 try checkRelative(arena, drive_abs_path, &.{ exe_path, cwd_path, drive_abs_path }, null, &empty_env);
68 try checkRelative(arena, cwd_path, &.{ exe_path, drive_abs_path, cwd_path }, null, &empty_env);
69 }
70 } else if (parsed_cwd_path.kind == .drive_absolute) {
71 const cur_drive_letter = parsed_cwd_path.root[0];
72 const path_beyond_root = cwd_path[3..];
73 const unc_cwd = try std.fmt.allocPrint(arena, "\\\\127.0.0.1\\{c}$\\{s}", .{ cur_drive_letter, path_beyond_root });
74
75 {
76 try checkRelative(arena, cwd_path, &.{ exe_path, unc_cwd, cwd_path }, null, &empty_env);
77 try checkRelative(arena, unc_cwd, &.{ exe_path, cwd_path, unc_cwd }, null, &empty_env);
78 }
79 {
80 const drive_abs = cwd_path;
81 const drive_rel = parsed_cwd_path.root[0..2];
82 try checkRelative(arena, "", &.{ exe_path, drive_abs, drive_rel }, null, &empty_env);
83 try checkRelative(arena, "", &.{ exe_path, drive_rel, drive_abs }, null, &empty_env);
84 }
85 } else {
86 return error.UnexpectedPathType;
87 }
88}
89
90fn checkRelative(
91 allocator: std.mem.Allocator,
92 expected_stdout: []const u8,
93 argv: []const []const u8,
94 cwd: ?[]const u8,
95 env_map: ?*const std.process.EnvMap,
96) !void {
97 const result = try std.process.Child.run(.{
98 .allocator = allocator,
99 .argv = argv,
100 .cwd = cwd,
101 .env_map = env_map,
102 });
103 defer allocator.free(result.stdout);
104 defer allocator.free(result.stderr);
105
106 try std.testing.expectEqualStrings("", result.stderr);
107 try std.testing.expectEqualStrings(expected_stdout, result.stdout);
108}
109
110fn getAltDriveLetter(path: []const u8) !u8 {
111 const parsed = std.fs.path.parsePathWindows(u8, path);
112 return switch (parsed.kind) {
113 .drive_absolute => {
114 const cur_drive_letter = parsed.root[0];
115 const next_drive_letter_index = (std.ascii.toUpper(cur_drive_letter) - 'A' + 1) % 26;
116 const next_drive_letter = next_drive_letter_index + 'A';
117 return next_drive_letter;
118 },
119 .unc_absolute => {
120 return 'C';
121 },
122 else => return error.UnexpectedPathType,
123 };
124}
125
126test getAltDriveLetter {
127 try std.testing.expectEqual('D', try getAltDriveLetter("C:\\"));
128 try std.testing.expectEqual('B', try getAltDriveLetter("a:\\"));
129 try std.testing.expectEqual('A', try getAltDriveLetter("Z:\\"));
130 try std.testing.expectEqual('C', try getAltDriveLetter("\\\\foo\\bar"));
131}