authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-05-22 17:58:59-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2021-05-22 17:58:59-04:00
log7cd9b30e0aee01eec148b867e2f949e7449e258d
treef3a9612c95ddac9bb1a6e5b743d91d2473dadd84
parent79dee75b1ccd8f3f595aad0d4150851cff58f691
parentb0116afd8adbc73d820fee418ec74104f43bff6e
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #7664 from marler8997/fixWindowsPaths

implement nt path conversion for windows

6 files changed, 248 insertions(+), 22 deletions(-)

lib/std/fs.zig-9
......@@ -1365,15 +1365,6 @@ pub const Dir = struct {
13651365 .SecurityDescriptor = null,
13661366 .SecurityQualityOfService = null,
13671367 };
1368 if (sub_path_w[0] == '.' and sub_path_w[1] == 0) {
1369 // Windows does not recognize this, but it does work with empty string.
1370 nt_name.Length = 0;
1371 }
1372 if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) {
1373 // If you're looking to contribute to zig and fix this, see here for an example of how to
1374 // implement this: https://git.midipix.org/ntapi/tree/src/fs/ntapi_tt_open_physical_parent_directory.c
1375 @panic("TODO opening '..' with a relative directory handle is not yet implemented on Windows");
1376 }
13771368 const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0;
13781369 var io: w.IO_STATUS_BLOCK = undefined;
13791370 const rc = w.ntdll.NtCreateFile(
lib/std/fs/test.zig+17-1
......@@ -79,7 +79,23 @@ test "openDirAbsolute" {
7979 break :blk try fs.realpathAlloc(&arena.allocator, relative_path);
8080 };
8181
82 var dir = try fs.openDirAbsolute(base_path, .{});
82 {
83 var dir = try fs.openDirAbsolute(base_path, .{});
84 defer dir.close();
85 }
86
87 for ([_][]const u8{ ".", ".." }) |sub_path| {
88 const dir_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, sub_path });
89 defer arena.allocator.free(dir_path);
90 var dir = try fs.openDirAbsolute(dir_path, .{});
91 defer dir.close();
92 }
93}
94
95test "openDir cwd parent .." {
96 if (builtin.os.tag == .wasi) return error.SkipZigTest;
97
98 var dir = try fs.cwd().openDir("..", .{});
8399 defer dir.close();
84100}
85101
lib/std/mem.zig+47
......@@ -2120,6 +2120,53 @@ test "replace" {
21202120 try testing.expectEqualStrings(expected, output[0..expected.len]);
21212121}
21222122
2123/// Replace all occurences of `needle` with `replacement`.
2124pub fn replaceScalar(comptime T: type, slice: []T, needle: T, replacement: T) void {
2125 for (slice) |e, i| {
2126 if (e == needle) {
2127 slice[i] = replacement;
2128 }
2129 }
2130}
2131
2132/// Collapse consecutive duplicate elements into one entry.
2133pub fn collapseRepeatsLen(comptime T: type, slice: []T, elem: T) usize {
2134 if (slice.len == 0) return 0;
2135 var write_idx: usize = 1;
2136 var read_idx: usize = 1;
2137 while (read_idx < slice.len) : (read_idx += 1) {
2138 if (slice[read_idx - 1] != elem or slice[read_idx] != elem) {
2139 slice[write_idx] = slice[read_idx];
2140 write_idx += 1;
2141 }
2142 }
2143 return write_idx;
2144}
2145
2146/// Collapse consecutive duplicate elements into one entry.
2147pub fn collapseRepeats(comptime T: type, slice: []T, elem: T) []T {
2148 return slice[0 .. collapseRepeatsLen(T, slice, elem)];
2149}
2150
2151fn testCollapseRepeats(str: []const u8, elem: u8, expected: []const u8) !void {
2152 const mutable = try std.testing.allocator.dupe(u8, str);
2153 defer std.testing.allocator.free(mutable);
2154 try testing.expect(std.mem.eql(u8, collapseRepeats(u8, mutable, elem), expected));
2155}
2156test "collapseRepeats" {
2157 try testCollapseRepeats("", '/', "");
2158 try testCollapseRepeats("a", '/', "a");
2159 try testCollapseRepeats("/", '/', "/");
2160 try testCollapseRepeats("//", '/', "/");
2161 try testCollapseRepeats("/a", '/', "/a");
2162 try testCollapseRepeats("//a", '/', "/a");
2163 try testCollapseRepeats("a/", '/', "a/");
2164 try testCollapseRepeats("a//", '/', "a/");
2165 try testCollapseRepeats("a/a", '/', "a/a");
2166 try testCollapseRepeats("a//a", '/', "a/a");
2167 try testCollapseRepeats("//a///a////", '/', "/a/a/");
2168}
2169
21232170/// Calculate the size needed in an output buffer to perform a replacement.
21242171/// The needle must not be empty.
21252172pub fn replacementSize(comptime T: type, input: []const T, needle: []const T, replacement: []const T) usize {
lib/std/os/windows.zig+107-12
......@@ -1723,6 +1723,81 @@ pub const PathSpace = struct {
17231723 }
17241724};
17251725
1726/// The error type for `removeDotDirsSanitized`
1727pub const RemoveDotDirsError = error{TooManyParentDirs};
1728
1729/// Removes '.' and '..' path components from a "sanitized relative path".
1730/// A "sanitized path" is one where:
1731/// 1) all forward slashes have been replaced with back slashes
1732/// 2) all repeating back slashes have been collapsed
1733/// 3) the path is a relative one (does not start with a back slash)
1734pub fn removeDotDirsSanitized(comptime T: type, path: []T) RemoveDotDirsError!usize {
1735 std.debug.assert(path.len == 0 or path[0] != '\\');
1736
1737 var write_idx: usize = 0;
1738 var read_idx: usize = 0;
1739 while (read_idx < path.len) {
1740 if (path[read_idx] == '.') {
1741 if (read_idx + 1 == path.len)
1742 return write_idx;
1743
1744 const after_dot = path[read_idx + 1];
1745 if (after_dot == '\\') {
1746 read_idx += 2;
1747 continue;
1748 }
1749 if (after_dot == '.' and (read_idx + 2 == path.len or path[read_idx + 2] == '\\')) {
1750 if (write_idx == 0) return error.TooManyParentDirs;
1751 std.debug.assert(write_idx >= 2);
1752 write_idx -= 1;
1753 while (true) {
1754 write_idx -= 1;
1755 if (write_idx == 0) break;
1756 if (path[write_idx] == '\\') {
1757 write_idx += 1;
1758 break;
1759 }
1760 }
1761 if (read_idx + 2 == path.len)
1762 return write_idx;
1763 read_idx += 3;
1764 continue;
1765 }
1766 }
1767
1768 // skip to the next path separator
1769 while (true) : (read_idx += 1) {
1770 if (read_idx == path.len)
1771 return write_idx;
1772 path[write_idx] = path[read_idx];
1773 write_idx += 1;
1774 if (path[read_idx] == '\\')
1775 break;
1776 }
1777 read_idx += 1;
1778 }
1779 return write_idx;
1780}
1781
1782/// Normalizes a Windows path with the following steps:
1783/// 1) convert all forward slashes to back slashes
1784/// 2) collapse duplicate back slashes
1785/// 3) remove '.' and '..' directory parts
1786/// Returns the length of the new path.
1787pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize {
1788 mem.replaceScalar(T, path, '/', '\\');
1789 const new_len = mem.collapseRepeatsLen(T, path, '\\');
1790
1791 const prefix_len: usize = init: {
1792 if (new_len >= 1 and path[0] == '\\') break :init 1;
1793 if (new_len >= 2 and path[1] == ':')
1794 break :init if (new_len >= 3 and path[2] == '\\') @as(usize, 3) else @as(usize, 2);
1795 break :init 0;
1796 };
1797
1798 return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]);
1799}
1800
17261801/// Same as `sliceToPrefixedFileW` but accepts a pointer
17271802/// to a null-terminated path.
17281803pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace {
......@@ -1742,28 +1817,42 @@ pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace {
17421817 else => {},
17431818 }
17441819 }
1820 const prefix_u16 = [_]u16{ '\\', '?', '?', '\\' };
17451821 const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: {
1746 const prefix_u16 = [_]u16{ '\\', '?', '?', '\\' };
17471822 mem.copy(u16, path_space.data[0..], prefix_u16[0..]);
17481823 break :blk prefix_u16.len;
17491824 };
17501825 path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s);
17511826 if (path_space.len > path_space.data.len) return error.NameTooLong;
1752 // > File I/O functions in the Windows API convert "/" to "\" as part of
1753 // > converting the name to an NT-style name, except when using the "\\?\"
1754 // > prefix as detailed in the following sections.
1755 // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
1756 // Because we want the larger maximum path length for absolute paths, we
1757 // convert forward slashes to backward slashes here.
1758 for (path_space.data[0..path_space.len]) |*elem| {
1759 if (elem.* == '/') {
1760 elem.* = '\\';
1761 }
1762 }
1827 path_space.len = start_index + (normalizePath(u16, path_space.data[start_index..path_space.len]) catch |err| switch (err) {
1828 error.TooManyParentDirs => {
1829 if (!std.fs.path.isAbsolute(s)) {
1830 var temp_path: PathSpace = undefined;
1831 temp_path.len = try std.unicode.utf8ToUtf16Le(&temp_path.data, s);
1832 std.debug.assert(temp_path.len == path_space.len);
1833 temp_path.data[path_space.len] = 0;
1834 path_space.len = prefix_u16.len + try getFullPathNameW(&temp_path.data, path_space.data[prefix_u16.len..]);
1835 mem.copy(u16, &path_space.data, &prefix_u16);
1836 std.debug.assert(path_space.data[path_space.len] == 0);
1837 return path_space;
1838 }
1839 return error.BadPathName;
1840 },
1841 });
17631842 path_space.data[path_space.len] = 0;
17641843 return path_space;
17651844}
17661845
1846fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
1847 const result= kernel32.GetFullPathNameW(path, @intCast(u32, out.len), std.meta.assumeSentinel(out.ptr, 0), null);
1848 if (result == 0) {
1849 switch (kernel32.GetLastError()) {
1850 else => |err| return unexpectedError(err),
1851 }
1852 }
1853 return result;
1854}
1855
17671856/// Assumes an absolute path.
17681857pub fn wToPrefixedFileW(s: []const u16) !PathSpace {
17691858 // TODO https://github.com/ziglang/zig/issues/2765
......@@ -1864,3 +1953,9 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError {
18641953 }
18651954 return error.Unexpected;
18661955}
1956
1957test "" {
1958 if (builtin.os.tag == .windows) {
1959 _ = @import("windows/test.zig");
1960 }
1961}
lib/std/os/windows/kernel32.zig+7
......@@ -136,6 +136,13 @@ pub extern "kernel32" fn GetFinalPathNameByHandleW(
136136 dwFlags: DWORD,
137137) callconv(WINAPI) DWORD;
138138
139pub extern "kernel32" fn GetFullPathNameW(
140 lpFileName: [*:0]const u16,
141 nBufferLength: u32,
142 lpBuffer: ?[*:0]u16,
143 lpFilePart: ?*?[*:0]u16,
144) callconv(@import("std").os.windows.WINAPI) u32;
145
139146pub extern "kernel32" fn GetOverlappedResult(hFile: HANDLE, lpOverlapped: *OVERLAPPED, lpNumberOfBytesTransferred: *DWORD, bWait: BOOL) callconv(WINAPI) BOOL;
140147
141148pub extern "kernel32" fn GetProcessHeap() callconv(WINAPI) ?HANDLE;
lib/std/os/windows/test.zig created+70
......@@ -0,0 +1,70 @@
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2015-2020 Zig Contributors
3// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
4// The MIT license requires this copyright notice to be included in all copies
5// and substantial portions of the software.
6const std = @import("../../std.zig");
7const builtin = @import("builtin");
8const windows = std.os.windows;
9const mem = std.mem;
10const testing = std.testing;
11const expect = testing.expect;
12
13fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
14 const mutable = try testing.allocator.dupe(u8, str);
15 defer testing.allocator.free(mutable);
16 const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
17 try testing.expect(mem.eql(u8, actual, expected));
18}
19fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
20 const mutable = try testing.allocator.dupe(u8, str);
21 defer testing.allocator.free(mutable);
22 try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
23}
24test "removeDotDirs" {
25 try testRemoveDotDirs("", "");
26 try testRemoveDotDirs(".", "");
27 try testRemoveDotDirs(".\\", "");
28 try testRemoveDotDirs(".\\.", "");
29 try testRemoveDotDirs(".\\.\\", "");
30 try testRemoveDotDirs(".\\.\\.", "");
31
32 try testRemoveDotDirs("a", "a");
33 try testRemoveDotDirs("a\\", "a\\");
34 try testRemoveDotDirs("a\\b", "a\\b");
35 try testRemoveDotDirs("a\\.", "a\\");
36 try testRemoveDotDirs("a\\b\\.", "a\\b\\");
37 try testRemoveDotDirs("a\\.\\b", "a\\b");
38
39 try testRemoveDotDirs(".a", ".a");
40 try testRemoveDotDirs(".a\\", ".a\\");
41 try testRemoveDotDirs(".a\\.b", ".a\\.b");
42 try testRemoveDotDirs(".a\\.", ".a\\");
43 try testRemoveDotDirs(".a\\.\\.", ".a\\");
44 try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
45 try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
46
47 try testRemoveDotDirsError(error.TooManyParentDirs, "..");
48 try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
49 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
50 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
51
52 try testRemoveDotDirs("a\\..", "");
53 try testRemoveDotDirs("a\\..\\", "");
54 try testRemoveDotDirs("a\\..\\.", "");
55 try testRemoveDotDirs("a\\..\\.\\", "");
56 try testRemoveDotDirs("a\\..\\.\\.", "");
57 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
58
59 try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
60 try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
61 try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
62 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
63 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
64 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
65 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
66 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
67
68 try testRemoveDotDirs("a\\b\\..\\", "a\\");
69 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
70}