Commit 3d41d3fb6e

Jakub Konka <kubkon@jakubkonka.com>
2020-07-21 19:22:22
Draft out ReadLinkW using NT primitives
1 parent 4887350
Changed files (2)
lib/std/os/windows.zig
@@ -731,6 +731,117 @@ pub fn CreateSymbolicLinkW(
     _ = try DeviceIoControl(symlink_handle, FSCTL_SET_REPARSE_POINT, buffer[0..buf_len], null, null);
 }
 
+pub const ReadLinkError = error{
+    FileNotFound,
+    AccessDenied,
+    Unexpected,
+    NameTooLong,
+    UnsupportedReparsePointType,
+    InvalidUtf8,
+    BadPathName,
+};
+
+pub fn ReadLink(
+    dir: ?HANDLE,
+    sub_path: []const u8,
+    out_buffer: []u8,
+) ReadLinkError![]u8 {
+    const sub_path_w = try sliceToPrefixedFileW(sub_path);
+    return ReadLinkW(dir, sub_path_w.span().ptr, out_buffer);
+}
+
+pub fn ReadLinkW(dir: ?HANDLE, sub_path_w: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 {
+    const path_len_bytes = math.cast(u16, mem.lenZ(sub_path_w) * 2) catch |err| switch (err) {
+        error.Overflow => return error.NameTooLong,
+    };
+    var nt_name = UNICODE_STRING{
+        .Length = path_len_bytes,
+        .MaximumLength = path_len_bytes,
+        .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)),
+    };
+
+    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;
+    }
+
+    var attr = OBJECT_ATTRIBUTES{
+        .Length = @sizeOf(OBJECT_ATTRIBUTES),
+        .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else dir,
+        .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
+        .ObjectName = &nt_name,
+        .SecurityDescriptor = null,
+        .SecurityQualityOfService = null,
+    };
+    var io: IO_STATUS_BLOCK = undefined;
+    var result_handle: HANDLE = undefined;
+    const rc = ntdll.NtCreateFile(
+        &result_handle,
+        FILE_READ_ATTRIBUTES,
+        &attr,
+        &io,
+        null,
+        FILE_ATTRIBUTE_NORMAL,
+        FILE_SHARE_READ,
+        FILE_OPEN,
+        FILE_OPEN_REPARSE_POINT,
+        null,
+        0,
+    );
+    switch (rc) {
+        .SUCCESS => {},
+        .OBJECT_NAME_INVALID => unreachable,
+        .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
+        .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
+        .NO_MEDIA_IN_DEVICE => return error.FileNotFound,
+        .INVALID_PARAMETER => unreachable,
+        .SHARING_VIOLATION => return error.AccessDenied,
+        .ACCESS_DENIED => return error.AccessDenied,
+        .PIPE_BUSY => return error.AccessDenied,
+        .OBJECT_PATH_SYNTAX_BAD => unreachable,
+        .OBJECT_NAME_COLLISION => unreachable,
+        .FILE_IS_A_DIRECTORY => unreachable,
+        else => return unexpectedStatus(rc),
+    }
+    defer CloseHandle(result_handle);
+
+    var reparse_buf: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
+    _ = try DeviceIoControl(result_handle, FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null);
+
+    const reparse_struct = @ptrCast(*const REPARSE_DATA_BUFFER, @alignCast(@alignOf(REPARSE_DATA_BUFFER), &reparse_buf[0]));
+    switch (reparse_struct.ReparseTag) {
+        IO_REPARSE_TAG_SYMLINK => {
+            const buf = @ptrCast(*const SYMBOLIC_LINK_REPARSE_BUFFER, @alignCast(@alignOf(SYMBOLIC_LINK_REPARSE_BUFFER), &reparse_struct.DataBuffer[0]));
+            const offset = buf.SubstituteNameOffset >> 1;
+            const len = buf.SubstituteNameLength >> 1;
+            const path_buf = @as([*]const u16, &buf.PathBuffer);
+            const is_relative = buf.Flags & SYMLINK_FLAG_RELATIVE != 0;
+            return parseReadlinkPath(path_buf[offset .. offset + len], is_relative, out_buffer);
+        },
+        IO_REPARSE_TAG_MOUNT_POINT => {
+            const buf = @ptrCast(*const MOUNT_POINT_REPARSE_BUFFER, @alignCast(@alignOf(MOUNT_POINT_REPARSE_BUFFER), &reparse_struct.DataBuffer[0]));
+            const offset = buf.SubstituteNameOffset >> 1;
+            const len = buf.SubstituteNameLength >> 1;
+            const path_buf = @as([*]const u16, &buf.PathBuffer);
+            return parseReadlinkPath(path_buf[offset .. offset + len], false, out_buffer);
+        },
+        else => |value| {
+            std.debug.warn("unsupported symlink type: {}", .{value});
+            return error.UnsupportedReparsePointType;
+        },
+    }
+}
+
+fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
+    const prefix = [_]u16{ '\\', '?', '?', '\\' };
+    var start_index: usize = 0;
+    if (!is_relative and std.mem.startsWith(u16, path, &prefix)) {
+        start_index = prefix.len;
+    }
+    const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable;
+    return out_buffer[0..out_len];
+}
+
 pub const DeleteFileError = error{
     FileNotFound,
     AccessDenied,
@@ -1343,21 +1454,6 @@ pub const PathSpace = struct {
     pub fn span(self: PathSpace) [:0]const u16 {
         return self.data[0..self.len :0];
     }
-
-    fn ensureNtStyle(self: *PathSpace) void {
-        // > File I/O functions in the Windows API convert "/" to "\" as part of
-        // > converting the name to an NT-style name, except when using the "\\?\"
-        // > prefix as detailed in the following sections.
-        // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
-        // Because we want the larger maximum path length for absolute paths, we
-        // convert forward slashes to backward slashes here.
-        for (self.data[0..self.len]) |*elem| {
-            if (elem.* == '/') {
-                elem.* = '\\';
-            }
-        }
-        self.data[self.len] = 0;
-    }
 };
 
 /// Same as `sliceToPrefixedFileW` but accepts a pointer
@@ -1366,50 +1462,14 @@ pub fn cStrToPrefixedFileW(s: [*:0]const u8) !PathSpace {
     return sliceToPrefixedFileW(mem.spanZ(s));
 }
 
-/// Same as `sliceToWin32PrefixedFileW` but accepts a pointer
-/// to a null-terminated path.
-pub fn cStrToWin32PrefixedFileW(s: [*:0]const u8) !PathSpace {
-    return sliceToWin32PrefixedFileW(mem.spanZ(s));
-}
-
 /// Converts the path `s` to WTF16, null-terminated. If the path is absolute,
 /// it will get NT-style prefix `\??\` prepended automatically. For prepending
 /// Win32-style prefix, see `sliceToWin32PrefixedFileW` instead.
 pub fn sliceToPrefixedFileW(s: []const u8) !PathSpace {
-    return sliceToPrefixedFileWInternal(s, PathPrefix.Nt);
-}
-
-/// Converts the path `s` to WTF16, null-terminated. If the path is absolute,
-/// it will get Win32-style extended prefix `\\?\` prepended automatically. For prepending
-/// NT-style prefix, see `sliceToPrefixedFileW` instead.
-pub fn sliceToWin32PrefixedFileW(s: []const u8) !PathSpace {
-    return sliceToPrefixedFileWInternal(s, PathPrefix.Win32);
-}
-
-const PathPrefix = enum {
-    Win32,
-    Nt,
-
-    fn toUtf8(self: PathPrefix) []const u8 {
-        return switch (self) {
-            .Win32 => "\\\\?\\",
-            .Nt => "\\??\\",
-        };
-    }
-
-    fn toUtf16(self: PathPrefix) []const u16 {
-        return switch (self) {
-            .Win32 => &[_]u16{ '\\', '\\', '?', '\\' },
-            .Nt => &[_]u16{ '\\', '?', '?', '\\' },
-        };
-    }
-};
-
-fn sliceToPrefixedFileWInternal(s: []const u8, prefix: PathPrefix) !PathSpace {
     // TODO https://github.com/ziglang/zig/issues/2765
     var path_space: PathSpace = undefined;
-    const prefix_utf8 = prefix.toUtf8();
-    const prefix_index: usize = if (mem.startsWith(u8, s, prefix_utf8)) prefix_utf8.len else 0;
+    const prefix = "\\??\\";
+    const prefix_index: usize = if (mem.startsWith(u8, s, prefix)) prefix.len else 0;
     for (s[prefix_index..]) |byte| {
         switch (byte) {
             '*', '?', '"', '<', '>', '|' => return error.BadPathName,
@@ -1417,13 +1477,24 @@ fn sliceToPrefixedFileWInternal(s: []const u8, prefix: PathPrefix) !PathSpace {
         }
     }
     const start_index = if (prefix_index > 0 or !std.fs.path.isAbsolute(s)) 0 else blk: {
-        const prefix_utf16 = prefix.toUtf16();
-        mem.copy(u16, path_space.data[0..], prefix_utf16);
-        break :blk prefix_utf16.len;
+        const prefix_u16 = [_]u16{ '\\', '?', '?', '\\' };
+        mem.copy(u16, path_space.data[0..], prefix_u16[0..]);
+        break :blk prefix_u16.len;
     };
     path_space.len = start_index + try std.unicode.utf8ToUtf16Le(path_space.data[start_index..], s);
     if (path_space.len > path_space.data.len) return error.NameTooLong;
-    path_space.ensureNtStyle();
+    // > File I/O functions in the Windows API convert "/" to "\" as part of
+    // > converting the name to an NT-style name, except when using the "\\?\"
+    // > prefix as detailed in the following sections.
+    // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
+    // Because we want the larger maximum path length for absolute paths, we
+    // convert forward slashes to backward slashes here.
+    for (path_space.data[0..path_space.len]) |*elem| {
+        if (elem.* == '/') {
+            elem.* = '\\';
+        }
+    }
+    path_space.data[path_space.len] = 0;
     return path_space;
 }
 
@@ -1440,7 +1511,18 @@ pub fn wToPrefixedFileW(s: []const u16) !PathSpace {
     path_space.len = start_index + s.len;
     if (path_space.len > path_space.data.len) return error.NameTooLong;
     mem.copy(u16, path_space.data[start_index..], s);
-    path_space.ensureNtStyle();
+    // > File I/O functions in the Windows API convert "/" to "\" as part of
+    // > converting the name to an NT-style name, except when using the "\\?\"
+    // > prefix as detailed in the following sections.
+    // from https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
+    // Because we want the larger maximum path length for absolute paths, we
+    // convert forward slashes to backward slashes here.
+    for (path_space.data[0..path_space.len]) |*elem| {
+        if (elem.* == '/') {
+            elem.* = '\\';
+        }
+    }
+    path_space.data[path_space.len] = 0;
     return path_space;
 }
 
@@ -1484,73 +1566,3 @@ pub fn unexpectedStatus(status: NTSTATUS) std.os.UnexpectedError {
     }
     return error.Unexpected;
 }
-
-pub const OpenReparsePointError = error{
-    FileNotFound,
-    NoDevice,
-    SharingViolation,
-    AccessDenied,
-    PipeBusy,
-    PathAlreadyExists,
-    Unexpected,
-    NameTooLong,
-};
-
-/// Open file as a reparse point
-pub fn OpenReparsePoint(
-    dir: ?HANDLE,
-    sub_path_w: [*:0]const u16,
-) OpenReparsePointError!HANDLE {
-    const path_len_bytes = math.cast(u16, mem.lenZ(sub_path_w) * 2) catch |err| switch (err) {
-        error.Overflow => return error.NameTooLong,
-    };
-    var nt_name = UNICODE_STRING{
-        .Length = path_len_bytes,
-        .MaximumLength = path_len_bytes,
-        .Buffer = @intToPtr([*]u16, @ptrToInt(sub_path_w)),
-    };
-
-    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;
-    }
-
-    var attr = OBJECT_ATTRIBUTES{
-        .Length = @sizeOf(OBJECT_ATTRIBUTES),
-        .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w)) null else dir,
-        .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
-        .ObjectName = &nt_name,
-        .SecurityDescriptor = null,
-        .SecurityQualityOfService = null,
-    };
-    var io: IO_STATUS_BLOCK = undefined;
-    var result_handle: HANDLE = undefined;
-    const rc = ntdll.NtCreateFile(
-        &result_handle,
-        FILE_READ_ATTRIBUTES,
-        &attr,
-        &io,
-        null,
-        FILE_ATTRIBUTE_NORMAL,
-        FILE_SHARE_READ,
-        FILE_OPEN,
-        FILE_OPEN_REPARSE_POINT,
-        null,
-        0,
-    );
-    switch (rc) {
-        .SUCCESS => return result_handle,
-        .OBJECT_NAME_INVALID => unreachable,
-        .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
-        .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
-        .NO_MEDIA_IN_DEVICE => return error.NoDevice,
-        .INVALID_PARAMETER => unreachable,
-        .SHARING_VIOLATION => return error.SharingViolation,
-        .ACCESS_DENIED => return error.AccessDenied,
-        .PIPE_BUSY => return error.PipeBusy,
-        .OBJECT_PATH_SYNTAX_BAD => unreachable,
-        .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
-        .FILE_IS_A_DIRECTORY => unreachable,
-        else => return unexpectedStatus(rc),
-    }
-}
lib/std/os.zig
@@ -2364,8 +2364,7 @@ pub fn readlink(file_path: []const u8, out_buffer: []u8) ReadLinkError![]u8 {
     if (builtin.os.tag == .wasi) {
         @compileError("readlink is not supported in WASI; use readlinkat instead");
     } else if (builtin.os.tag == .windows) {
-        const file_path_w = try windows.sliceToWin32PrefixedFileW(file_path);
-        return readlinkW(file_path_w.span().ptr, out_buffer);
+        return windows.ReadLink(std.fs.cwd().fd, file_path, out_buffer);
     } else {
         const file_path_c = try toPosixPath(file_path);
         return readlinkZ(&file_path_c, out_buffer);
@@ -2377,56 +2376,7 @@ pub const readlinkC = @compileError("deprecated: renamed to readlinkZ");
 /// Windows-only. Same as `readlink` except `file_path` is null-terminated, WTF16 encoded.
 /// See also `readlinkZ`.
 pub fn readlinkW(file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 {
-    const w = windows;
-
-    const sharing = w.FILE_SHARE_DELETE | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE;
-    const disposition = w.OPEN_EXISTING;
-    const flags = w.FILE_FLAG_BACKUP_SEMANTICS | w.FILE_FLAG_OPEN_REPARSE_POINT;
-    const handle = w.CreateFileW(file_path, 0, sharing, null, disposition, flags, null) catch |err| {
-        switch (err) {
-            error.SharingViolation => return error.AccessDenied,
-            error.PipeBusy => unreachable,
-            error.PathAlreadyExists => unreachable,
-            else => |e| return e,
-        }
-    };
-    defer w.CloseHandle(handle);
-
-    var reparse_buf: [w.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
-    _ = try w.DeviceIoControl(handle, w.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..], null);
-
-    const reparse_struct = @ptrCast(*const w.REPARSE_DATA_BUFFER, @alignCast(@alignOf(w.REPARSE_DATA_BUFFER), &reparse_buf[0]));
-    switch (reparse_struct.ReparseTag) {
-        w.IO_REPARSE_TAG_SYMLINK => {
-            const buf = @ptrCast(*const w.SYMBOLIC_LINK_REPARSE_BUFFER, @alignCast(@alignOf(w.SYMBOLIC_LINK_REPARSE_BUFFER), &reparse_struct.DataBuffer[0]));
-            const offset = buf.SubstituteNameOffset >> 1;
-            const len = buf.SubstituteNameLength >> 1;
-            const path_buf = @as([*]const u16, &buf.PathBuffer);
-            const is_relative = buf.Flags & w.SYMLINK_FLAG_RELATIVE != 0;
-            return parseReadlinkPath(path_buf[offset .. offset + len], is_relative, out_buffer);
-        },
-        w.IO_REPARSE_TAG_MOUNT_POINT => {
-            const buf = @ptrCast(*const w.MOUNT_POINT_REPARSE_BUFFER, @alignCast(@alignOf(w.MOUNT_POINT_REPARSE_BUFFER), &reparse_struct.DataBuffer[0]));
-            const offset = buf.SubstituteNameOffset >> 1;
-            const len = buf.SubstituteNameLength >> 1;
-            const path_buf = @as([*]const u16, &buf.PathBuffer);
-            return parseReadlinkPath(path_buf[offset .. offset + len], false, out_buffer);
-        },
-        else => |value| {
-            std.debug.warn("unsupported symlink type: {}", .{value});
-            return error.UnsupportedReparsePointType;
-        },
-    }
-}
-
-fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
-    const prefix = [_]u16{ '\\', '?', '?', '\\' };
-    var start_index: usize = 0;
-    if (!is_relative and mem.startsWith(u16, path, &prefix)) {
-        start_index = prefix.len;
-    }
-    const out_len = std.unicode.utf16leToUtf8(out_buffer, path[start_index..]) catch unreachable;
-    return out_buffer[0..out_len];
+    return windows.ReadLinkW(std.fs.cwd().fd, file_path, out_buffer);
 }
 
 /// Same as `readlink` except `file_path` is null-terminated.
@@ -2459,8 +2409,7 @@ pub fn readlinkat(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) ReadLink
         return readlinkatWasi(dirfd, file_path, out_buffer);
     }
     if (builtin.os.tag == .windows) {
-        const file_path_w = try windows.cStrToPrefixedFileW(file_path);
-        return readlinkatW(dirfd, file_path.span().ptr, out_buffer);
+        return windows.ReadLink(dirfd, file_path, out_buffer);
     }
     const file_path_c = try toPosixPath(file_path);
     return readlinkatZ(dirfd, &file_path_c, out_buffer);
@@ -2491,19 +2440,7 @@ pub fn readlinkatWasi(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) Read
 /// Windows-only. Same as `readlinkat` except `file_path` is null-terminated, WTF16 encoded.
 /// See also `readlinkat`.
 pub fn readlinkatW(dirfd: fd_t, file_path: [*:0]const u16, out_buffer: []u8) ReadLinkError![]u8 {
-    const w = windows;
-
-    const handle = w.OpenReparsePoint(dir, file_path) catch |err| {
-        switch (err) {
-            error.SharingViolation => return error.AccessDenied,
-            error.PathAlreadyExists => unreachable,
-            error.PipeBusy => unreachable,
-            error.PathAlreadyExists => unreachable,
-            error.NoDevice => return error.FileNotFound,
-            else => |e| return e,
-        }
-    };
-    @compileError("TODO implement on Windows");
+    return windows.ReadLinkW(dirfd, file_path, out_buffer);
 }
 
 /// Same as `readlinkat` except `file_path` is null-terminated.