@@ -156,23 +156,80 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
156
156
try testing .expectEqualStrings (target_path , given );
157
157
}
158
158
159
- test "stat on a symlink returns Kind.sym_link" {
159
+ test "File.stat on a File that is a symlink returns Kind.sym_link" {
160
+ // This test requires getting a file descriptor of a symlink which
161
+ // is not possible on all targets
162
+ switch (builtin .target .os .tag ) {
163
+ .windows , .linux = > {},
164
+ else = > return error .SkipZigTest ,
165
+ }
166
+
160
167
try testWithAllSupportedPathTypes (struct {
161
168
fn impl (ctx : * TestContext ) ! void {
162
169
const dir_target_path = try ctx .transformPath ("subdir" );
163
170
try ctx .dir .makeDir (dir_target_path );
164
171
165
- // TODO: Also test a symlink to a file.
166
- // There's currently no way to avoid following symlinks when opening files.
167
- // https://github.com/ziglang/zig/issues/18327
168
-
169
172
ctx .dir .symLink (dir_target_path , "symlink" , .{ .is_directory = true }) catch | err | switch (err ) {
170
173
// Symlink requires admin privileges on windows, so this test can legitimately fail.
171
174
error .AccessDenied = > return error .SkipZigTest ,
172
175
else = > return err ,
173
176
};
174
177
175
- var symlink = try ctx .dir .openDir ("symlink" , .{ .no_follow = true });
178
+ var symlink = switch (builtin .target .os .tag ) {
179
+ .windows = > windows_symlink : {
180
+ const w = std .os .windows ;
181
+
182
+ const sub_path_w = try std .os .windows .cStrToPrefixedFileW (ctx .dir .fd , "symlink" );
183
+
184
+ var result = Dir {
185
+ .fd = undefined ,
186
+ };
187
+
188
+ const path_len_bytes = @as (u16 , @intCast (sub_path_w .span ().len * 2 ));
189
+ var nt_name = w.UNICODE_STRING {
190
+ .Length = path_len_bytes ,
191
+ .MaximumLength = path_len_bytes ,
192
+ .Buffer = @constCast (sub_path_w ),
193
+ };
194
+ var attr = w.OBJECT_ATTRIBUTES {
195
+ .Length = @sizeOf (w .OBJECT_ATTRIBUTES ),
196
+ .RootDirectory = if (fs .path .isAbsoluteWindowsW (sub_path_w )) null else ctx .dir .fd ,
197
+ .Attributes = 0 ,
198
+ .ObjectName = & nt_name ,
199
+ .SecurityDescriptor = null ,
200
+ .SecurityQualityOfService = null ,
201
+ };
202
+ var io : w.IO_STATUS_BLOCK = undefined ;
203
+ const rc = w .ntdll .NtCreateFile (
204
+ & result .fd ,
205
+ w .STANDARD_RIGHTS_READ | w .FILE_READ_ATTRIBUTES | w .FILE_READ_EA | w .SYNCHRONIZE | w .FILE_TRAVERSE ,
206
+ & attr ,
207
+ & io ,
208
+ null ,
209
+ w .FILE_ATTRIBUTE_NORMAL ,
210
+ w .FILE_SHARE_READ | w .FILE_SHARE_WRITE ,
211
+ w .FILE_OPEN ,
212
+ // FILE_OPEN_REPARSE_POINT is the important thing here
213
+ w .FILE_OPEN_REPARSE_POINT | w .FILE_DIRECTORY_FILE | w .FILE_SYNCHRONOUS_IO_NONALERT | w .FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point ,
214
+ null ,
215
+ 0 ,
216
+ );
217
+
218
+ switch (rc ) {
219
+ .SUCCESS = > break :windows_symlink result ,
220
+ else = > return w .unexpectedStatus (rc ),
221
+ }
222
+ },
223
+ .linux = > linux_symlink : {
224
+ const sub_path_c = try os .toPosixPath ("symlink" );
225
+ // the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink
226
+ // note that if O_DIRECTORY is set, then this will error with ENOTDIR
227
+ const flags = os .O .NOFOLLOW | os .O .PATH | os .O .RDONLY | os .O .CLOEXEC ;
228
+ const fd = try os .openatZ (ctx .dir .fd , & sub_path_c , flags , 0 );
229
+ break :linux_symlink Dir { .fd = fd };
230
+ },
231
+ else = > unreachable ,
232
+ };
176
233
defer symlink .close ();
177
234
178
235
const stat = try symlink .stat ();
0 commit comments