diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 6880940c0371..943df39730ba 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1168,8 +1168,9 @@ pub const Dir = struct { /// Asserts that the path parameter has no null bytes. pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.sliceToPrefixedFileW(sub_path); - return self.openDirW(sub_path_w.span().ptr, args); + const nt_path = try os.windows.NtPath.init((try os.windows.utf8ToWPathSpace(sub_path)).span()); + defer nt_path.deinit(); + return self.openDirWindows(std.fs.path.isAbsoluteWindows(sub_path), nt_path.str, args); } else if (builtin.os.tag == .wasi) { return self.openDirWasi(sub_path, args); } else { @@ -1223,8 +1224,7 @@ pub const Dir = struct { /// Same as `openDir` except the parameter is null-terminated. pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions) OpenError!Dir { if (builtin.os.tag == .windows) { - const sub_path_w = try os.windows.cStrToPrefixedFileW(sub_path_c); - return self.openDirW(sub_path_w.span().ptr, args); + return self.openDir(std.mem.spanZ(sub_path_c), args); } const symlink_flags: u32 = if (args.no_follow) os.O_NOFOLLOW else 0x0; if (!args.iterate) { @@ -1235,15 +1235,13 @@ pub const Dir = struct { } } - /// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed. + /// Same as `openDir` except the path parameter is WTF-16 encoded. /// This function asserts the target OS is Windows. pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions) OpenError!Dir { const w = os.windows; - // TODO remove some of these flags if args.access_sub_paths is false - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE; - const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; - return self.openDirAccessMaskW(sub_path_w, flags, args.no_follow); + const nt_path = try w.NtPath.init(sub_path_w); + defer nt_path.deinit(); + return self.openDirWindows(std.fs.path.isAbsoluteWindowsW(sub_path_w), nt_path.str, args); } /// `flags` must contain `os.O_DIRECTORY`. @@ -1264,37 +1262,28 @@ pub const Dir = struct { return Dir{ .fd = fd }; } - fn openDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, no_follow: bool) OpenError!Dir { + fn openDirWindows(self: Dir, is_absolute: bool, nt_path: os.windows.UNICODE_STRING, args: OpenDirOptions) OpenError!Dir { const w = os.windows; + // TODO remove some of these flags if args.access_sub_paths is false + const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE; + const access_mask: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; + var result = Dir{ .fd = undefined, }; - const path_len_bytes = @intCast(u16, mem.lenZ(sub_path_w) * 2); - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)), - }; + var adjusted_nt_path = if (is_absolute) nt_path else try w.toRelativeNtPath(nt_path); var attr = w.OBJECT_ATTRIBUTES{ .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, + .RootDirectory = if (is_absolute) null else self.fd, .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, + .ObjectName = &adjusted_nt_path, .SecurityDescriptor = null, .SecurityQualityOfService = null, }; - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - // Windows does not recognize this, but it does work with empty string. - nt_name.Length = 0; - } - if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { - // If you're looking to contribute to zig and fix this, see here for an example of how to - // implement this: https://git.midipix.org/ntapi/tree/src/fs/ntapi_tt_open_physical_parent_directory.c - @panic("TODO opening '..' with a relative directory handle is not yet implemented on Windows"); - } - const open_reparse_point: w.DWORD = if (no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; + const open_reparse_point: w.DWORD = if (args.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; var io: w.IO_STATUS_BLOCK = undefined; const rc = w.ntdll.NtCreateFile( &result.fd, diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index f4d50ca9582d..fd3aaf156df5 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -79,8 +79,17 @@ test "openDirAbsolute" { break :blk try fs.realpathAlloc(&arena.allocator, relative_path); }; - var dir = try fs.openDirAbsolute(base_path, .{}); - defer dir.close(); + { + var dir = try fs.openDirAbsolute(base_path, .{}); + defer dir.close(); + } + + for ([_][]const u8 { ".", ".." }) |sub_path| { + const dir_path = try fs.path.join(&arena.allocator, &[_][]const u8 { base_path, sub_path }); + defer arena.allocator.free(dir_path); + var dir = try fs.openDirAbsolute(dir_path, .{}); + defer dir.close(); + } } test "readLinkAbsolute" { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index b994720ce995..cc9bb093f4e2 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1528,6 +1528,16 @@ pub const PathSpace = struct { } }; +/// Decode a utf8 encoded path to a "wide character" path. Returns the path by value via PathSpace +/// which can accomodate paths up to `PATH_MAX_WIDE` characters long. +pub fn utf8ToWPathSpace(utf8_path: []const u8) !PathSpace { + // TODO https://github.com/ziglang/zig/issues/2765 + var path_space : PathSpace = undefined; + path_space.len = try std.unicode.utf8ToUtf16Le(&path_space.data, utf8_path); + path_space.data[path_space.len] = 0; + return path_space; +} + /// Same as `sliceToPrefixedFileW` but accepts a pointer /// to a null-terminated path. pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace { @@ -1569,6 +1579,48 @@ pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace { return path_space; } +/// Convert `dos_path` to an NT path that can be passed to Nt* functions like NtCreateFile. +pub const NtPath = struct { + str: UNICODE_STRING, + pub fn init(dos_path: [*:0]const u16) !NtPath { + var str : UNICODE_STRING = undefined; + const status = ntdll.RtlDosPathNameToNtPathName_U_WithStatus(dos_path, &str, null, null); + if (status != .SUCCESS) return unexpectedStatus(status); + return NtPath { .str = str }; + } + // TODO: not sure if the path is guaranteed to be NULL-terminted + pub fn span(self: NtPath) []const u16 { + return self.str.Buffer[0 .. self.str.Length / 2]; + } + pub fn deinit(self: NtPath) void { + if (TRUE != kernel32.HeapFree(kernel32.GetProcessHeap().?, 0, self.str.Buffer)) { + std.debug.panic("in NtPath, HeapFree failed with {}\n", .{kernel32.GetLastError()}); + } + } +}; + +/// return the relative path of `nt_path` based on CWD +pub fn toRelativeNtPath(nt_path: UNICODE_STRING) !UNICODE_STRING { + const cwd_nt_path = try NtPath.init(&[_:0]u16 {'.'}); + defer cwd_nt_path.deinit(); + const cwd_span = cwd_nt_path.span(); + std.debug.assert(mem.startsWith(u16, unicodeSpan(nt_path), cwd_span)); + std.debug.assert(nt_path.Buffer[cwd_span.len] == '\\'); + return unicodeSubstring(nt_path, @intCast(c_ushort, cwd_span.len + 1)); +} + +pub fn unicodeSpan(str: UNICODE_STRING) []u16 { + return str.Buffer[0..str.Length/2]; +} +pub fn unicodeSubstring(str: UNICODE_STRING, char_offset: c_ushort) UNICODE_STRING { + std.debug.assert(char_offset * 2 <= str.Length); + return .{ + .Buffer = str.Buffer + char_offset, + .MaximumLength = str.MaximumLength - (char_offset*2), + .Length = str.Length - (char_offset*2), + }; +} + /// Assumes an absolute path. pub fn wToPrefixedFileW(s: []const u16) !PathSpace { // TODO https://github.com/ziglang/zig/issues/2765 @@ -1637,3 +1689,9 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError { } return error.Unexpected; } + +test "" { + if (builtin.os.tag == .windows) { + _ = @import("windows/test.zig"); + } +} diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index fc485f87f299..bf538fb50a6a 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -113,3 +113,10 @@ pub extern "NtDll" fn NtWaitForKeyedEvent( ) callconv(WINAPI) NTSTATUS; pub extern "NtDll" fn RtlSetCurrentDirectory_U(PathName: *UNICODE_STRING) callconv(WINAPI) NTSTATUS; + +pub extern "NtDll" fn RtlDosPathNameToNtPathName_U_WithStatus( + DosFileName: [*:0]const WCHAR, + NtFileName: *UNICODE_STRING, + FilePath: ?*[*:0]WCHAR, + cd: ?*CURDIR, +) callconv(WINAPI) NTSTATUS; diff --git a/lib/std/os/windows/test.zig b/lib/std/os/windows/test.zig new file mode 100644 index 000000000000..67ea2a9101ca --- /dev/null +++ b/lib/std/os/windows/test.zig @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2015-2020 Zig Contributors +// This file is part of [zig](https://ziglang.org/), which is MIT licensed. +// The MIT license requires this copyright notice to be included in all copies +// and substantial portions of the software. +const std = @import("../../std.zig"); +const builtin = @import("builtin"); +const windows = std.os.windows; +const mem = std.mem; +const testing = std.testing; +const expect = testing.expect; + +fn testNtPath(input: []const u8, expected: []const u8) !void { + const input_w = try std.unicode.utf8ToUtf16LeWithNull(testing.allocator, input); + defer testing.allocator.free(input_w); + const expected_w = try std.unicode.utf8ToUtf16LeWithNull(testing.allocator, expected); + defer testing.allocator.free(expected_w); + + const nt_path = try windows.NtPath.init(input_w); + defer nt_path.deinit(); + const relative_path = try windows.toRelativeNtPath(nt_path.str); + expect(mem.eql(u16, windows.unicodeSpan(relative_path), expected_w)); +} + +test "NtPath" { + try testNtPath("a", "a"); + try testNtPath("a\\b", "a\\b"); + try testNtPath("a\\.\\b", "a\\b"); + try testNtPath("a\\..\\b", "b"); +}