Skip to content

Commit aa4332f

Browse files
authored
Merge pull request #25539 from squeek502/windows-readlinkw
windows: Make readLinkW APIs output WTF-16, reduce stack usage of callers
2 parents f6fecfd + 6aa3570 commit aa4332f

File tree

5 files changed

+142
-117
lines changed

5 files changed

+142
-117
lines changed

lib/std/fs/Dir.zig

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,8 +1361,14 @@ pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u
13611361
return self.readLinkWasi(sub_path, buffer);
13621362
}
13631363
if (native_os == .windows) {
1364-
const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path);
1365-
return self.readLinkW(sub_path_w.span(), buffer);
1364+
var sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path);
1365+
const result_w = try self.readLinkW(sub_path_w.span(), &sub_path_w.data);
1366+
1367+
const len = std.unicode.calcWtf8Len(result_w);
1368+
if (len > buffer.len) return error.NameTooLong;
1369+
1370+
const end_index = std.unicode.wtf16LeToWtf8(buffer, result_w);
1371+
return buffer[0..end_index];
13661372
}
13671373
const sub_path_c = try posix.toPosixPath(sub_path);
13681374
return self.readLinkZ(&sub_path_c, buffer);
@@ -1376,15 +1382,24 @@ pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 {
13761382
/// Same as `readLink`, except the `sub_path_c` parameter is null-terminated.
13771383
pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 {
13781384
if (native_os == .windows) {
1379-
const sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c);
1380-
return self.readLinkW(sub_path_w.span(), buffer);
1385+
var sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c);
1386+
const result_w = try self.readLinkW(sub_path_w.span(), &sub_path_w.data);
1387+
1388+
const len = std.unicode.calcWtf8Len(result_w);
1389+
if (len > buffer.len) return error.NameTooLong;
1390+
1391+
const end_index = std.unicode.wtf16LeToWtf8(buffer, result_w);
1392+
return buffer[0..end_index];
13811393
}
13821394
return posix.readlinkatZ(self.fd, sub_path_c, buffer);
13831395
}
13841396

1385-
/// Windows-only. Same as `readLink` except the pathname parameter
1386-
/// is WTF16 LE encoded.
1387-
pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 {
1397+
/// Windows-only. Same as `readLink` except the path parameter
1398+
/// is WTF-16 LE encoded, NT-prefixed.
1399+
///
1400+
/// `sub_path_w` will never be accessed after `buffer` has been written to, so it
1401+
/// is safe to reuse a single buffer for both.
1402+
pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u16) ![]u16 {
13881403
return windows.ReadLink(self.fd, sub_path_w, buffer);
13891404
}
13901405

lib/std/fs/test.zig

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,16 @@ test "Dir.readLink" {
193193
// test 1: symlink to a file
194194
try setupSymlink(ctx.dir, file_target_path, "symlink1", .{});
195195
try testReadLink(ctx.dir, canonical_file_target_path, "symlink1");
196+
if (builtin.os.tag == .windows) {
197+
try testReadLinkW(testing.allocator, ctx.dir, canonical_file_target_path, "symlink1");
198+
}
196199

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

201207
// test 3: relative path symlink
202208
const parent_file = ".." ++ fs.path.sep_str ++ "target.txt";
@@ -205,6 +211,9 @@ test "Dir.readLink" {
205211
defer subdir.close();
206212
try setupSymlink(subdir, canonical_parent_file, "relative-link.txt", .{});
207213
try testReadLink(subdir, canonical_parent_file, "relative-link.txt");
214+
if (builtin.os.tag == .windows) {
215+
try testReadLinkW(testing.allocator, subdir, canonical_parent_file, "relative-link.txt");
216+
}
208217
}
209218
}.impl);
210219
}
@@ -215,6 +224,17 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
215224
try testing.expectEqualStrings(target_path, actual);
216225
}
217226

227+
fn testReadLinkW(allocator: mem.Allocator, dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
228+
const target_path_w = try std.unicode.wtf8ToWtf16LeAlloc(allocator, target_path);
229+
defer allocator.free(target_path_w);
230+
// Calling the W functions directly requires the path to be NT-prefixed
231+
const symlink_path_w = try std.os.windows.sliceToPrefixedFileW(dir.fd, symlink_path);
232+
const wtf16_buffer = try allocator.alloc(u16, target_path_w.len);
233+
defer allocator.free(wtf16_buffer);
234+
const actual = try dir.readLinkW(symlink_path_w.span(), wtf16_buffer);
235+
try testing.expectEqualSlices(u16, target_path_w, actual);
236+
}
237+
218238
fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {
219239
var buffer: [fs.max_path_bytes]u8 = undefined;
220240
const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);

lib/std/os/windows.zig

Lines changed: 47 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -889,61 +889,28 @@ pub const ReadLinkError = error{
889889
AccessDenied,
890890
Unexpected,
891891
NameTooLong,
892+
BadPathName,
893+
AntivirusInterference,
892894
UnsupportedReparsePointType,
893895
};
894896

895-
pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLinkError![]u8 {
896-
// Here, we use `NtCreateFile` to shave off one syscall if we were to use `OpenFile` wrapper.
897-
// With the latter, we'd need to call `NtCreateFile` twice, once for file symlink, and if that
898-
// failed, again for dir symlink. Omitting any mention of file/dir flags makes it possible
899-
// to open the symlink there and then.
900-
const path_len_bytes = math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
901-
var nt_name = UNICODE_STRING{
902-
.Length = path_len_bytes,
903-
.MaximumLength = path_len_bytes,
904-
.Buffer = @constCast(sub_path_w.ptr),
905-
};
906-
var attr = OBJECT_ATTRIBUTES{
907-
.Length = @sizeOf(OBJECT_ATTRIBUTES),
908-
.RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir,
909-
.Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
910-
.ObjectName = &nt_name,
911-
.SecurityDescriptor = null,
912-
.SecurityQualityOfService = null,
897+
/// `sub_path_w` will never be accessed after `out_buffer` has been written to, so it
898+
/// is safe to reuse a single buffer for both.
899+
pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u16) ReadLinkError![]u16 {
900+
const result_handle = OpenFile(sub_path_w, .{
901+
.access_mask = FILE_READ_ATTRIBUTES | SYNCHRONIZE,
902+
.dir = dir,
903+
.creation = FILE_OPEN,
904+
.follow_symlinks = false,
905+
.filter = .any,
906+
}) catch |err| switch (err) {
907+
error.IsDir, error.NotDir => return error.Unexpected, // filter = .any
908+
error.PathAlreadyExists => return error.Unexpected, // FILE_OPEN
909+
error.WouldBlock => return error.Unexpected,
910+
error.NoDevice => return error.FileNotFound,
911+
error.PipeBusy => return error.AccessDenied,
912+
else => |e| return e,
913913
};
914-
var result_handle: HANDLE = undefined;
915-
var io: IO_STATUS_BLOCK = undefined;
916-
917-
const rc = ntdll.NtCreateFile(
918-
&result_handle,
919-
FILE_READ_ATTRIBUTES | SYNCHRONIZE,
920-
&attr,
921-
&io,
922-
null,
923-
FILE_ATTRIBUTE_NORMAL,
924-
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
925-
FILE_OPEN,
926-
FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT,
927-
null,
928-
0,
929-
);
930-
switch (rc) {
931-
.SUCCESS => {},
932-
.OBJECT_NAME_INVALID => unreachable,
933-
.OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
934-
.OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
935-
.NO_MEDIA_IN_DEVICE => return error.FileNotFound,
936-
.BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
937-
.BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
938-
.INVALID_PARAMETER => unreachable,
939-
.SHARING_VIOLATION => return error.AccessDenied,
940-
.ACCESS_DENIED => return error.AccessDenied,
941-
.PIPE_BUSY => return error.AccessDenied,
942-
.OBJECT_PATH_SYNTAX_BAD => unreachable,
943-
.OBJECT_NAME_COLLISION => unreachable,
944-
.FILE_IS_A_DIRECTORY => unreachable,
945-
else => return unexpectedStatus(rc),
946-
}
947914
defer CloseHandle(result_handle);
948915

949916
var reparse_buf: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(REPARSE_DATA_BUFFER)) = undefined;
@@ -961,35 +928,33 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin
961928
const len = buf.SubstituteNameLength >> 1;
962929
const path_buf = @as([*]const u16, &buf.PathBuffer);
963930
const is_relative = buf.Flags & SYMLINK_FLAG_RELATIVE != 0;
964-
return parseReadlinkPath(path_buf[offset..][0..len], is_relative, out_buffer);
931+
return parseReadLinkPath(path_buf[offset..][0..len], is_relative, out_buffer);
965932
},
966933
IO_REPARSE_TAG_MOUNT_POINT => {
967934
const buf: *const MOUNT_POINT_REPARSE_BUFFER = @ptrCast(@alignCast(&reparse_struct.DataBuffer[0]));
968935
const offset = buf.SubstituteNameOffset >> 1;
969936
const len = buf.SubstituteNameLength >> 1;
970937
const path_buf = @as([*]const u16, &buf.PathBuffer);
971-
return parseReadlinkPath(path_buf[offset..][0..len], false, out_buffer);
938+
return parseReadLinkPath(path_buf[offset..][0..len], false, out_buffer);
972939
},
973-
else => |value| {
974-
std.debug.print("unsupported symlink type: {}", .{value});
940+
else => {
975941
return error.UnsupportedReparsePointType;
976942
},
977943
}
978944
}
979945

980-
/// Asserts that there is enough space is `out_buffer`.
981-
/// The result is encoded as [WTF-8](https://wtf-8.codeberg.page/).
982-
fn parseReadlinkPath(path: []const u16, is_relative: bool, out_buffer: []u8) []u8 {
983-
const win32_namespace_path = path: {
984-
if (is_relative) break :path path;
985-
const win32_path = ntToWin32Namespace(path) catch |err| switch (err) {
986-
error.NameTooLong => unreachable,
987-
error.NotNtPath => break :path path,
946+
fn parseReadLinkPath(path: []const u16, is_relative: bool, out_buffer: []u16) error{NameTooLong}![]u16 {
947+
path: {
948+
if (is_relative) break :path;
949+
return ntToWin32Namespace(path, out_buffer) catch |err| switch (err) {
950+
error.NameTooLong => |e| return e,
951+
error.NotNtPath => break :path,
988952
};
989-
break :path win32_path.span();
990-
};
991-
const out_len = std.unicode.wtf16LeToWtf8(out_buffer, win32_namespace_path);
992-
return out_buffer[0..out_len];
953+
}
954+
if (out_buffer.len < path.len) return error.NameTooLong;
955+
const dest = out_buffer[0..path.len];
956+
@memcpy(dest, path);
957+
return dest;
993958
}
994959

995960
pub const DeleteFileError = error{
@@ -2620,34 +2585,31 @@ test getUnprefixedPathType {
26202585
/// https://github.com/reactos/reactos/blob/master/modules/rostests/apitests/ntdll/RtlNtPathNameToDosPathName.c
26212586
///
26222587
/// `path` should be encoded as WTF-16LE.
2623-
pub fn ntToWin32Namespace(path: []const u16) !PathSpace {
2588+
///
2589+
/// Supports in-place modification (`path` and `out` may refer to the same slice).
2590+
pub fn ntToWin32Namespace(path: []const u16, out: []u16) error{ NameTooLong, NotNtPath }![]u16 {
26242591
if (path.len > PATH_MAX_WIDE) return error.NameTooLong;
26252592

2626-
var path_space: PathSpace = undefined;
26272593
const namespace_prefix = getNamespacePrefix(u16, path);
26282594
switch (namespace_prefix) {
26292595
.nt => {
26302596
var dest_index: usize = 0;
26312597
var after_prefix = path[4..]; // after the `\??\`
26322598
// The prefix \??\UNC\ means this is a UNC path, in which case the
26332599
// `\??\UNC\` should be replaced by `\\` (two backslashes)
2634-
// TODO: the "UNC" should technically be matched case-insensitively, but
2635-
// it's unlikely to matter since most/all paths passed into this
2636-
// function will have come from the OS meaning it should have
2637-
// the 'canonical' uppercase UNC.
26382600
const is_unc = after_prefix.len >= 4 and
2639-
std.mem.eql(u16, after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
2601+
eqlIgnoreCaseWTF16(after_prefix[0..3], std.unicode.utf8ToUtf16LeStringLiteral("UNC")) and
26402602
std.fs.path.PathType.windows.isSep(u16, std.mem.littleToNative(u16, after_prefix[3]));
2603+
const win32_len = path.len - @as(usize, if (is_unc) 6 else 4);
2604+
if (out.len < win32_len) return error.NameTooLong;
26412605
if (is_unc) {
2642-
path_space.data[0] = comptime std.mem.nativeToLittle(u16, '\\');
2606+
out[0] = comptime std.mem.nativeToLittle(u16, '\\');
26432607
dest_index += 1;
26442608
// We want to include the last `\` of `\??\UNC\`
26452609
after_prefix = path[7..];
26462610
}
2647-
@memcpy(path_space.data[dest_index..][0..after_prefix.len], after_prefix);
2648-
path_space.len = dest_index + after_prefix.len;
2649-
path_space.data[path_space.len] = 0;
2650-
return path_space;
2611+
@memmove(out[dest_index..][0..after_prefix.len], after_prefix);
2612+
return out[0..win32_len];
26512613
},
26522614
else => return error.NotNtPath,
26532615
}
@@ -2656,25 +2618,14 @@ pub fn ntToWin32Namespace(path: []const u16) !PathSpace {
26562618
test ntToWin32Namespace {
26572619
const L = std.unicode.utf8ToUtf16LeStringLiteral;
26582620

2659-
try testNtToWin32Namespace(L("UNC"), L("\\??\\UNC"));
2660-
try testNtToWin32Namespace(L("\\\\"), L("\\??\\UNC\\"));
2661-
try testNtToWin32Namespace(L("\\\\path1"), L("\\??\\UNC\\path1"));
2662-
try testNtToWin32Namespace(L("\\\\path1\\path2"), L("\\??\\UNC\\path1\\path2"));
2663-
2664-
try testNtToWin32Namespace(L(""), L("\\??\\"));
2665-
try testNtToWin32Namespace(L("C:"), L("\\??\\C:"));
2666-
try testNtToWin32Namespace(L("C:\\"), L("\\??\\C:\\"));
2667-
try testNtToWin32Namespace(L("C:\\test"), L("\\??\\C:\\test"));
2668-
try testNtToWin32Namespace(L("C:\\test\\"), L("\\??\\C:\\test\\"));
2621+
var mutable_unc_path_buf = L("\\??\\UNC\\path1\\path2").*;
2622+
try std.testing.expectEqualSlices(u16, L("\\\\path1\\path2"), try ntToWin32Namespace(&mutable_unc_path_buf, &mutable_unc_path_buf));
26692623

2670-
try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("foo")));
2671-
try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("C:\\test")));
2672-
try std.testing.expectError(error.NotNtPath, ntToWin32Namespace(L("\\\\.\\test")));
2673-
}
2624+
var mutable_path_buf = L("\\??\\C:\\test\\").*;
2625+
try std.testing.expectEqualSlices(u16, L("C:\\test\\"), try ntToWin32Namespace(&mutable_path_buf, &mutable_path_buf));
26742626

2675-
fn testNtToWin32Namespace(expected: []const u16, path: []const u16) !void {
2676-
const converted = try ntToWin32Namespace(path);
2677-
try std.testing.expectEqualSlices(u16, expected, converted.span());
2627+
var too_small_buf: [6]u16 = undefined;
2628+
try std.testing.expectError(error.NameTooLong, ntToWin32Namespace(L("\\??\\C:\\test"), &too_small_buf));
26782629
}
26792630

26802631
fn getFullPathNameW(path: [*:0]const u16, out: []u16) !usize {

0 commit comments

Comments
 (0)