authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-19 14:26:35-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-07-22 20:12:57+02:00
logaad9870a284410b9b7fa4d5c5d84f4a4d697c71b
tree77385a222705df559a686f16ebbd541f4d306f05
parent2b1c6633aaf1d59d4affd88e87a507a02836c478

Io.Threaded: Decouple follow_symlinks and asynchronous IO on Windows

The logic around follow_symlinks and asynchronous IO was first introduced in 66bbe4ec4c11839217d6a9d65771d60d45cd6bc1, and then cemented in 390194431e7fa439d67686e5a0e9efceed2d898a with the added comment: > If we're not following symlinks, we need to ensure we don't pass in any synchronization flags such as FILE_SYNCHRONOUS_IO_NONALERT. As far as I can tell, this comment is erroneous. There is no documented incompatibility between OPEN_REPARSE_POINT and SYNCHRONOUS_IO_NONALERT, and empirically everything works fine when using SYNCHRONOUS_IO_NONALERT. In fact, in 68ed787751ed36af3db0c52031741a5c31413034 the ReadLink implementation (a main usage of OPEN_REPARSE_POINT) was specifically switched to *not* use asynchronous IO (although that was effectively reverted during the move to std.Io). This also fixes a bug with `.follow_symlinks = false` since, before this commit, the returned File would always have `.nonblocking = false` even though that was not correct when `.follow_symlinks` was false.

2 files changed, 41 insertions(+), 4 deletions(-)

lib/std/Io/Threaded.zig+4-4
...@@ -5053,7 +5053,7 @@ pub fn dirOpenFileWtf16(...@@ -5053,7 +5053,7 @@ pub fn dirOpenFileWtf16(
5053 .VALID_FLAGS,5053 .VALID_FLAGS,
5054 .OPEN,5054 .OPEN,
5055 .{5055 .{
5056 .IO = if (flags.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,5056 .IO = .SYNCHRONOUS_NONALERT,
5057 .NON_DIRECTORY_FILE = !allow_directory,5057 .NON_DIRECTORY_FILE = !allow_directory,
5058 .OPEN_REPARSE_POINT = !flags.follow_symlinks,5058 .OPEN_REPARSE_POINT = !flags.follow_symlinks,
5059 },5059 },
...@@ -8136,7 +8136,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink...@@ -8136,7 +8136,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink
8136 .{8136 .{
8137 .DIRECTORY_FILE = false,8137 .DIRECTORY_FILE = false,
8138 .NON_DIRECTORY_FILE = false,8138 .NON_DIRECTORY_FILE = false,
8139 .IO = .ASYNCHRONOUS,8139 .IO = .SYNCHRONOUS_NONALERT,
8140 .OPEN_REPARSE_POINT = true,8140 .OPEN_REPARSE_POINT = true,
8141 },8141 },
8142 null,8142 null,
...@@ -8202,7 +8202,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink...@@ -8202,7 +8202,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink
82028202
8203 var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(windows.REPARSE_DATA_BUFFER)) = undefined;8203 var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 align(@alignOf(windows.REPARSE_DATA_BUFFER)) = undefined;
8204 switch ((try deviceIoControl(&.{8204 switch ((try deviceIoControl(&.{
8205 .file = .{ .handle = result_handle, .flags = .{ .nonblocking = true } },8205 .file = .{ .handle = result_handle, .flags = .{ .nonblocking = false } },
8206 .code = .GET_REPARSE_POINT,8206 .code = .GET_REPARSE_POINT,
8207 .out = &reparse_buf,8207 .out = &reparse_buf,
8208 })).u.Status) {8208 })).u.Status) {
...@@ -18995,7 +18995,7 @@ fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!windows...@@ -18995,7 +18995,7 @@ fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!windows
18995 .{18995 .{
18996 .DIRECTORY_FILE = options.filter == .dir_only,18996 .DIRECTORY_FILE = options.filter == .dir_only,
18997 .NON_DIRECTORY_FILE = options.filter == .non_directory_only,18997 .NON_DIRECTORY_FILE = options.filter == .non_directory_only,
18998 .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,18998 .IO = .SYNCHRONOUS_NONALERT,
18999 .OPEN_REPARSE_POINT = !options.follow_symlinks,18999 .OPEN_REPARSE_POINT = !options.follow_symlinks,
19000 },19000 },
19001 null,19001 null,
lib/std/fs/test.zig+37
...@@ -758,6 +758,43 @@ test "readFileAlloc" {...@@ -758,6 +758,43 @@ test "readFileAlloc" {
758 );758 );
759}759}
760760
761test "file operations with follow_symlinks=false" {
762 const io = testing.io;
763
764 var tmp_dir = tmpDir(.{});
765 defer tmp_dir.cleanup();
766
767 const contents = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";
768 try tmp_dir.dir.writeFile(io, .{
769 .sub_path = "test_file",
770 .data = contents,
771 });
772
773 // Without lock
774 {
775 var file = try tmp_dir.dir.openFile(io, "test_file", .{ .follow_symlinks = false });
776 defer file.close(io);
777
778 var file_reader = file.reader(io, &.{});
779 const actual_contents = try file_reader.interface.allocRemaining(testing.allocator, .unlimited);
780 defer testing.allocator.free(actual_contents);
781
782 try std.testing.expectEqualSlices(u8, contents, actual_contents);
783 }
784
785 // With lock
786 {
787 var file = try tmp_dir.dir.openFile(io, "test_file", .{ .follow_symlinks = false, .lock = .exclusive });
788 defer file.close(io);
789
790 var file_reader = file.reader(io, &.{});
791 const actual_contents = try file_reader.interface.allocRemaining(testing.allocator, .unlimited);
792 defer testing.allocator.free(actual_contents);
793
794 try std.testing.expectEqualSlices(u8, contents, actual_contents);
795 }
796}
797
761test "Dir.statFile" {798test "Dir.statFile" {
762 try testWithAllSupportedPathTypes(struct {799 try testWithAllSupportedPathTypes(struct {
763 fn impl(ctx: *TestContext) !void {800 fn impl(ctx: *TestContext) !void {