Merge pull request 'Windows: Avoid ever-expanding DeviceIoControl error set, handle NOT_A_REPARSE_POINT in ReadLink' (#30186) from squeek502/zig:win-deviceiocontrol into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/30186
This commit is contained in:
Andrew Kelley
2025-12-19 18:50:27 +01:00
2 changed files with 57 additions and 67 deletions
+26
View File
@@ -218,6 +218,32 @@ test "Dir.readLink" {
}.impl);
}
test "Dir.readLink on non-symlinks" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const file_path = try ctx.transformPath("file.txt");
try ctx.dir.writeFile(.{ .sub_path = file_path, .data = "nonsense" });
const dir_path = try ctx.transformPath("subdir");
try ctx.dir.makeDir(dir_path);
// file
var buffer: [fs.max_path_bytes]u8 = undefined;
try std.testing.expectError(error.NotLink, ctx.dir.readLink(file_path, &buffer));
if (builtin.os.tag == .windows) {
var file_path_w = try std.os.windows.sliceToPrefixedFileW(ctx.dir.fd, file_path);
try std.testing.expectError(error.NotLink, ctx.dir.readLinkW(file_path_w.span(), &file_path_w.data));
}
// dir
try std.testing.expectError(error.NotLink, ctx.dir.readLink(dir_path, &buffer));
if (builtin.os.tag == .windows) {
var dir_path_w = try std.os.windows.sliceToPrefixedFileW(ctx.dir.fd, dir_path);
try std.testing.expectError(error.NotLink, ctx.dir.readLinkW(dir_path_w.span(), &dir_path_w.data));
}
}
}.impl);
}
fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
var buffer: [fs.max_path_bytes]u8 = undefined;
const actual = try dir.readLink(symlink_path, buffer[0..]);
+31 -67
View File
@@ -2574,23 +2574,6 @@ pub fn CreatePipe(rd: *HANDLE, wr: *HANDLE, sattr: *const SECURITY_ATTRIBUTES) C
wr.* = write;
}
pub const DeviceIoControlError = error{
AccessDenied,
/// The volume does not contain a recognized file system. File system
/// drivers might not be loaded, or the volume may be corrupt.
UnrecognizedVolume,
Pending,
/// Attempted to connect a named pipe in the "closing" state, meaning a previous client has
/// has closed their handle but we have not yet disconnected the pipe.
PipeClosing,
/// Attempted to connect a named pipe in the "connected" state, meaning a client has already
/// opened the pipe; there is a good connection between client and server.
PipeAlreadyConnected,
/// Attempted to connect a non-blocking named pipe which is already listening for connections.
PipeAlreadyListening,
Unexpected,
};
/// A Zig wrapper around `NtDeviceIoControlFile` and `NtFsControlFile` syscalls.
/// It implements similar behavior to `DeviceIoControl` and is meant to serve
/// as a direct substitute for that call.
@@ -2606,9 +2589,9 @@ pub fn DeviceIoControl(
in: []const u8 = &.{},
out: []u8 = &.{},
},
) DeviceIoControlError!void {
) NTSTATUS {
var io_status_block: IO_STATUS_BLOCK = undefined;
const rc = switch (io_control_code.DeviceType) {
return switch (io_control_code.DeviceType) {
.FILE_SYSTEM, .NAMED_PIPE => ntdll.NtFsControlFile(
device,
opts.event,
@@ -2634,19 +2617,6 @@ pub fn DeviceIoControl(
@intCast(opts.out.len),
),
};
switch (rc) {
.SUCCESS => {},
.PIPE_CLOSING => return error.PipeClosing,
.PIPE_CONNECTED => return error.PipeAlreadyConnected,
.PIPE_LISTENING => return error.PipeAlreadyListening,
.PRIVILEGE_NOT_HELD => return error.AccessDenied,
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_DEVICE_REQUEST => return error.AccessDenied, // Not supported by the underlying filesystem
.INVALID_PARAMETER => unreachable,
.UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume,
.PENDING => return error.Pending,
else => return unexpectedStatus(rc),
}
}
pub fn GetOverlappedResult(h: HANDLE, overlapped: *OVERLAPPED, wait: bool) !DWORD {
@@ -3037,9 +3007,6 @@ pub const CreateSymbolicLinkError = error{
NoDevice,
NetworkNotFound,
BadPathName,
/// The volume does not contain a recognized file system. File system
/// drivers might not be loaded, or the volume may be corrupt.
UnrecognizedVolume,
Unexpected,
};
@@ -3139,13 +3106,14 @@ pub fn CreateSymbolicLink(
@memcpy(buffer[@sizeOf(SYMLINK_DATA)..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
const paths_start = @sizeOf(SYMLINK_DATA) + final_target_path.len * 2;
@memcpy(buffer[paths_start..][0 .. final_target_path.len * 2], @as([*]const u8, @ptrCast(final_target_path)));
_ = DeviceIoControl(symlink_handle, FSCTL.SET_REPARSE_POINT, .{ .in = buffer[0..buf_len] }) catch |err| switch (err) {
error.PipeClosing => unreachable,
error.PipeAlreadyConnected => unreachable,
error.PipeAlreadyListening => unreachable,
error.Pending => unreachable,
else => |e| return e,
};
const rc = DeviceIoControl(symlink_handle, FSCTL.SET_REPARSE_POINT, .{ .in = buffer[0..buf_len] });
switch (rc) {
.SUCCESS => {},
.PRIVILEGE_NOT_HELD => return error.AccessDenied,
.ACCESS_DENIED => return error.AccessDenied,
.INVALID_DEVICE_REQUEST => return error.AccessDenied, // Not supported by the underlying filesystem
else => return unexpectedStatus(rc),
}
}
pub const ReadLinkError = error{
@@ -3157,6 +3125,7 @@ pub const ReadLinkError = error{
BadPathName,
AntivirusInterference,
UnsupportedReparsePointType,
NotLink,
};
/// `sub_path_w` will never be accessed after `out_buffer` has been written to, so it
@@ -3184,15 +3153,12 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u16) ReadLi
defer CloseHandle(result_handle);
var reparse_buf: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(REPARSE_DATA_BUFFER)) = undefined;
_ = DeviceIoControl(result_handle, FSCTL.GET_REPARSE_POINT, .{ .out = reparse_buf[0..] }) catch |err| switch (err) {
error.PipeClosing => unreachable,
error.PipeAlreadyConnected => unreachable,
error.PipeAlreadyListening => unreachable,
error.AccessDenied => return error.Unexpected,
error.UnrecognizedVolume => return error.Unexpected,
error.Pending => unreachable,
else => |e| return e,
};
const rc = DeviceIoControl(result_handle, FSCTL.GET_REPARSE_POINT, .{ .out = reparse_buf[0..] });
switch (rc) {
.SUCCESS => {},
.NOT_A_REPARSE_POINT => return error.NotLink,
else => return unexpectedStatus(rc),
}
const reparse_struct: *const REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
const IoReparseTagInt = @typeInfo(IO_REPARSE_TAG).@"struct".backing_integer.?;
@@ -3744,14 +3710,14 @@ pub fn GetFinalPathNameByHandle(
input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2);
@memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr)));
DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf }) catch |err| switch (err) {
error.PipeClosing => unreachable,
error.PipeAlreadyConnected => unreachable,
error.PipeAlreadyListening => unreachable,
error.AccessDenied => return error.Unexpected,
error.Pending => unreachable,
else => |e| return e,
};
{
const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf });
switch (rc) {
.SUCCESS => {},
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
else => return unexpectedStatus(rc),
}
}
const mount_points_struct: *const MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]);
const mount_points = @as(
@@ -3803,14 +3769,12 @@ pub fn GetFinalPathNameByHandle(
vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2);
@memcpy(@as([*]WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink);
DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf }) catch |err| switch (err) {
error.PipeClosing => unreachable,
error.PipeAlreadyConnected => unreachable,
error.PipeAlreadyListening => unreachable,
error.AccessDenied => return error.Unexpected,
error.Pending => unreachable,
else => |e| return e,
};
const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf });
switch (rc) {
.SUCCESS => {},
.UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume,
else => return unexpectedStatus(rc),
}
const volume_paths_struct: *const MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]);
const volume_path = std.mem.sliceTo(@as(
[*]const u16,