Skip to content
Open
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
35 changes: 29 additions & 6 deletions lib/std/fs/Dir.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2026,8 +2026,14 @@ pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u
return self.readLinkWasi(sub_path, buffer);
}
if (native_os == .windows) {
const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path);
return self.readLinkW(sub_path_w.span(), buffer);
var sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path);
const result_w = try self.readLinkW2(sub_path_w.span(), &sub_path_w.data);

const len = std.unicode.calcWtf8Len(result_w);
if (len > buffer.len) return error.NameTooLong;

const end_index = std.unicode.wtf16LeToWtf8(buffer, result_w);
return buffer[0..end_index];
}
const sub_path_c = try posix.toPosixPath(sub_path);
return self.readLinkZ(&sub_path_c, buffer);
Expand All @@ -2041,18 +2047,35 @@ pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 {
/// Same as `readLink`, except the `sub_path_c` parameter is null-terminated.
pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 {
if (native_os == .windows) {
const sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c);
return self.readLinkW(sub_path_w.span(), buffer);
var sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c);
const result_w = try self.readLinkW2(sub_path_w.span(), &sub_path_w.data);

const len = std.unicode.calcWtf8Len(result_w);
if (len > buffer.len) return error.NameTooLong;

const end_index = std.unicode.wtf16LeToWtf8(buffer, result_w);
return buffer[0..end_index];
}
return posix.readlinkatZ(self.fd, sub_path_c, buffer);
}

/// Windows-only. Same as `readLink` except the pathname parameter
/// is WTF16 LE encoded.
/// Deprecated; use `readLinkW2`.
///
/// Windows-only. Same as `readLink` except the path parameter
/// is WTF-16 LE encoded, NT-prefixed.
pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 {
return windows.ReadLink(self.fd, sub_path_w, buffer);
}

/// Windows-only. Same as `readLink` except the path parameter
/// is WTF-16 LE encoded, NT-prefixed.
///
/// `sub_path_w` will never be accessed after `buffer` has been written to, so it
/// is safe to reuse a single buffer for both.
pub fn readLinkW2(self: Dir, sub_path_w: []const u16, buffer: []u16) ![]u16 {
return windows.ReadLink2(self.fd, sub_path_w, buffer);
}

/// Read all of file contents using a preallocated buffer.
/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len`
/// the situation is ambiguous. It could either mean that the entire file was read, and
Expand Down
28 changes: 28 additions & 0 deletions lib/std/fs/test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,16 @@ test "Dir.readLink" {
// test 1: symlink to a file
try setupSymlink(ctx.dir, file_target_path, "symlink1", .{});
try testReadLink(ctx.dir, canonical_file_target_path, "symlink1");
if (builtin.os.tag == .windows) {
try testReadLinkW(testing.allocator, ctx.dir, canonical_file_target_path, "symlink1");
}

// test 2: symlink to a directory (can be different on Windows)
try setupSymlink(ctx.dir, dir_target_path, "symlink2", .{ .is_directory = true });
try testReadLink(ctx.dir, canonical_dir_target_path, "symlink2");
if (builtin.os.tag == .windows) {
try testReadLinkW(testing.allocator, ctx.dir, canonical_dir_target_path, "symlink2");
}

// test 3: relative path symlink
const parent_file = ".." ++ fs.path.sep_str ++ "target.txt";
Expand All @@ -201,6 +207,9 @@ test "Dir.readLink" {
defer subdir.close();
try setupSymlink(subdir, canonical_parent_file, "relative-link.txt", .{});
try testReadLink(subdir, canonical_parent_file, "relative-link.txt");
if (builtin.os.tag == .windows) {
try testReadLinkW(testing.allocator, subdir, canonical_parent_file, "relative-link.txt");
}
}
}.impl);
}
Expand All @@ -211,6 +220,25 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
try testing.expectEqualStrings(target_path, actual);
}

fn testReadLinkW(allocator: mem.Allocator, dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
const target_path_w = try std.unicode.wtf8ToWtf16LeAlloc(allocator, target_path);
defer allocator.free(target_path_w);
// Calling the W functions directly requires the path to be NT-prefixed
const symlink_path_w = try std.os.windows.sliceToPrefixedFileW(dir.fd, symlink_path);
{
const wtf8_buffer = try allocator.alloc(u8, target_path.len);
defer allocator.free(wtf8_buffer);
const actual = try dir.readLinkW(symlink_path_w.span(), wtf8_buffer);
try testing.expectEqualStrings(target_path, actual);
}
{
const wtf16_buffer = try allocator.alloc(u16, target_path_w.len);
defer allocator.free(wtf16_buffer);
const actual = try dir.readLinkW2(symlink_path_w.span(), wtf16_buffer);
try testing.expectEqualSlices(u16, target_path_w, actual);
}
}

fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {
var buffer: [fs.max_path_bytes]u8 = undefined;
const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);
Expand Down
188 changes: 124 additions & 64 deletions lib/std/os/windows.zig
Original file line number Diff line number Diff line change
Expand Up @@ -887,61 +887,76 @@ pub const ReadLinkError = error{
AccessDenied,
Unexpected,
NameTooLong,
BadPathName,
AntivirusInterference,
UnsupportedReparsePointType,
};

/// Deprecated; use `ReadLink2`.
pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLinkError![]u8 {
// Here, we use `NtCreateFile` to shave off one syscall if we were to use `OpenFile` wrapper.
// With the latter, we'd need to call `NtCreateFile` twice, once for file symlink, and if that
// failed, again for dir symlink. Omitting any mention of file/dir flags makes it possible
// to open the symlink there and then.
const path_len_bytes = math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
var nt_name = UNICODE_STRING{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(sub_path_w.ptr),
const result_handle = OpenFile(sub_path_w, .{
.access_mask = FILE_READ_ATTRIBUTES | SYNCHRONIZE,
.dir = dir,
.creation = FILE_OPEN,
.follow_symlinks = false,
.filter = .any,
}) catch |err| switch (err) {
error.IsDir, error.NotDir => return error.Unexpected, // filter = .any
error.PathAlreadyExists => return error.Unexpected, // FILE_OPEN
error.WouldBlock => return error.Unexpected,
error.NoDevice => return error.FileNotFound,
error.PipeBusy => return error.AccessDenied,
else => |e| return e,
};
var attr = OBJECT_ATTRIBUTES{
.Length = @sizeOf(OBJECT_ATTRIBUTES),
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else dir,
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
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, null, reparse_buf[0..]) catch |err| switch (err) {
error.AccessDenied => return error.Unexpected,
error.UnrecognizedVolume => return error.Unexpected,
else => |e| return e,
};
var result_handle: HANDLE = undefined;
var io: IO_STATUS_BLOCK = undefined;

const rc = ntdll.NtCreateFile(
&result_handle,
FILE_READ_ATTRIBUTES | SYNCHRONIZE,
&attr,
&io,
null,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT,
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,
.BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
.BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
.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),
const reparse_struct: *const REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
switch (reparse_struct.ReparseTag) {
IO_REPARSE_TAG_SYMLINK => {
const buf: *const SYMBOLIC_LINK_REPARSE_BUFFER = @ptrCast(@alignCast(&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..][0..len], is_relative, out_buffer);
},
IO_REPARSE_TAG_MOUNT_POINT => {
const buf: *const MOUNT_POINT_REPARSE_BUFFER = @ptrCast(@alignCast(&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..][0..len], false, out_buffer);
},
else => {
return error.UnsupportedReparsePointType;
},
}
}

/// `sub_path_w` will never be accessed after `out_buffer` has been written to, so it
/// is safe to reuse a single buffer for both.
pub fn ReadLink2(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u16) ReadLinkError![]u16 {
const result_handle = OpenFile(sub_path_w, .{
.access_mask = FILE_READ_ATTRIBUTES | SYNCHRONIZE,
.dir = dir,
.creation = FILE_OPEN,
.follow_symlinks = false,
.filter = .any,
}) catch |err| switch (err) {
error.IsDir, error.NotDir => return error.Unexpected, // filter = .any
error.PathAlreadyExists => return error.Unexpected, // FILE_OPEN
error.WouldBlock => return error.Unexpected,
error.NoDevice => return error.FileNotFound,
error.PipeBusy => return error.AccessDenied,
else => |e| return e,
};
defer CloseHandle(result_handle);

var reparse_buf: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(REPARSE_DATA_BUFFER)) = undefined;
Expand All @@ -959,25 +974,26 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin
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..][0..len], is_relative, out_buffer);
return parseReadLinkPath2(path_buf[offset..][0..len], is_relative, out_buffer);
},
IO_REPARSE_TAG_MOUNT_POINT => {
const buf: *const MOUNT_POINT_REPARSE_BUFFER = @ptrCast(@alignCast(&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..][0..len], false, out_buffer);
return parseReadLinkPath2(path_buf[offset..][0..len], false, out_buffer);
},
else => |value| {
std.debug.print("unsupported symlink type: {}", .{value});
else => {
return error.UnsupportedReparsePointType;
},
}
}

/// Asserts that there is enough space is `out_buffer`.
/// Deprecated, should be deleted when ReadLink2 is renamed to ReadLink
///
/// Asserts that there is enough space in `out_buffer`.
/// The result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
fn parseReadLinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
const win32_namespace_path = path: {
if (is_relative) break :path path;
const win32_path = ntToWin32Namespace(path) catch |err| switch (err) {
Expand All @@ -990,6 +1006,20 @@ fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u
return out_buffer[0..out_len];
}

fn parseReadLinkPath2(path: []const u16, is_relative: bool, out_buffer: []u16) error{NameTooLong}![]u16 {
path: {
if (is_relative) break :path;
return ntToWin32Namespace2(path, out_buffer) catch |err| switch (err) {
error.NameTooLong => |e| return e,
error.NotNtPath => break :path,
};
}
if (out_buffer.len < path.len) return error.NameTooLong;
const dest = out_buffer[0..path.len];
@memcpy(dest, path);
return dest;
}

pub const DeleteFileError = error{
FileNotFound,
AccessDenied,
Expand Down Expand Up @@ -2720,6 +2750,8 @@ test getUnprefixedPathType {
try std.testing.expectEqual(UnprefixedPathType.drive_absolute, getUnprefixedPathType(u8, "x:/a/b/c"));
}

/// Deprecated; use `ntToWin32Namespace2`.
///
/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
/// The possible transformations are:
/// \??\C:\Some\Path -> C:\Some\Path
Expand All @@ -2731,33 +2763,48 @@ test getUnprefixedPathType {
///
/// `path` should be encoded as WTF-16LE.
pub fn ntToWin32Namespace(path: []const u16) !PathSpace {
var path_space: PathSpace = undefined;
const win32_path = try ntToWin32Namespace2(path, &path_space.data);
path_space.len = win32_path.len;
path_space.data[win32_path.len] = 0;
return path_space;
}

/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
/// The possible transformations are:
/// \??\C:\Some\Path -> C:\Some\Path
/// \??\UNC\server\share\foo -> \\server\share\foo
/// If the path does not have the NT namespace prefix, then `error.NotNtPath` is returned.
///
/// Functionality is based on the ReactOS test cases found here:
/// https://github.com/reactos/reactos/blob/master/modules/rostests/apitests/ntdll/RtlNtPathNameToDosPathName.c
///
/// `path` should be encoded as WTF-16LE.
///
/// Supports in-place modification (`path` and `out` may refer to the same slice).
pub fn ntToWin32Namespace2(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {
if (path.len > PATH_MAX_WIDE) return error.NameTooLong;

var path_space: PathSpace = undefined;
const namespace_prefix = getNamespacePrefix(u16, path);
switch (namespace_prefix) {
.nt => {
var dest_index: usize = 0;
var after_prefix = path[4..]; // after the `\??\`
// The prefix \??\UNC\ means this is a UNC path, in which case the
// `\??\UNC\` should be replaced by `\\` (two backslashes)
// TODO: the "UNC" should technically be matched case-insensitively, but
// it's unlikely to matter since most/all paths passed into this
// function will have come from the OS meaning it should have
// the 'canonical' uppercase UNC.
const is_unc = after_prefix.len >= 4 and
std.mem.eql(u16, after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
eqlIgnoreCaseWTF16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));
const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
if (out.len < win32_len) return error.NameTooLong;
if (is_unc) {
path_space.data[0] = comptime std.mem.nativeToLittle(u16, '\\');
out[0] = comptime std.mem.nativeToLittle(u16, '\\');
dest_index += 1;
// We want to include the last `\` of `\??\UNC\`
after_prefix = path[7..];
}
@memcpy(path_space.data[dest_index..][0..after_prefix.len], after_prefix);
path_space.len = dest_index + after_prefix.len;
path_space.data[path_space.len] = 0;
return path_space;
@memmove(out[dest_index..][0..after_prefix.len], after_prefix);
return out[0..win32_len];
},
else => return error.NotNtPath,
}
Expand Down Expand Up @@ -2787,6 +2834,19 @@ fn testNtToWin32Namespace(expected: []const u16, path: []const u16) !void {
try std.testing.expectEqualSlices(u16, expected, converted.span());
}

test ntToWin32Namespace2 {
const L = std.unicode.utf8ToUtf16LeStringLiteral;

var mutable_unc_path_buf = L("\\??\\UNC\\path1\\path2").*;
try std.testing.expectEqualSlices(u16, L("\\\\path1\\path2"), try ntToWin32Namespace2(&mutable_unc_path_buf, &mutable_unc_path_buf));

var mutable_path_buf = L("\\??\\C:\\test\\").*;
try std.testing.expectEqualSlices(u16, L("C:\\test\\"), try ntToWin32Namespace2(&mutable_path_buf, &mutable_path_buf));

var too_small_buf: [6]u16 = undefined;
try std.testing.expectError(error.NameTooLong, ntToWin32Namespace2(L("\\??\\C:\\test"), &too_small_buf));
}

fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {
const result = kernel32.GetFullPathNameW(path, @as(u32, @intCast(out.len)), out.ptr, null);
if (result == 0) {
Expand Down
Loading