Skip to content

Fix paths on Windows #7537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 18 additions & 29 deletions lib/std/fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we delete this function now? The idea of this function is that it's slightly more efficient if you already have wide strings you want to pass, but if we have to do the conversion anyway, there's no reason for this function to exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we delete it, then applications that have a u16 string will now need to do 2 conversions, one to u8 and then the second internal conversion.

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`.
Expand All @@ -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,
Expand Down
13 changes: 11 additions & 2 deletions lib/std/fs/test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
58 changes: 58 additions & 0 deletions lib/std/os/windows.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Comment on lines +1587 to +1588
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces unnecessary heap allocation, causing OutOfMemory to be added to the OpenError set. We can do better. Check the wine implementation of RtlDosPathNameToNtPathName_U_WithStatus for inspiration. Opening a file should not do heap allocation for the path name.

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
Expand Down Expand Up @@ -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");
}
}
7 changes: 7 additions & 0 deletions lib/std/os/windows/ntdll.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
30 changes: 30 additions & 0 deletions lib/std/os/windows/test.zig
Original file line number Diff line number Diff line change
@@ -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");
}