authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-05 06:25:38+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-05 06:25:38+01:00
log4c35d53b9a95c7956aeca8347a1884d02526a0a1
tree300395e820281affa126d312f05508b07d4186fc
parentfcef9905ae859601d085576012b81dc05f67c46f
parent3078a3197bce73f6ce70117989ea09f991470f50

Merge pull request 'std.os.windows: move OpenFile and GetFinalPathNameByHandle into Io.Threaded' (#31111) from windows-open-file-again into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/31111

9 files changed, 1221 insertions(+), 1262 deletions(-)

build.zig+1-4
...@@ -625,10 +625,7 @@ pub fn build(b: *std.Build) !void {...@@ -625,10 +625,7 @@ pub fn build(b: *std.Build) !void {
625 .aarch64 => 1_813_612_134,625 .aarch64 => 1_813_612_134,
626 else => 1_900_000_000,626 else => 1_900_000_000,
627 },627 },
628 .windows => switch (b.graph.host.result.cpu.arch) {628 .windows => 400_000_000,
629 .x86_64 => 386_287_616,
630 else => 400_000_000,
631 },
632 else => 2_200_000_000,629 else => 2_200_000_000,
633 },630 },
634 }));631 }));
lib/std/Build/Watch.zig+1-1
...@@ -358,7 +358,7 @@ const Os = switch (builtin.os.tag) {...@@ -358,7 +358,7 @@ const Os = switch (builtin.os.tag) {
358 var dir_handle: windows.HANDLE = undefined;358 var dir_handle: windows.HANDLE = undefined;
359 const root_fd = path.root_dir.handle.handle;359 const root_fd = path.root_dir.handle.handle;
360 const sub_path = path.subPathOrDot();360 const sub_path = path.subPathOrDot();
361 const sub_path_w = try windows.sliceToPrefixedFileW(root_fd, sub_path);361 const sub_path_w = try std.Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path); // TODO eliminate this call
362 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;362 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
363363
364 var nt_name = windows.UNICODE_STRING{364 var nt_name = windows.UNICODE_STRING{
lib/std/Io/Threaded.zig+881-165
...@@ -1404,6 +1404,8 @@ const splat_buffer_size = 64;...@@ -1404,6 +1404,8 @@ const splat_buffer_size = 64;
1404/// posix systems.1404/// posix systems.
1405const poll_buffer_len = 64;1405const poll_buffer_len = 64;
1406const default_PATH = "/usr/local/bin:/bin/:/usr/bin";1406const default_PATH = "/usr/local/bin:/bin/:/usr/bin";
1407/// There are multiple kernel bugs being worked around with retries.
1408const max_windows_kernel_bug_retries = 13;
14071409
1408comptime {1410comptime {
1409 if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX);1411 if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX);
...@@ -3256,35 +3258,109 @@ fn dirCreateDirWasi(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, permi...@@ -3256,35 +3258,109 @@ fn dirCreateDirWasi(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, permi
3256fn dirCreateDirWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, permissions: Dir.Permissions) Dir.CreateDirError!void {3258fn dirCreateDirWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, permissions: Dir.Permissions) Dir.CreateDirError!void {
3257 const t: *Threaded = @ptrCast(@alignCast(userdata));3259 const t: *Threaded = @ptrCast(@alignCast(userdata));
3258 _ = t;3260 _ = t;
3259
3260 const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);
3261 _ = permissions; // TODO use this value3261 _ = permissions; // TODO use this value
32623262
3263 const syscall: Syscall = try .start();3263 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
3264 const sub_dir_handle = while (true) {3264 const sub_path_w = sub_path_w_array.span();
3265 break windows.OpenFile(sub_path_w.span(), .{3265 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
3266 .dir = dir.handle,3266
3267 .access_mask = .{3267 var nt_name: windows.UNICODE_STRING = .{
3268 .GENERIC = .{ .READ = true },3268 .Length = path_len_bytes,
3269 .STANDARD = .{ .SYNCHRONIZE = true },3269 .MaximumLength = path_len_bytes,
3270 },3270 .Buffer = @constCast(sub_path_w.ptr),
3271 .creation = .CREATE,3271 };
3272 .filter = .dir_only,3272 const attr: windows.OBJECT_ATTRIBUTES = .{
3273 }) catch |err| switch (err) {3273 .Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
3274 error.IsDir => return syscall.fail(error.Unexpected),3274 .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle,
3275 error.PipeBusy => return syscall.fail(error.Unexpected),3275 .Attributes = .{
3276 error.NoDevice => return syscall.fail(error.Unexpected),3276 .INHERIT = false,
3277 error.WouldBlock => return syscall.fail(error.Unexpected),3277 },
3278 error.AntivirusInterference => return syscall.fail(error.Unexpected),3278 .ObjectName = &nt_name,
3279 error.OperationCanceled => {3279 .SecurityDescriptor = null,
3280 try syscall.checkCancel();3280 .SecurityQualityOfService = null,
3281 continue;3281 };
3282 },3282
3283 else => |e| return syscall.fail(e),3283 var sub_dir_handle: windows.HANDLE = undefined;
3284 };3284 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
3285 var attempt: u5 = 0;
3286 var syscall: Syscall = try .start();
3287 while (true) switch (windows.ntdll.NtCreateFile(
3288 &sub_dir_handle,
3289 .{
3290 .GENERIC = .{ .READ = true },
3291 .STANDARD = .{ .SYNCHRONIZE = true },
3292 },
3293 &attr,
3294 &io_status_block,
3295 null,
3296 .{ .NORMAL = true },
3297 .VALID_FLAGS,
3298 .CREATE,
3299 .{
3300 .DIRECTORY_FILE = true,
3301 .NON_DIRECTORY_FILE = false,
3302 .IO = .SYNCHRONOUS_NONALERT,
3303 .OPEN_REPARSE_POINT = false,
3304 },
3305 null,
3306 0,
3307 )) {
3308 .SUCCESS => {
3309 syscall.finish();
3310 windows.CloseHandle(sub_dir_handle);
3311 return;
3312 },
3313 .CANCELLED => {
3314 try syscall.checkCancel();
3315 continue;
3316 },
3317 .SHARING_VIOLATION => {
3318 // This occurs if the file attempting to be opened is a running
3319 // executable. However, there's a kernel bug: the error may be
3320 // incorrectly returned for an indeterminate amount of time
3321 // after an executable file is closed. Here we work around the
3322 // kernel bug with retry attempts.
3323 syscall.finish();
3324 if (max_windows_kernel_bug_retries - attempt == 0) return error.Unexpected;
3325 try parking_sleep.sleep(.{ .duration = .{
3326 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
3327 .clock = .awake,
3328 } });
3329 attempt += 1;
3330 syscall = try .start();
3331 continue;
3332 },
3333 .DELETE_PENDING => {
3334 // This error means that there *was* a file in this location on
3335 // the file system, but it was deleted. However, the OS is not
3336 // finished with the deletion operation, and so this CreateFile
3337 // call has failed. There is not really a sane way to handle
3338 // this other than retrying the creation after the OS finishes
3339 // the deletion.
3340 syscall.finish();
3341 if (max_windows_kernel_bug_retries - attempt == 0) return error.Unexpected;
3342 try parking_sleep.sleep(.{ .duration = .{
3343 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
3344 .clock = .awake,
3345 } });
3346 attempt += 1;
3347 syscall = try .start();
3348 continue;
3349 },
3350 .OBJECT_NAME_INVALID => return syscall.fail(error.BadPathName),
3351 .OBJECT_NAME_NOT_FOUND => return syscall.fail(error.FileNotFound),
3352 .OBJECT_PATH_NOT_FOUND => return syscall.fail(error.FileNotFound),
3353 .BAD_NETWORK_PATH => return syscall.fail(error.NetworkNotFound), // \\server was not found
3354 .BAD_NETWORK_NAME => return syscall.fail(error.NetworkNotFound), // \\server was found but \\server\share wasn't
3355 .ACCESS_DENIED => return syscall.fail(error.AccessDenied),
3356 .OBJECT_NAME_COLLISION => return syscall.fail(error.PathAlreadyExists),
3357 .NOT_A_DIRECTORY => return syscall.fail(error.NotDir),
3358 .USER_MAPPED_FILE => return syscall.fail(error.AccessDenied),
3359 .INVALID_PARAMETER => |status| return syscall.ntstatusBug(status),
3360 .OBJECT_PATH_SYNTAX_BAD => |status| return syscall.ntstatusBug(status),
3361 .INVALID_HANDLE => |status| return syscall.ntstatusBug(status),
3362 else => |status| return syscall.unexpectedNtstatus(status),
3285 };3363 };
3286 syscall.finish();
3287 windows.CloseHandle(sub_dir_handle);
3288}3364}
32893365
3290fn dirCreateDirPath(3366fn dirCreateDirPath(
...@@ -3363,7 +3439,7 @@ fn dirCreateDirPathOpenWindows(...@@ -3363,7 +3439,7 @@ fn dirCreateDirPathOpenWindows(
3363 };3439 };
33643440
3365 components: while (true) {3441 components: while (true) {
3366 const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path);3442 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, component.path);
3367 const sub_path_w = sub_path_w_array.span();3443 const sub_path_w = sub_path_w_array.span();
3368 const is_last = it.peekNext() == null;3444 const is_last = it.peekNext() == null;
3369 const create_disposition: w.FILE.CREATE_DISPOSITION = if (is_last) .OPEN_IF else .CREATE;3445 const create_disposition: w.FILE.CREATE_DISPOSITION = if (is_last) .OPEN_IF else .CREATE;
...@@ -4064,7 +4140,7 @@ fn dirAccessWindows(...@@ -4064,7 +4140,7 @@ fn dirAccessWindows(
40644140
4065 _ = options; // TODO4141 _ = options; // TODO
40664142
4067 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);4143 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
4068 const sub_path_w = sub_path_w_array.span();4144 const sub_path_w = sub_path_w_array.span();
40694145
4070 if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return;4146 if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return;
...@@ -4285,7 +4361,7 @@ fn dirCreateFileWindows(...@@ -4285,7 +4361,7 @@ fn dirCreateFileWindows(
4285 if (std.mem.eql(u8, sub_path, ".")) return error.IsDir;4361 if (std.mem.eql(u8, sub_path, ".")) return error.IsDir;
4286 if (std.mem.eql(u8, sub_path, "..")) return error.IsDir;4362 if (std.mem.eql(u8, sub_path, "..")) return error.IsDir;
42874363
4288 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);4364 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
4289 const sub_path_w = sub_path_w_array.span();4365 const sub_path_w = sub_path_w_array.span();
4290 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;4366 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
42914367
...@@ -4314,11 +4390,7 @@ fn dirCreateFileWindows(...@@ -4314,11 +4390,7 @@ fn dirCreateFileWindows(
4314 };4390 };
43154391
4316 var io_status_block: windows.IO_STATUS_BLOCK = undefined;4392 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
4317
4318 // There are multiple kernel bugs being worked around with retries.
4319 const max_attempts = 13;
4320 var attempt: u5 = 0;4393 var attempt: u5 = 0;
4321
4322 var handle: windows.HANDLE = undefined;4394 var handle: windows.HANDLE = undefined;
4323 var syscall: Syscall = try .start();4395 var syscall: Syscall = try .start();
4324 while (true) switch (windows.ntdll.NtCreateFile(4396 while (true) switch (windows.ntdll.NtCreateFile(
...@@ -4352,7 +4424,7 @@ fn dirCreateFileWindows(...@@ -4352,7 +4424,7 @@ fn dirCreateFileWindows(
4352 // after an executable file is closed. Here we work around the4424 // after an executable file is closed. Here we work around the
4353 // kernel bug with retry attempts.4425 // kernel bug with retry attempts.
4354 syscall.finish();4426 syscall.finish();
4355 if (max_attempts - attempt == 0) return error.FileBusy;4427 if (max_windows_kernel_bug_retries - attempt == 0) return error.FileBusy;
4356 try parking_sleep.sleep(.{ .duration = .{4428 try parking_sleep.sleep(.{ .duration = .{
4357 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),4429 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
4358 .clock = .awake,4430 .clock = .awake,
...@@ -4368,7 +4440,7 @@ fn dirCreateFileWindows(...@@ -4368,7 +4440,7 @@ fn dirCreateFileWindows(
4368 // call has failed. Here, we simulate the kernel bug being4440 // call has failed. Here, we simulate the kernel bug being
4369 // fixed by sleeping and retrying until the error goes away.4441 // fixed by sleeping and retrying until the error goes away.
4370 syscall.finish();4442 syscall.finish();
4371 if (max_attempts - attempt == 0) return error.FileBusy;4443 if (max_windows_kernel_bug_retries - attempt == 0) return error.FileBusy;
4372 try parking_sleep.sleep(.{ .duration = .{4444 try parking_sleep.sleep(.{ .duration = .{
4373 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),4445 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
4374 .clock = .awake,4446 .clock = .awake,
...@@ -4887,7 +4959,7 @@ fn dirOpenFileWindows(...@@ -4887,7 +4959,7 @@ fn dirOpenFileWindows(
4887) File.OpenError!File {4959) File.OpenError!File {
4888 const t: *Threaded = @ptrCast(@alignCast(userdata));4960 const t: *Threaded = @ptrCast(@alignCast(userdata));
4889 _ = t;4961 _ = t;
4890 const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path);4962 const sub_path_w_array = try sliceToPrefixedFileW(dir.handle, sub_path);
4891 const sub_path_w = sub_path_w_array.span();4963 const sub_path_w = sub_path_w_array.span();
4892 const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;4964 const dir_handle = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle;
4893 return dirOpenFileWtf16(dir_handle, sub_path_w, flags);4965 return dirOpenFileWtf16(dir_handle, sub_path_w, flags);
...@@ -4910,11 +4982,7 @@ pub fn dirOpenFileWtf16(...@@ -4910,11 +4982,7 @@ pub fn dirOpenFileWtf16(
4910 .Buffer = @constCast(sub_path_w.ptr),4982 .Buffer = @constCast(sub_path_w.ptr),
4911 };4983 };
4912 var io_status_block: w.IO_STATUS_BLOCK = undefined;4984 var io_status_block: w.IO_STATUS_BLOCK = undefined;
4913
4914 // There are multiple kernel bugs being worked around with retries.
4915 const max_attempts = 13;
4916 var attempt: u5 = 0;4985 var attempt: u5 = 0;
4917
4918 var syscall: Syscall = try .start();4986 var syscall: Syscall = try .start();
4919 const handle = while (true) {4987 const handle = while (true) {
4920 var result: w.HANDLE = undefined;4988 var result: w.HANDLE = undefined;
...@@ -4966,7 +5034,7 @@ pub fn dirOpenFileWtf16(...@@ -4966,7 +5034,7 @@ pub fn dirOpenFileWtf16(
4966 // after an executable file is closed. Here we work around the5034 // after an executable file is closed. Here we work around the
4967 // kernel bug with retry attempts.5035 // kernel bug with retry attempts.
4968 syscall.finish();5036 syscall.finish();
4969 if (max_attempts - attempt == 0) return error.FileBusy;5037 if (max_windows_kernel_bug_retries - attempt == 0) return error.FileBusy;
4970 try parking_sleep.sleep(.{ .duration = .{5038 try parking_sleep.sleep(.{ .duration = .{
4971 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),5039 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
4972 .clock = .awake,5040 .clock = .awake,
...@@ -4991,7 +5059,7 @@ pub fn dirOpenFileWtf16(...@@ -4991,7 +5059,7 @@ pub fn dirOpenFileWtf16(
4991 // call has failed. Here, we simulate the kernel bug being5059 // call has failed. Here, we simulate the kernel bug being
4992 // fixed by sleeping and retrying until the error goes away.5060 // fixed by sleeping and retrying until the error goes away.
4993 syscall.finish();5061 syscall.finish();
4994 if (max_attempts - attempt == 0) return error.FileBusy;5062 if (max_windows_kernel_bug_retries - attempt == 0) return error.FileBusy;
4995 try parking_sleep.sleep(.{ .duration = .{5063 try parking_sleep.sleep(.{ .duration = .{
4996 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),5064 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
4997 .clock = .awake,5065 .clock = .awake,
...@@ -5151,7 +5219,7 @@ fn dirOpenDirPosix(...@@ -5151,7 +5219,7 @@ fn dirOpenDirPosix(
5151 _ = t;5219 _ = t;
51525220
5153 if (is_windows) {5221 if (is_windows) {
5154 const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);5222 const sub_path_w = try sliceToPrefixedFileW(dir.handle, sub_path);
5155 return dirOpenDirWindows(dir, sub_path_w.span(), options);5223 return dirOpenDirWindows(dir, sub_path_w.span(), options);
5156 }5224 }
51575225
...@@ -5984,30 +6052,22 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,...@@ -5984,30 +6052,22 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,
5984 const t: *Threaded = @ptrCast(@alignCast(userdata));6052 const t: *Threaded = @ptrCast(@alignCast(userdata));
5985 _ = t;6053 _ = t;
59866054
5987 var path_name_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path);6055 var path_name_w = try sliceToPrefixedFileW(dir.handle, sub_path);
59886056
5989 const h_file = handle: {6057 const h_file = handle: {
5990 const syscall: Syscall = try .start();6058 if (OpenFile(path_name_w.span(), .{
5991 while (true) {6059 .dir = dir.handle,
5992 if (windows.OpenFile(path_name_w.span(), .{6060 .access_mask = .{
5993 .dir = dir.handle,6061 .GENERIC = .{ .READ = true },
5994 .access_mask = .{6062 .STANDARD = .{ .SYNCHRONIZE = true },
5995 .GENERIC = .{ .READ = true },6063 },
5996 .STANDARD = .{ .SYNCHRONIZE = true },6064 .creation = .OPEN,
5997 },6065 .filter = .any,
5998 .creation = .OPEN,6066 })) |handle| {
5999 .filter = .any,6067 break :handle handle;
6000 })) |handle| {6068 } else |err| switch (err) {
6001 syscall.finish();6069 error.WouldBlock => unreachable,
6002 break :handle handle;6070 else => |e| return e,
6003 } else |err| switch (err) {
6004 error.WouldBlock => unreachable,
6005 error.OperationCanceled => {
6006 try syscall.checkCancel();
6007 continue;
6008 },
6009 else => |e| return syscall.fail(e),
6010 }
6011 }6071 }
6012 };6072 };
6013 defer windows.CloseHandle(h_file);6073 defer windows.CloseHandle(h_file);
...@@ -6016,9 +6076,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,...@@ -6016,9 +6076,7 @@ fn dirRealPathFileWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8,
60166076
6017fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!usize {6077fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!usize {
6018 var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;6078 var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;
6019 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks6079 const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &wide_buf);
6020 try Thread.checkCancel();
6021 const wide_slice = try windows.GetFinalPathNameByHandle(h_file, .{}, &wide_buf);
60226080
6023 const len = std.unicode.calcWtf8Len(wide_slice);6081 const len = std.unicode.calcWtf8Len(wide_slice);
6024 if (len > out_buffer.len)6082 if (len > out_buffer.len)
...@@ -6027,6 +6085,552 @@ fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!...@@ -6027,6 +6085,552 @@ fn realPathWindows(h_file: windows.HANDLE, out_buffer: []u8) File.RealPathError!
6027 return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);6085 return std.unicode.wtf16LeToWtf8(out_buffer, wide_slice);
6028}6086}
60296087
6088/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`.
6089/// Defaults to DOS volume names.
6090pub const GetFinalPathNameByHandleFormat = struct {
6091 volume_name: enum {
6092 /// Format as DOS volume name
6093 Dos,
6094 /// Format as NT volume name
6095 Nt,
6096 } = .Dos,
6097};
6098
6099pub const GetFinalPathNameByHandleError = error{
6100 AccessDenied,
6101 FileNotFound,
6102 NameTooLong,
6103 /// The volume does not contain a recognized file system. File system
6104 /// drivers might not be loaded, or the volume may be corrupt.
6105 UnrecognizedVolume,
6106} || Io.Cancelable || Io.UnexpectedError;
6107
6108/// Returns canonical (normalized) path of handle.
6109/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include
6110/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`).
6111/// If DOS volume name format is selected, note that this function does *not* prepend
6112/// `\\?\` prefix to the resultant path.
6113pub fn GetFinalPathNameByHandle(
6114 hFile: windows.HANDLE,
6115 fmt: GetFinalPathNameByHandleFormat,
6116 out_buffer: []u16,
6117) GetFinalPathNameByHandleError![]u16 {
6118 const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) {
6119 // we assume InvalidHandle is close enough to FileNotFound in semantics
6120 // to not further complicate the error set
6121 error.InvalidHandle => return error.FileNotFound,
6122 else => |e| return e,
6123 };
6124
6125 switch (fmt.volume_name) {
6126 .Nt => {
6127 // the returned path is already in .Nt format
6128 return final_path;
6129 },
6130 .Dos => {
6131 // parse the string to separate volume path from file path
6132 const device_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");
6133
6134 // We aren't entirely sure of the structure of the path returned by
6135 // QueryObjectName in all contexts/environments.
6136 // This code is written to cover the various cases that have
6137 // been encountered and solved appropriately. But note that there's
6138 // no easy way to verify that they have all been tackled!
6139 // (Unless you, the reader knows of one then please do action that!)
6140 if (!std.mem.startsWith(u16, final_path, device_prefix)) {
6141 // Wine seems to return NT namespaced paths starting with \??\ from QueryObjectName
6142 // (e.g. `\??\Z:\some\path\to\a\file.txt`), in which case we can just strip the
6143 // prefix to turn it into an absolute path.
6144 // https://github.com/ziglang/zig/issues/26029
6145 // https://bugs.winehq.org/show_bug.cgi?id=39569
6146 return windows.ntToWin32Namespace(final_path, out_buffer) catch |err| switch (err) {
6147 error.NotNtPath => return error.Unexpected,
6148 error.NameTooLong => |e| return e,
6149 };
6150 }
6151
6152 const file_path_begin_index = std.mem.findPos(u16, final_path, device_prefix.len, &[_]u16{'\\'}) orelse unreachable;
6153 const volume_name_u16 = final_path[0..file_path_begin_index];
6154 const device_name_u16 = volume_name_u16[device_prefix.len..];
6155 const file_name_u16 = final_path[file_path_begin_index..];
6156
6157 // MUP is Multiple UNC Provider, and indicates that the path is a UNC
6158 // path. In this case, the canonical UNC path can be gotten by just
6159 // dropping the \Device\Mup\ and making sure the path begins with \\
6160 if (std.mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) {
6161 out_buffer[0] = '\\';
6162 @memmove(out_buffer[1..][0..file_name_u16.len], file_name_u16);
6163 return out_buffer[0 .. 1 + file_name_u16.len];
6164 }
6165
6166 // Get DOS volume name. DOS volume names are actually symbolic link objects to the
6167 // actual NT volume. For example:
6168 // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
6169 const MIN_SIZE = @sizeOf(windows.MOUNTMGR_MOUNT_POINT) + windows.MAX_PATH;
6170 // We initialize the input buffer to all zeros for convenience since
6171 // `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this.
6172 var input_buf: [MIN_SIZE]u8 align(@alignOf(windows.MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE;
6173 var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(windows.MOUNTMGR_MOUNT_POINTS)) = undefined;
6174
6175 // This surprising path is a filesystem path to the mount manager on Windows.
6176 // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
6177 // This is the NT namespaced version of \\.\MountPointManager
6178 const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager");
6179 const mgmt_handle = OpenFile(mgmt_path_u16, .{
6180 .access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } },
6181 .creation = .OPEN,
6182 }) catch |err| switch (err) {
6183 error.IsDir => return error.Unexpected,
6184 error.NotDir => return error.Unexpected,
6185 error.NoDevice => return error.Unexpected,
6186 error.AccessDenied => return error.Unexpected,
6187 error.PipeBusy => return error.Unexpected,
6188 error.FileBusy => return error.Unexpected,
6189 error.PathAlreadyExists => return error.Unexpected,
6190 error.WouldBlock => return error.Unexpected,
6191 error.NetworkNotFound => return error.Unexpected,
6192 error.AntivirusInterference => return error.Unexpected,
6193 error.BadPathName => return error.Unexpected,
6194 else => |e| return e,
6195 };
6196 defer windows.CloseHandle(mgmt_handle);
6197
6198 var input_struct: *windows.MOUNTMGR_MOUNT_POINT = @ptrCast(&input_buf[0]);
6199 input_struct.DeviceNameOffset = @sizeOf(windows.MOUNTMGR_MOUNT_POINT);
6200 input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2);
6201 @memcpy(input_buf[@sizeOf(windows.MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr)));
6202
6203 {
6204 const rc = windows.DeviceIoControl(mgmt_handle, windows.IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf });
6205 switch (rc) {
6206 .SUCCESS => {},
6207 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
6208 else => return windows.unexpectedStatus(rc),
6209 }
6210 }
6211 const mount_points_struct: *const windows.MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]);
6212
6213 const mount_points = @as(
6214 [*]const windows.MOUNTMGR_MOUNT_POINT,
6215 @ptrCast(&mount_points_struct.MountPoints[0]),
6216 )[0..mount_points_struct.NumberOfMountPoints];
6217
6218 for (mount_points) |mount_point| {
6219 const symlink = @as(
6220 [*]const u16,
6221 @ptrCast(@alignCast(&output_buf[mount_point.SymbolicLinkNameOffset])),
6222 )[0 .. mount_point.SymbolicLinkNameLength / 2];
6223
6224 // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
6225 // with traditional DOS drive letters, so pick the first one available.
6226 var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\");
6227 const prefix = prefix_buf[0..prefix_buf.len];
6228
6229 if (std.mem.startsWith(u16, symlink, prefix)) {
6230 const drive_letter = symlink[prefix.len..];
6231
6232 if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;
6233
6234 @memcpy(out_buffer[0..drive_letter.len], drive_letter);
6235 @memmove(out_buffer[drive_letter.len..][0..file_name_u16.len], file_name_u16);
6236 const total_len = drive_letter.len + file_name_u16.len;
6237
6238 // Validate that DOS does not contain any spurious nul bytes.
6239 assert(std.mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
6240
6241 return out_buffer[0..total_len];
6242 } else if (mountmgrIsVolumeName(symlink)) {
6243 // If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d},
6244 // then it is a volume mounted as a path rather than a drive letter. We need to
6245 // query the mount manager again to get the DOS path for the volume.
6246
6247 // 49 is the maximum length accepted by mountmgrIsVolumeName
6248 const vol_input_size = @sizeOf(windows.MOUNTMGR_TARGET_NAME) + (49 * 2);
6249 var vol_input_buf: [vol_input_size]u8 align(@alignOf(windows.MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size;
6250 // Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path,
6251 // and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>).
6252 // However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here.
6253 const min_output_size = @sizeOf(windows.MOUNTMGR_VOLUME_PATHS) + (windows.PATH_MAX_WIDE * 2);
6254 var vol_output_buf: [min_output_size]u8 align(@alignOf(windows.MOUNTMGR_VOLUME_PATHS)) = undefined;
6255
6256 var vol_input_struct: *windows.MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]);
6257 vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2);
6258 @memcpy(@as([*]windows.WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink);
6259
6260 const rc = windows.DeviceIoControl(mgmt_handle, windows.IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf });
6261 switch (rc) {
6262 .SUCCESS => {},
6263 .UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume,
6264 else => return windows.unexpectedStatus(rc),
6265 }
6266 const volume_paths_struct: *const windows.MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]);
6267 const volume_path = std.mem.sliceTo(@as(
6268 [*]const u16,
6269 &volume_paths_struct.MultiSz,
6270 )[0 .. volume_paths_struct.MultiSzLength / 2], 0);
6271
6272 if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong;
6273
6274 // `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where
6275 // we want to place the filename before returning. Here are the possible overlapping cases:
6276 //
6277 // out_buffer: [filename]
6278 // dest: [___(a)___] [___(b)___]
6279 //
6280 // In the case of (a), we need to copy forwards, and in the case of (b) we need
6281 // to copy backwards. We also need to do this before copying the volume path because
6282 // it could overwrite the file_name_u16 memory.
6283 const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len];
6284 @memmove(file_name_dest, file_name_u16);
6285 @memcpy(out_buffer[0..volume_path.len], volume_path);
6286 const total_len = volume_path.len + file_name_u16.len;
6287
6288 // Validate that DOS does not contain any spurious nul bytes.
6289 assert(std.mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
6290
6291 return out_buffer[0..total_len];
6292 }
6293 }
6294
6295 // If we've ended up here, then something went wrong/is corrupted in the OS,
6296 // so error out!
6297 return error.FileNotFound;
6298 },
6299 }
6300}
6301
6302test GetFinalPathNameByHandle {
6303 if (builtin.os.tag != .windows)
6304 return;
6305
6306 //any file will do
6307 var tmp = std.testing.tmpDir(.{});
6308 defer tmp.cleanup();
6309 const handle = tmp.dir.handle;
6310 var buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
6311
6312 //check with sufficient size
6313 const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer);
6314 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer);
6315
6316 const required_len_in_u16 = nt_path.len + @divExact(@intFromPtr(nt_path.ptr) - @intFromPtr(&buffer), 2) + 1;
6317 //check with insufficient size
6318 try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1]));
6319 try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1]));
6320
6321 //check with exactly-sufficient size
6322 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]);
6323 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]);
6324}
6325
6326/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h
6327fn mountmgrIsVolumeName(name: []const u16) bool {
6328 return (name.len == 48 or (name.len == 49 and name[48] == std.mem.nativeToLittle(u16, '\\'))) and
6329 name[0] == std.mem.nativeToLittle(u16, '\\') and
6330 (name[1] == std.mem.nativeToLittle(u16, '?') or name[1] == std.mem.nativeToLittle(u16, '\\')) and
6331 name[2] == std.mem.nativeToLittle(u16, '?') and
6332 name[3] == std.mem.nativeToLittle(u16, '\\') and
6333 std.mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and
6334 name[19] == std.mem.nativeToLittle(u16, '-') and
6335 name[24] == std.mem.nativeToLittle(u16, '-') and
6336 name[29] == std.mem.nativeToLittle(u16, '-') and
6337 name[34] == std.mem.nativeToLittle(u16, '-') and
6338 name[47] == std.mem.nativeToLittle(u16, '}');
6339}
6340
6341test mountmgrIsVolumeName {
6342 @setEvalBranchQuota(2000);
6343 const L = std.unicode.utf8ToUtf16LeStringLiteral;
6344 try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
6345 try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
6346 try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
6347 try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
6348 try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
6349 try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo")));
6350 try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}")));
6351}
6352
6353pub const QueryObjectNameError = error{
6354 AccessDenied,
6355 InvalidHandle,
6356 NameTooLong,
6357 Unexpected,
6358};
6359
6360pub fn QueryObjectName(handle: windows.HANDLE, out_buffer: []u16) QueryObjectNameError![]u16 {
6361 const out_buffer_aligned = std.mem.alignInSlice(out_buffer, @alignOf(windows.OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong;
6362
6363 const info: *windows.OBJECT_NAME_INFORMATION = @ptrCast(out_buffer_aligned);
6364 // buffer size is specified in bytes
6365 const out_buffer_len = std.math.cast(windows.ULONG, out_buffer_aligned.len * 2) orelse std.math.maxInt(windows.ULONG);
6366 // last argument would return the length required for full_buffer, not exposed here
6367 return switch (windows.ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null)) {
6368 .SUCCESS => blk: {
6369 // info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0)
6370 // if the object was "unnamed", not sure if this can happen for file handles
6371 if (info.Name.MaximumLength == 0) break :blk error.Unexpected;
6372 // resulting string length is specified in bytes
6373 const path_length_unterminated = @divExact(info.Name.Length, 2);
6374 break :blk info.Name.Buffer.?[0..path_length_unterminated];
6375 },
6376 .ACCESS_DENIED => error.AccessDenied,
6377 .INVALID_HANDLE => error.InvalidHandle,
6378 // triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH),
6379 // or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL)
6380 .INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong,
6381 else => |e| windows.unexpectedStatus(e),
6382 };
6383}
6384
6385test QueryObjectName {
6386 if (builtin.os.tag != .windows)
6387 return;
6388
6389 //any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.
6390 var tmp = std.testing.tmpDir(.{});
6391 defer tmp.cleanup();
6392 const handle = tmp.dir.handle;
6393 var out_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
6394
6395 const result_path = try QueryObjectName(handle, &out_buffer);
6396 const required_len_in_u16 = result_path.len + @divExact(@intFromPtr(result_path.ptr) - @intFromPtr(&out_buffer), 2) + 1;
6397 //insufficient size
6398 try std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1]));
6399 //exactly-sufficient size
6400 _ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]);
6401}
6402
6403const Wtf16ToPrefixedFileWError = error{
6404 AccessDenied,
6405 FileNotFound,
6406} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
6407
6408/// Converts the `path` to WTF16, null-terminated. If the path contains any
6409/// namespace prefix, or is anything but a relative path (rooted, drive relative,
6410/// etc) the result will have the NT-style prefix `\??\`.
6411///
6412/// Similar to RtlDosPathNameToNtPathName_U with a few differences:
6413/// - Does not allocate on the heap.
6414/// - Relative paths are kept as relative unless they contain too many ..
6415/// components, in which case they are resolved against the `dir` if it
6416/// is non-null, or the CWD if it is null.
6417/// - Special case device names like COM1, NUL, etc are not handled specially (TODO)
6418/// - . and space are not stripped from the end of relative paths (potential TODO)
6419pub fn wToPrefixedFileW(dir: ?windows.HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!WindowsPathSpace {
6420 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
6421 if (windows.hasCommonNtPrefix(u16, path)) {
6422 // TODO: Figure out a way to design an API that can avoid the copy for NT,
6423 // since it is always returned fully unmodified.
6424 var path_space: WindowsPathSpace = undefined;
6425 path_space.data[0..nt_prefix.len].* = nt_prefix;
6426 const len_after_prefix = path.len - nt_prefix.len;
6427 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
6428 path_space.len = path.len;
6429 path_space.data[path_space.len] = 0;
6430 return path_space;
6431 } else {
6432 const path_type = Dir.path.getWin32PathType(u16, path);
6433 var path_space: WindowsPathSpace = undefined;
6434 if (path_type == .local_device) {
6435 switch (getLocalDevicePathType(u16, path)) {
6436 .verbatim => {
6437 path_space.data[0..nt_prefix.len].* = nt_prefix;
6438 const len_after_prefix = path.len - nt_prefix.len;
6439 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
6440 path_space.len = path.len;
6441 path_space.data[path_space.len] = 0;
6442 return path_space;
6443 },
6444 .local_device, .fake_verbatim => {
6445 const path_byte_len = windows.ntdll.RtlGetFullPathName_U(
6446 path.ptr,
6447 path_space.data.len * 2,
6448 &path_space.data,
6449 null,
6450 );
6451 if (path_byte_len == 0) {
6452 // TODO: This may not be the right error
6453 return error.BadPathName;
6454 } else if (path_byte_len / 2 > path_space.data.len) {
6455 return error.NameTooLong;
6456 }
6457 path_space.len = path_byte_len / 2;
6458 // Both prefixes will be normalized but retained, so all
6459 // we need to do now is replace them with the NT prefix
6460 path_space.data[0..nt_prefix.len].* = nt_prefix;
6461 return path_space;
6462 },
6463 }
6464 }
6465 relative: {
6466 if (path_type == .relative) {
6467 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
6468 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
6469
6470 // TODO: Potentially strip all trailing . and space characters from the
6471 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
6472 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
6473 // are allowed, but such paths may not interact well with Windows (i.e.
6474 // files with these paths can't be deleted from explorer.exe, etc).
6475 // This could be something that normalizePath may want to do.
6476
6477 @memcpy(path_space.data[0..path.len], path);
6478 // Try to normalize, but if we get too many parent directories,
6479 // then we need to start over and use RtlGetFullPathName_U instead.
6480 path_space.len = windows.normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
6481 error.TooManyParentDirs => break :relative,
6482 };
6483 path_space.data[path_space.len] = 0;
6484 return path_space;
6485 }
6486 }
6487 // We now know we are going to return an absolute NT path, so
6488 // we can unconditionally prefix it with the NT prefix.
6489 path_space.data[0..nt_prefix.len].* = nt_prefix;
6490 if (path_type == .root_local_device) {
6491 // `\\.` and `\\?` always get converted to `\??\` exactly, so
6492 // we can just stop here
6493 path_space.len = nt_prefix.len;
6494 path_space.data[path_space.len] = 0;
6495 return path_space;
6496 }
6497 const path_buf_offset = switch (path_type) {
6498 // UNC paths will always start with `\\`. However, we want to
6499 // end up with something like `\??\UNC\server\share`, so to get
6500 // RtlGetFullPathName to write into the spot we want the `server`
6501 // part to end up, we need to provide an offset such that
6502 // the `\\` part gets written where the `C\` of `UNC\` will be
6503 // in the final NT path.
6504 .unc_absolute => nt_prefix.len + 2,
6505 else => nt_prefix.len,
6506 };
6507 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
6508 const path_to_get: [:0]const u16 = path_to_get: {
6509 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
6510 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
6511 if (path_type != .relative or dir == null) {
6512 break :path_to_get path;
6513 }
6514 // We can also skip GetFinalPathNameByHandle if the handle matches
6515 // the handle returned by Io.Dir.cwd()
6516 if (dir.? == Io.Dir.cwd().handle) {
6517 break :path_to_get path;
6518 }
6519 // At this point, we know we have a relative path that had too many
6520 // `..` components to be resolved by normalizePath, so we need to
6521 // convert it into an absolute path and let RtlGetFullPathName_U
6522 // canonicalize it. We do this by getting the path of the `dir`
6523 // and appending the relative path to it.
6524 var dir_path_buf: [windows.PATH_MAX_WIDE:0]u16 = undefined;
6525 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
6526 // This mapping is not correct; it is actually expected
6527 // that calling GetFinalPathNameByHandle might return
6528 // error.UnrecognizedVolume, and in fact has been observed
6529 // in the wild. The problem is that wToPrefixedFileW was
6530 // never intended to make *any* OS syscall APIs. It's only
6531 // supposed to convert a string to one that is eligible to
6532 // be used in the ntdll syscalls.
6533 //
6534 // To solve this, this function needs to no longer call
6535 // GetFinalPathNameByHandle under any conditions, or the
6536 // calling function needs to get reworked to not need to
6537 // call this function.
6538 //
6539 // This may involve making breaking API changes.
6540 error.UnrecognizedVolume => return error.Unexpected,
6541 else => |e| return e,
6542 };
6543 if (dir_path.len + 1 + path.len > windows.PATH_MAX_WIDE) {
6544 return error.NameTooLong;
6545 }
6546 // We don't have to worry about potentially doubling up path separators
6547 // here since RtlGetFullPathName_U will handle canonicalizing it.
6548 dir_path_buf[dir_path.len] = '\\';
6549 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
6550 const full_len = dir_path.len + 1 + path.len;
6551 dir_path_buf[full_len] = 0;
6552 break :path_to_get dir_path_buf[0..full_len :0];
6553 };
6554 const path_byte_len = windows.ntdll.RtlGetFullPathName_U(
6555 path_to_get.ptr,
6556 buf_len * 2,
6557 path_space.data[path_buf_offset..].ptr,
6558 null,
6559 );
6560 if (path_byte_len == 0) {
6561 // TODO: This may not be the right error
6562 return error.BadPathName;
6563 } else if (path_byte_len / 2 > buf_len) {
6564 return error.NameTooLong;
6565 }
6566 path_space.len = path_buf_offset + (path_byte_len / 2);
6567 if (path_type == .unc_absolute) {
6568 // Now add in the UNC, the `C` should overwrite the first `\` of the
6569 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
6570 assert(path_space.data[path_buf_offset] == '\\');
6571 assert(path_space.data[path_buf_offset + 1] == '\\');
6572 const unc = [_]u16{ 'U', 'N', 'C' };
6573 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
6574 }
6575 return path_space;
6576 }
6577}
6578
6579const LocalDevicePathType = enum {
6580 /// `\\.\` (path separators can be `\` or `/`)
6581 local_device,
6582 /// `\\?\`
6583 /// When converted to an NT path, everything past the prefix is left
6584 /// untouched and `\\?\` is replaced by `\??\`.
6585 verbatim,
6586 /// `\\?\` without all path separators being `\`.
6587 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
6588 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
6589 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
6590 /// be treated as part of the final path])
6591 fake_verbatim,
6592};
6593
6594/// Only relevant for Win32 -> NT path conversion.
6595/// Asserts `path` is of type `Dir.path.Win32PathType.local_device`.
6596fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
6597 if (std.debug.runtime_safety) {
6598 assert(Dir.path.getWin32PathType(T, path) == .local_device);
6599 }
6600
6601 const backslash = std.mem.nativeToLittle(T, '\\');
6602 const all_backslash = path[0] == backslash and
6603 path[1] == backslash and
6604 path[3] == backslash;
6605 return switch (path[2]) {
6606 std.mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
6607 std.mem.nativeToLittle(T, '.') => .local_device,
6608 else => unreachable,
6609 };
6610}
6611
6612pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError;
6613
6614/// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path.
6615/// https://wtf-8.codeberg.page/
6616pub fn sliceToPrefixedFileW(dir: ?windows.HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!WindowsPathSpace {
6617 var temp_path: WindowsPathSpace = undefined;
6618 temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) {
6619 error.InvalidWtf8 => return error.BadPathName,
6620 };
6621 temp_path.data[temp_path.len] = 0;
6622 return wToPrefixedFileW(dir, temp_path.span());
6623}
6624
6625pub const WindowsPathSpace = struct {
6626 data: [windows.PATH_MAX_WIDE:0]u16,
6627 len: usize,
6628
6629 pub fn span(self: *const WindowsPathSpace) [:0]const u16 {
6630 return self.data[0..self.len :0];
6631 }
6632};
6633
6030fn dirRealPathFilePosix(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, out_buffer: []u8) Dir.RealPathFileError!usize {6634fn dirRealPathFilePosix(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, out_buffer: []u8) Dir.RealPathFileError!usize {
6031 if (native_os == .wasi) return error.OperationUnsupported;6635 if (native_os == .wasi) return error.OperationUnsupported;
60326636
...@@ -6478,7 +7082,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov...@@ -6478,7 +7082,7 @@ fn dirDeleteWindows(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, remov
6478 _ = t;7082 _ = t;
6479 const w = windows;7083 const w = windows;
64807084
6481 const sub_path_w_buf = try w.sliceToPrefixedFileW(dir.handle, sub_path);7085 const sub_path_w_buf = try sliceToPrefixedFileW(dir.handle, sub_path);
6482 const sub_path_w = sub_path_w_buf.span();7086 const sub_path_w = sub_path_w_buf.span();
64837087
6484 const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2));7088 const path_len_bytes = @as(u16, @intCast(sub_path_w.len * 2));
...@@ -6759,37 +7363,29 @@ fn dirRenameWindowsInner(...@@ -6759,37 +7363,29 @@ fn dirRenameWindowsInner(
6759 replace_if_exists: bool,7363 replace_if_exists: bool,
6760) Dir.RenamePreserveError!void {7364) Dir.RenamePreserveError!void {
6761 const w = windows;7365 const w = windows;
6762 const old_path_w_buf = try windows.sliceToPrefixedFileW(old_dir.handle, old_sub_path);7366 const old_path_w_buf = try sliceToPrefixedFileW(old_dir.handle, old_sub_path);
6763 const old_path_w = old_path_w_buf.span();7367 const old_path_w = old_path_w_buf.span();
6764 const new_path_w_buf = try windows.sliceToPrefixedFileW(new_dir.handle, new_sub_path);7368 const new_path_w_buf = try sliceToPrefixedFileW(new_dir.handle, new_sub_path);
6765 const new_path_w = new_path_w_buf.span();7369 const new_path_w = new_path_w_buf.span();
67667370
6767 const src_fd = src_fd: {7371 const src_fd = src_fd: {
6768 const syscall: Syscall = try .start();7372 if (OpenFile(old_path_w, .{
6769 while (true) {7373 .dir = old_dir.handle,
6770 if (w.OpenFile(old_path_w, .{7374 .access_mask = .{
6771 .dir = old_dir.handle,7375 .GENERIC = .{ .WRITE = true },
6772 .access_mask = .{7376 .STANDARD = .{
6773 .GENERIC = .{ .WRITE = true },7377 .RIGHTS = .{ .DELETE = true },
6774 .STANDARD = .{7378 .SYNCHRONIZE = true,
6775 .RIGHTS = .{ .DELETE = true },
6776 .SYNCHRONIZE = true,
6777 },
6778 },
6779 .creation = .OPEN,
6780 .filter = .any, // This function is supposed to rename both files and directories.
6781 .follow_symlinks = false,
6782 })) |handle| {
6783 syscall.finish();
6784 break :src_fd handle;
6785 } else |err| switch (err) {
6786 error.WouldBlock => unreachable, // Not possible without `.share_access_nonblocking = true`.
6787 error.OperationCanceled => {
6788 try syscall.checkCancel();
6789 continue;
6790 },7379 },
6791 else => |e| return e,7380 },
6792 }7381 .creation = .OPEN,
7382 .filter = .any, // This function is supposed to rename both files and directories.
7383 .follow_symlinks = false,
7384 })) |handle| {
7385 break :src_fd handle;
7386 } else |err| switch (err) {
7387 error.WouldBlock => unreachable, // Not possible without `.share_access_nonblocking = true`.
7388 else => |e| return e,
6793 }7389 }
6794 };7390 };
6795 defer w.CloseHandle(src_fd);7391 defer w.CloseHandle(src_fd);
...@@ -7092,7 +7688,7 @@ fn dirSymLinkWindows(...@@ -7092,7 +7688,7 @@ fn dirSymLinkWindows(
7092 // Target path does not use sliceToPrefixedFileW because certain paths7688 // Target path does not use sliceToPrefixedFileW because certain paths
7093 // are handled differently when creating a symlink than they would be7689 // are handled differently when creating a symlink than they would be
7094 // when converting to an NT namespaced path.7690 // when converting to an NT namespaced path.
7095 var target_path_w: w.PathSpace = undefined;7691 var target_path_w: WindowsPathSpace = undefined;
7096 target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path);7692 target_path_w.len = try w.wtf8ToWtf16Le(&target_path_w.data, target_path);
7097 target_path_w.data[target_path_w.len] = 0;7693 target_path_w.data[target_path_w.len] = 0;
7098 // However, we need to canonicalize any path separators to `\`, since if7694 // However, we need to canonicalize any path separators to `\`, since if
...@@ -7104,7 +7700,7 @@ fn dirSymLinkWindows(...@@ -7104,7 +7700,7 @@ fn dirSymLinkWindows(
7104 std.mem.nativeToLittle(u16, '\\'),7700 std.mem.nativeToLittle(u16, '\\'),
7105 );7701 );
71067702
7107 const sym_link_path_w = try w.sliceToPrefixedFileW(dir.handle, sym_link_path);7703 const sym_link_path_w = try sliceToPrefixedFileW(dir.handle, sym_link_path);
71087704
7109 const SYMLINK_DATA = extern struct {7705 const SYMLINK_DATA = extern struct {
7110 ReparseTag: w.IO_REPARSE_TAG,7706 ReparseTag: w.IO_REPARSE_TAG,
...@@ -7118,32 +7714,25 @@ fn dirSymLinkWindows(...@@ -7118,32 +7714,25 @@ fn dirSymLinkWindows(
7118 };7714 };
71197715
7120 const symlink_handle = handle: {7716 const symlink_handle = handle: {
7121 const syscall: Syscall = try .start();7717 if (OpenFile(sym_link_path_w.span(), .{
7122 while (true) {7718 .access_mask = .{
7123 if (w.OpenFile(sym_link_path_w.span(), .{7719 .GENERIC = .{ .READ = true, .WRITE = true },
7124 .access_mask = .{7720 .STANDARD = .{ .SYNCHRONIZE = true },
7125 .GENERIC = .{ .READ = true, .WRITE = true },7721 },
7126 .STANDARD = .{ .SYNCHRONIZE = true },7722 .dir = dir.handle,
7127 },7723 .creation = .CREATE,
7128 .dir = dir.handle,7724 .filter = if (flags.is_directory) .dir_only else .non_directory_only,
7129 .creation = .CREATE,7725 })) |handle| {
7130 .filter = if (flags.is_directory) .dir_only else .non_directory_only,7726 break :handle handle;
7131 })) |handle| {7727 } else |err| switch (err) {
7132 syscall.finish();7728 error.IsDir => return error.PathAlreadyExists,
7133 break :handle handle;7729 error.NotDir => return error.Unexpected,
7134 } else |err| switch (err) {7730 error.WouldBlock => return error.Unexpected,
7135 error.IsDir => return syscall.fail(error.PathAlreadyExists),7731 error.PipeBusy => return error.Unexpected,
7136 error.NotDir => return syscall.fail(error.Unexpected),7732 error.FileBusy => return error.Unexpected,
7137 error.WouldBlock => return syscall.fail(error.Unexpected),7733 error.NoDevice => return error.Unexpected,
7138 error.PipeBusy => return syscall.fail(error.Unexpected),7734 error.AntivirusInterference => return error.Unexpected,
7139 error.NoDevice => return syscall.fail(error.Unexpected),7735 else => |e| return e,
7140 error.AntivirusInterference => return syscall.fail(error.Unexpected),
7141 error.OperationCanceled => {
7142 try syscall.checkCancel();
7143 continue;
7144 },
7145 else => |e| return e,
7146 }
7147 }7736 }
7148 };7737 };
7149 defer w.CloseHandle(symlink_handle);7738 defer w.CloseHandle(symlink_handle);
...@@ -7158,7 +7747,7 @@ fn dirSymLinkWindows(...@@ -7158,7 +7747,7 @@ fn dirSymLinkWindows(
7158 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw7747 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createsymboliclinkw
7159 var is_target_absolute = false;7748 var is_target_absolute = false;
7160 const final_target_path = target_path: {7749 const final_target_path = target_path: {
7161 if (w.hasCommonNtPrefix(u16, target_path_w.span())) {7750 if (windows.hasCommonNtPrefix(u16, target_path_w.span())) {
7162 // Already an NT path, no need to do anything to it7751 // Already an NT path, no need to do anything to it
7163 break :target_path target_path_w.span();7752 break :target_path target_path_w.span();
7164 } else {7753 } else {
...@@ -7176,7 +7765,7 @@ fn dirSymLinkWindows(...@@ -7176,7 +7765,7 @@ fn dirSymLinkWindows(
7176 break :target_path target_path_w.span(),7765 break :target_path target_path_w.span(),
7177 }7766 }
7178 }7767 }
7179 var prefixed_target_path = try w.wToPrefixedFileW(dir.handle, target_path_w.span());7768 var prefixed_target_path = try wToPrefixedFileW(dir.handle, target_path_w.span());
7180 // We do this after prefixing to ensure that drive-relative paths are treated as absolute7769 // We do this after prefixing to ensure that drive-relative paths are treated as absolute
7181 is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());7770 is_target_absolute = Dir.path.isAbsoluteWindowsWtf16(prefixed_target_path.span());
7182 break :target_path prefixed_target_path.span();7771 break :target_path prefixed_target_path.span();
...@@ -7322,7 +7911,7 @@ fn dirReadLink(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, buffer: []...@@ -7322,7 +7911,7 @@ fn dirReadLink(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, buffer: []
7322fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize {7911fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize {
7323 // This gets used once for `sub_path` and then reused again temporarily7912 // This gets used once for `sub_path` and then reused again temporarily
7324 // before converting back to `buffer`.7913 // before converting back to `buffer`.
7325 var sub_path_w_buf = try windows.sliceToPrefixedFileW(dir.handle, sub_path);7914 var sub_path_w_buf = try sliceToPrefixedFileW(dir.handle, sub_path);
7326 const sub_path_w = sub_path_w_buf.span();7915 const sub_path_w = sub_path_w_buf.span();
7327 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;7916 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
7328 var nt_name: windows.UNICODE_STRING = .{7917 var nt_name: windows.UNICODE_STRING = .{
...@@ -7336,11 +7925,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink...@@ -7336,11 +7925,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink
7336 };7925 };
7337 var io_status_block: windows.IO_STATUS_BLOCK = undefined;7926 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
7338 var result_handle: windows.HANDLE = undefined;7927 var result_handle: windows.HANDLE = undefined;
7339
7340 // There are multiple kernel bugs being worked around with retries.
7341 const max_attempts = 13;
7342 var attempt: u5 = 0;7928 var attempt: u5 = 0;
7343
7344 var syscall: Syscall = try .start();7929 var syscall: Syscall = try .start();
7345 while (true) switch (windows.ntdll.NtCreateFile(7930 while (true) switch (windows.ntdll.NtCreateFile(
7346 &result_handle,7931 &result_handle,
...@@ -7380,7 +7965,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink...@@ -7380,7 +7965,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink
7380 // after an executable file is closed. Here we work around the7965 // after an executable file is closed. Here we work around the
7381 // kernel bug with retry attempts.7966 // kernel bug with retry attempts.
7382 syscall.finish();7967 syscall.finish();
7383 if (max_attempts - attempt == 0) return error.FileBusy;7968 if (max_windows_kernel_bug_retries - attempt == 0) return error.FileBusy;
7384 try parking_sleep.sleep(.{ .duration = .{7969 try parking_sleep.sleep(.{ .duration = .{
7385 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),7970 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
7386 .clock = .awake,7971 .clock = .awake,
...@@ -7396,7 +7981,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink...@@ -7396,7 +7981,7 @@ fn dirReadLinkWindows(dir: Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLink
7396 // call has failed. Here, we simulate the kernel bug being7981 // call has failed. Here, we simulate the kernel bug being
7397 // fixed by sleeping and retrying until the error goes away.7982 // fixed by sleeping and retrying until the error goes away.
7398 syscall.finish();7983 syscall.finish();
7399 if (max_attempts - attempt == 0) return error.FileBusy;7984 if (max_windows_kernel_bug_retries - attempt == 0) return error.FileBusy;
7400 try parking_sleep.sleep(.{ .duration = .{7985 try parking_sleep.sleep(.{ .duration = .{
7401 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),7986 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
7402 .clock = .awake,7987 .clock = .awake,
...@@ -9586,7 +10171,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.O...@@ -9586,7 +10171,7 @@ fn processExecutableOpen(userdata: ?*anyopaque, flags: File.OpenFlags) process.O
9586 // the file, we can let the openFileW call follow the symlink for us.10171 // the file, we can let the openFileW call follow the symlink for us.
9587 const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName;10172 const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName;
9588 const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];10173 const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0];
9589 const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name);10174 const prefixed_path_w = try wToPrefixedFileW(null, image_path_name);
9590 return dirOpenFileWtf16(null, prefixed_path_w.span(), flags);10175 return dirOpenFileWtf16(null, prefixed_path_w.span(), flags);
9591 },10176 },
9592 .driverkit,10177 .driverkit,
...@@ -9794,37 +10379,28 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut...@@ -9794,37 +10379,28 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) process.Execut
9794 // If ImagePathName is a symlink, then it will contain the path of the10379 // If ImagePathName is a symlink, then it will contain the path of the
9795 // symlink, not the path that the symlink points to. We want the path10380 // symlink, not the path that the symlink points to. We want the path
9796 // that the symlink points to, though, so we need to get the realpath.10381 // that the symlink points to, though, so we need to get the realpath.
9797 var path_name_w_buf = try w.wToPrefixedFileW(null, image_path_name);10382 var path_name_w_buf = try wToPrefixedFileW(null, image_path_name);
979810383
9799 const h_file = handle: {10384 const h_file = handle: {
9800 const syscall: Syscall = try .start();10385 if (OpenFile(path_name_w_buf.span(), .{
9801 while (true) {10386 .dir = null,
9802 if (w.OpenFile(path_name_w_buf.span(), .{10387 .access_mask = .{
9803 .dir = null,10388 .GENERIC = .{ .READ = true },
9804 .access_mask = .{10389 .STANDARD = .{ .SYNCHRONIZE = true },
9805 .GENERIC = .{ .READ = true },10390 },
9806 .STANDARD = .{ .SYNCHRONIZE = true },10391 .creation = .OPEN,
9807 },10392 .filter = .any,
9808 .creation = .OPEN,10393 })) |handle| {
9809 .filter = .any,10394 break :handle handle;
9810 })) |handle| {10395 } else |err| switch (err) {
9811 syscall.finish();10396 error.WouldBlock => unreachable,
9812 break :handle handle;10397 error.FileBusy => unreachable,
9813 } else |err| switch (err) {10398 else => |e| return e,
9814 error.WouldBlock => unreachable,
9815 error.OperationCanceled => {
9816 try syscall.checkCancel();
9817 continue;
9818 },
9819 else => |e| return e,
9820 }
9821 }10399 }
9822 };10400 };
9823 defer w.CloseHandle(h_file);10401 defer w.CloseHandle(h_file);
982410402
9825 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks10403 const wide_slice = try GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data);
9826 try Thread.checkCancel();
9827 const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, &path_name_w_buf.data);
982810404
9829 const len = std.unicode.calcWtf8Len(wide_slice);10405 const len = std.unicode.calcWtf8Len(wide_slice);
9830 if (len > out_buffer.len)10406 if (len > out_buffer.len)
...@@ -13598,9 +14174,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr...@@ -13598,9 +14174,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
1359814174
13599 if (is_windows) {14175 if (is_windows) {
13600 var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;14176 var dir_path_buffer: [windows.PATH_MAX_WIDE]u16 = undefined;
13601 // TODO move GetFinalPathNameByHandle logic into Io.Threaded and add cancel checks14177 const dir_path = try GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer);
13602 try Thread.checkCancel();
13603 const dir_path = try windows.GetFinalPathNameByHandle(dir.handle, .{}, &dir_path_buffer);
13604 const path_len_bytes = std.math.cast(u16, dir_path.len * 2) orelse return error.NameTooLong;14178 const path_len_bytes = std.math.cast(u16, dir_path.len * 2) orelse return error.NameTooLong;
13605 var nt_name: windows.UNICODE_STRING = .{14179 var nt_name: windows.UNICODE_STRING = .{
13606 .Length = path_len_bytes,14180 .Length = path_len_bytes,
...@@ -15326,9 +15900,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro...@@ -15326,9 +15900,7 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro
15326 .inherit => break :cwd_w null,15900 .inherit => break :cwd_w null,
15327 .dir => |cwd_dir| {15901 .dir => |cwd_dir| {
15328 var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1);15902 var dir_path_buffer = try arena.alloc(u16, windows.PATH_MAX_WIDE + 1);
15329 // TODO move GetFinalPathNameByHandle logic into std.Io.Threaded and add cancel checks15903 const dir_path = try GetFinalPathNameByHandle(
15330 try Thread.checkCancel();
15331 const dir_path = try windows.GetFinalPathNameByHandle(
15332 cwd_dir.handle,15904 cwd_dir.handle,
15333 .{},15905 .{},
15334 dir_path_buffer[0..windows.PATH_MAX_WIDE],15906 dir_path_buffer[0..windows.PATH_MAX_WIDE],
...@@ -15752,7 +16324,7 @@ fn windowsCreateProcessPathExt(...@@ -15752,7 +16324,7 @@ fn windowsCreateProcessPathExt(
15752 try dir_buf.append(arena, 0);16324 try dir_buf.append(arena, 0);
15753 defer dir_buf.shrinkRetainingCapacity(dir_path_len);16325 defer dir_buf.shrinkRetainingCapacity(dir_path_len);
15754 const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];16326 const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0];
15755 const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z);16327 const prefixed_path = try wToPrefixedFileW(null, dir_path_z);
15756 break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{16328 break :dir dirOpenDirWindows(.cwd(), prefixed_path.span(), .{
15757 .iterate = true,16329 .iterate = true,
15758 }) catch |err| switch (err) {16330 }) catch |err| switch (err) {
...@@ -18517,3 +19089,147 @@ pub fn mutexUnlock(m: *Io.Mutex) void {...@@ -18517,3 +19089,147 @@ pub fn mutexUnlock(m: *Io.Mutex) void {
18517 },19089 },
18518 }19090 }
18519}19091}
19092
19093const OpenError = error{
19094 IsDir,
19095 NotDir,
19096 FileNotFound,
19097 NoDevice,
19098 AccessDenied,
19099 PipeBusy,
19100 PathAlreadyExists,
19101 WouldBlock,
19102 NetworkNotFound,
19103 AntivirusInterference,
19104 FileBusy,
19105} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
19106
19107const OpenFileOptions = struct {
19108 access_mask: windows.ACCESS_MASK,
19109 dir: ?windows.HANDLE = null,
19110 sa: ?*windows.SECURITY_ATTRIBUTES = null,
19111 share_access: windows.FILE.SHARE = .VALID_FLAGS,
19112 creation: windows.FILE.CREATE_DISPOSITION,
19113 filter: Filter = .non_directory_only,
19114 /// If false, tries to open path as a reparse point without dereferencing it.
19115 /// Defaults to true.
19116 follow_symlinks: bool = true,
19117
19118 pub const Filter = enum {
19119 /// Causes `OpenFile` to return `error.IsDir` if the opened handle would be a directory.
19120 non_directory_only,
19121 /// Causes `OpenFile` to return `error.NotDir` if the opened handle is not a directory.
19122 dir_only,
19123 /// `OpenFile` does not discriminate between opening files and directories.
19124 any,
19125 };
19126};
19127
19128/// TODO: inline this logic everywhere and delete this function
19129fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!windows.HANDLE {
19130 if (std.mem.eql(u16, sub_path_w, &[_]u16{'.'}) and options.filter == .non_directory_only) {
19131 return error.IsDir;
19132 }
19133 if (std.mem.eql(u16, sub_path_w, &[_]u16{ '.', '.' }) and options.filter == .non_directory_only) {
19134 return error.IsDir;
19135 }
19136
19137 var result: windows.HANDLE = undefined;
19138
19139 const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
19140 var nt_name: windows.UNICODE_STRING = .{
19141 .Length = path_len_bytes,
19142 .MaximumLength = path_len_bytes,
19143 .Buffer = @constCast(sub_path_w.ptr),
19144 };
19145 const attr: windows.OBJECT_ATTRIBUTES = .{
19146 .RootDirectory = if (Dir.path.isAbsoluteWindowsWtf16(sub_path_w)) null else options.dir,
19147 .Attributes = .{ .INHERIT = if (options.sa) |sa| sa.bInheritHandle != windows.FALSE else false },
19148 .ObjectName = &nt_name,
19149 .SecurityDescriptor = if (options.sa) |ptr| ptr.lpSecurityDescriptor else null,
19150 };
19151
19152 var iosb: windows.IO_STATUS_BLOCK = undefined;
19153 var attempt: u5 = 0;
19154 var syscall: Syscall = try .start();
19155 while (true) {
19156 switch (windows.ntdll.NtCreateFile(
19157 &result,
19158 options.access_mask,
19159 &attr,
19160 &iosb,
19161 null,
19162 .{ .NORMAL = true },
19163 options.share_access,
19164 options.creation,
19165 .{
19166 .DIRECTORY_FILE = options.filter == .dir_only,
19167 .NON_DIRECTORY_FILE = options.filter == .non_directory_only,
19168 .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,
19169 .OPEN_REPARSE_POINT = !options.follow_symlinks,
19170 },
19171 null,
19172 0,
19173 )) {
19174 .SUCCESS => {
19175 syscall.finish();
19176 return result;
19177 },
19178 .CANCELLED => {
19179 try syscall.checkCancel();
19180 continue;
19181 },
19182 .SHARING_VIOLATION => {
19183 // This occurs if the file attempting to be opened is a running
19184 // executable. However, there's a kernel bug: the error may be
19185 // incorrectly returned for an indeterminate amount of time
19186 // after an executable file is closed. Here we work around the
19187 // kernel bug with retry attempts.
19188 syscall.finish();
19189 if (max_windows_kernel_bug_retries - attempt == 0) return error.FileBusy;
19190 try parking_sleep.sleep(.{ .duration = .{
19191 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
19192 .clock = .awake,
19193 } });
19194 attempt += 1;
19195 syscall = try .start();
19196 continue;
19197 },
19198 .DELETE_PENDING => {
19199 // This error means that there *was* a file in this location on
19200 // the file system, but it was deleted. However, the OS is not
19201 // finished with the deletion operation, and so this CreateFile
19202 // call has failed. There is not really a sane way to handle
19203 // this other than retrying the creation after the OS finishes
19204 // the deletion.
19205 syscall.finish();
19206 if (max_windows_kernel_bug_retries - attempt == 0) return error.FileBusy;
19207 try parking_sleep.sleep(.{ .duration = .{
19208 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
19209 .clock = .awake,
19210 } });
19211 attempt += 1;
19212 syscall = try .start();
19213 continue;
19214 },
19215 .OBJECT_NAME_INVALID => return syscall.fail(error.BadPathName),
19216 .OBJECT_NAME_NOT_FOUND => return syscall.fail(error.FileNotFound),
19217 .OBJECT_PATH_NOT_FOUND => return syscall.fail(error.FileNotFound),
19218 .BAD_NETWORK_PATH => return syscall.fail(error.NetworkNotFound), // \\server was not found
19219 .BAD_NETWORK_NAME => return syscall.fail(error.NetworkNotFound), // \\server was found but \\server\share wasn't
19220 .NO_MEDIA_IN_DEVICE => return syscall.fail(error.NoDevice),
19221 .ACCESS_DENIED => return syscall.fail(error.AccessDenied),
19222 .PIPE_BUSY => return syscall.fail(error.PipeBusy),
19223 .PIPE_NOT_AVAILABLE => return syscall.fail(error.NoDevice),
19224 .OBJECT_NAME_COLLISION => return syscall.fail(error.PathAlreadyExists),
19225 .FILE_IS_A_DIRECTORY => return syscall.fail(error.IsDir),
19226 .NOT_A_DIRECTORY => return syscall.fail(error.NotDir),
19227 .USER_MAPPED_FILE => return syscall.fail(error.AccessDenied),
19228 .VIRUS_INFECTED, .VIRUS_DELETED => return syscall.fail(error.AntivirusInterference),
19229 .INVALID_PARAMETER => |status| return syscall.ntstatusBug(status),
19230 .OBJECT_PATH_SYNTAX_BAD => |status| return syscall.ntstatusBug(status),
19231 .INVALID_HANDLE => |status| return syscall.ntstatusBug(status),
19232 else => |status| return syscall.unexpectedNtstatus(status),
19233 }
19234 }
19235}
lib/std/Io/Threaded/test.zig+335
...@@ -6,6 +6,7 @@ const std = @import("std");...@@ -6,6 +6,7 @@ const std = @import("std");
6const Io = std.Io;6const Io = std.Io;
7const testing = std.testing;7const testing = std.testing;
8const assert = std.debug.assert;8const assert = std.debug.assert;
9const windows = std.os.windows;
910
10test "concurrent vs main prevents deadlock via oversubscription" {11test "concurrent vs main prevents deadlock via oversubscription" {
11 if (true) {12 if (true) {
...@@ -277,3 +278,337 @@ test "memory mapping fallback" {...@@ -277,3 +278,337 @@ test "memory mapping fallback" {
277 try testing.expectEqualStrings("this9is9my data123", mm.memory);278 try testing.expectEqualStrings("this9is9my data123", mm.memory);
278 }279 }
279}280}
281
282/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing
283/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW
284/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation
285// because it allocates.
286fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !Io.Threaded.WindowsPathSpace {
287 var out: windows.UNICODE_STRING = undefined;
288 const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
289 if (rc != windows.TRUE) return error.BadPathName;
290 defer windows.ntdll.RtlFreeUnicodeString(&out);
291
292 var path_space: Io.Threaded.WindowsPathSpace = undefined;
293 const out_path = out.Buffer.?[0 .. out.Length / 2];
294 @memcpy(path_space.data[0..out_path.len], out_path);
295 path_space.len = out.Length / 2;
296 path_space.data[path_space.len] = 0;
297
298 return path_space;
299}
300
301/// Test that the Zig conversion matches the expected_path (for instances where
302/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does).
303fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
304 const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
305 const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path);
306 const actual_path = try Io.Threaded.wToPrefixedFileW(null, path_utf16);
307 std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
308 std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) });
309 return e;
310 };
311}
312
313/// Test that the Zig conversion matches the expected_path and that the
314/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does.
315fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
316 try testToPrefixedFileNoOracle(path, expected_path);
317 try testToPrefixedFileOnlyOracle(path);
318}
319
320/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does.
321fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
322 const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
323 const zig_result = try Io.Threaded.wToPrefixedFileW(null, path_utf16);
324 const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16);
325 std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| {
326 std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) });
327 return e;
328 };
329}
330
331test "toPrefixedFileW" {
332 if (builtin.os.tag != .windows) return error.SkipZigTest;
333
334 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
335 // Note that these tests do not actually touch the filesystem or care about whether or not
336 // any of the paths actually exist or are otherwise valid.
337
338 // Drive Absolute
339 try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
340 try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
341 try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
342 // Trailing . and space characters are stripped
343 try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
344 try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
345 try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
346 try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
347 // Drive letter casing is unchanged
348 try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
349
350 // Drive Relative
351 // These tests depend on the CWD of the specified drive letter which can vary,
352 // so instead we just test that the Zig implementation matches the result of
353 // RtlDosPathNameToNtPathName_U.
354 // TODO: Setting the =X: environment variable didn't seem to affect
355 // RtlDosPathNameToNtPathName_U, not sure why that is but getting that
356 // to work could be an avenue to making these cases environment-independent.
357 // All -> are examples of the result if the X drive's cwd was X:\ABC
358 try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
359 try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
360 try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
361 try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
362 try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
363 try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
364
365 // Rooted
366 // These tests depend on the drive letter of the CWD which can vary, so
367 // instead we just test that the Zig implementation matches the result of
368 // RtlDosPathNameToNtPathName_U.
369 // TODO: Getting the CWD path, getting the drive letter from it, and using it to
370 // construct the expected NT paths could be an avenue to making these cases
371 // environment-independent and therefore able to use testToPrefixedFileWithOracle.
372 // All -> are examples of the result if the CWD's drive letter was X
373 try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
374 try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
375 try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
376 try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
377 try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
378 try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
379
380 // Relative
381 // These cases differ in functionality to RtlDosPathNameToNtPathName_U.
382 // Relative paths remain relative if they don't have enough .. components
383 // to error with TooManyParentDirs
384 try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
385 // TODO: enable this if trailing . and spaces are stripped from relative paths
386 //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
387 try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
388 try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
389 // TooManyParentDirs, so resolved relative to the CWD
390 // All -> are examples of the result if the CWD was X:\ABC\DEF
391 try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
392 try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
393
394 // UNC Absolute
395 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
396 try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
397 try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
398 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
399 try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
400 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
401 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
402
403 // Local Device
404 try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
405 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
406 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
407 try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
408 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
409 // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
410 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
411 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
412
413 // Special-case device names
414 // TODO: Enable once these are supported
415 // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
416 //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
417 // Sometimes the special-cased device names are not respected
418 try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
419 try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
420
421 // Verbatim
422 // Left untouched except \\?\ is replaced by \??\
423 try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
424 try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
425 try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
426 try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
427 // NT Namespace
428 // Fully unmodified
429 try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
430 try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
431 try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
432 try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
433
434 // 'Fake' Verbatim
435 // If the prefix looks like the verbatim prefix but not all path separators in the
436 // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
437 // of the NT prefix.
438 try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
439 // 'Fake' NT
440 // If the prefix looks like the NT prefix but not all path separators in the prefix
441 // are backslashes, then it gets canonicalized and the /??/ is not dropped but
442 // rather treated as part of the path. In other words, the path is treated
443 // as a rooted path, so the final path is resolved relative to the CWD's
444 // drive letter.
445 // The -> shows an example of the result if the CWD's drive letter was X
446 try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
447
448 // Root Local Device
449 // \\. and \\? always get converted to \??\
450 try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
451 try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
452 try testToPrefixedFileWithOracle("//?", "\\??\\");
453 try testToPrefixedFileWithOracle("//.", "\\??\\");
454}
455
456fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
457 const mutable = try testing.allocator.dupe(u8, str);
458 defer testing.allocator.free(mutable);
459 const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
460 try testing.expect(std.mem.eql(u8, actual, expected));
461}
462fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
463 const mutable = try testing.allocator.dupe(u8, str);
464 defer testing.allocator.free(mutable);
465 try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
466}
467test "removeDotDirs" {
468 try testRemoveDotDirs("", "");
469 try testRemoveDotDirs(".", "");
470 try testRemoveDotDirs(".\\", "");
471 try testRemoveDotDirs(".\\.", "");
472 try testRemoveDotDirs(".\\.\\", "");
473 try testRemoveDotDirs(".\\.\\.", "");
474
475 try testRemoveDotDirs("a", "a");
476 try testRemoveDotDirs("a\\", "a\\");
477 try testRemoveDotDirs("a\\b", "a\\b");
478 try testRemoveDotDirs("a\\.", "a\\");
479 try testRemoveDotDirs("a\\b\\.", "a\\b\\");
480 try testRemoveDotDirs("a\\.\\b", "a\\b");
481
482 try testRemoveDotDirs(".a", ".a");
483 try testRemoveDotDirs(".a\\", ".a\\");
484 try testRemoveDotDirs(".a\\.b", ".a\\.b");
485 try testRemoveDotDirs(".a\\.", ".a\\");
486 try testRemoveDotDirs(".a\\.\\.", ".a\\");
487 try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
488 try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
489
490 try testRemoveDotDirsError(error.TooManyParentDirs, "..");
491 try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
492 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
493 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
494
495 try testRemoveDotDirs("a\\..", "");
496 try testRemoveDotDirs("a\\..\\", "");
497 try testRemoveDotDirs("a\\..\\.", "");
498 try testRemoveDotDirs("a\\..\\.\\", "");
499 try testRemoveDotDirs("a\\..\\.\\.", "");
500 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
501
502 try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
503 try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
504 try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
505 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
506 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
507 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
508 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
509 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
510
511 try testRemoveDotDirs("a\\b\\..\\", "a\\");
512 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
513}
514
515const RTL_PATH_TYPE = enum(c_int) {
516 Unknown,
517 UncAbsolute,
518 DriveAbsolute,
519 DriveRelative,
520 Rooted,
521 Relative,
522 LocalDevice,
523 RootLocalDevice,
524};
525
526pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
527 Path: [*:0]const u16,
528) callconv(.winapi) RTL_PATH_TYPE;
529
530test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
531 if (builtin.os.tag != .windows) return error.SkipZigTest;
532
533 var buf: std.ArrayList(u16) = .empty;
534 defer buf.deinit(std.testing.allocator);
535
536 var wtf8_buf: std.ArrayList(u8) = .empty;
537 defer wtf8_buf.deinit(std.testing.allocator);
538
539 var random = std.Random.DefaultPrng.init(std.testing.random_seed);
540 const rand = random.random();
541
542 for (0..1000) |_| {
543 buf.clearRetainingCapacity();
544 const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
545 wtf8_buf.clearRetainingCapacity();
546 const wtf8_len = std.unicode.calcWtf8Len(path);
547 try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
548 wtf8_buf.items.len = wtf8_len;
549 std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
550
551 const windows_type = RtlDetermineDosPathNameType_U(path);
552 const wtf16_type = std.fs.path.getWin32PathType(u16, path);
553 const wtf8_type = std.fs.path.getWin32PathType(u8, wtf8_buf.items);
554
555 checkPathType(windows_type, wtf16_type) catch |err| {
556 std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
557 std.debug.print("path bytes:\n", .{});
558 std.debug.dumpHex(std.mem.sliceAsBytes(path));
559 return err;
560 };
561
562 if (wtf16_type != wtf8_type) {
563 std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
564 std.debug.print("wtf-16 path bytes:\n", .{});
565 std.debug.dumpHex(std.mem.sliceAsBytes(path));
566 std.debug.print("wtf-8 path bytes:\n", .{});
567 std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
568 return error.Wtf8Wtf16Mismatch;
569 }
570 }
571}
572
573fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: std.fs.path.Win32PathType) !void {
574 const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
575 .unc_absolute => .UncAbsolute,
576 .drive_absolute => .DriveAbsolute,
577 .drive_relative => .DriveRelative,
578 .rooted => .Rooted,
579 .relative => .Relative,
580 .local_device => .LocalDevice,
581 .root_local_device => .RootLocalDevice,
582 };
583 if (windows_type != expected_windows_type) return error.PathTypeMismatch;
584}
585
586fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
587 const Choice = enum {
588 backslash,
589 slash,
590 control,
591 printable,
592 non_ascii,
593 };
594
595 const choices = rand.uintAtMostBiased(u16, 32);
596
597 for (0..choices) |_| {
598 const choice = rand.enumValue(Choice);
599 const code_unit = switch (choice) {
600 .backslash => '\\',
601 .slash => '/',
602 .control => switch (rand.uintAtMostBiased(u8, 0x20)) {
603 0x20 => '\x7F',
604 else => |b| b + 1, // no NUL
605 },
606 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
607 .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
608 };
609 try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
610 }
611
612 try buf.append(allocator, 0);
613 return buf.items[0 .. buf.items.len - 1 :0];
614}
lib/std/dynamic_library.zig+1-70
...@@ -17,7 +17,6 @@ pub const DynLib = struct {...@@ -17,7 +17,6 @@ pub const DynLib = struct {
17 ElfDynLib17 ElfDynLib
18 else18 else
19 DlDynLib,19 DlDynLib,
20 .windows => WindowsDynLib,
21 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .openbsd, .dragonfly, .illumos => DlDynLib,20 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .openbsd, .dragonfly, .illumos => DlDynLib,
22 else => struct {21 else => struct {
23 const open = @compileError("unsupported platform");22 const open = @compileError("unsupported platform");
...@@ -27,7 +26,7 @@ pub const DynLib = struct {...@@ -27,7 +26,7 @@ pub const DynLib = struct {
2726
28 inner: InnerType,27 inner: InnerType,
2928
30 pub const Error = ElfDynLibError || DlDynLibError || WindowsDynLibError;29 pub const Error = ElfDynLibError || DlDynLibError;
3130
32 /// Trusts the file. Malicious file will be able to execute arbitrary code.31 /// Trusts the file. Malicious file will be able to execute arbitrary code.
33 pub fn open(path: []const u8) Error!DynLib {32 pub fn open(path: []const u8) Error!DynLib {
...@@ -558,73 +557,6 @@ test "ElfDynLib" {...@@ -558,73 +557,6 @@ test "ElfDynLib" {
558 try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so", null));557 try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so", null));
559}558}
560559
561/// Separated to avoid referencing `WindowsDynLib`, because its field types may not
562/// be valid on other targets.
563const WindowsDynLibError = error{
564 FileNotFound,
565 InvalidPath,
566} || windows.LoadLibraryError;
567
568pub const WindowsDynLib = struct {
569 pub const Error = WindowsDynLibError;
570
571 dll: windows.HMODULE,
572
573 pub fn open(path: []const u8) Error!WindowsDynLib {
574 return openEx(path, .none);
575 }
576
577 /// WindowsDynLib specific
578 /// Opens dynamic library with specified library loading flags.
579 pub fn openEx(path: []const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
580 const path_w = windows.sliceToPrefixedFileW(null, path) catch return error.InvalidPath;
581 return openExW(path_w.span().ptr, flags);
582 }
583
584 pub fn openZ(path_c: [*:0]const u8) Error!WindowsDynLib {
585 return openExZ(path_c, .none);
586 }
587
588 /// WindowsDynLib specific
589 /// Opens dynamic library with specified library loading flags.
590 pub fn openExZ(path_c: [*:0]const u8, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
591 const path_w = windows.cStrToPrefixedFileW(null, path_c) catch return error.InvalidPath;
592 return openExW(path_w.span().ptr, flags);
593 }
594
595 /// WindowsDynLib specific
596 pub fn openW(path_w: [*:0]const u16) Error!WindowsDynLib {
597 return openExW(path_w, .none);
598 }
599
600 /// WindowsDynLib specific
601 /// Opens dynamic library with specified library loading flags.
602 pub fn openExW(path_w: [*:0]const u16, flags: windows.LoadLibraryFlags) Error!WindowsDynLib {
603 var offset: usize = 0;
604 if (path_w[0] == '\\' and path_w[1] == '?' and path_w[2] == '?' and path_w[3] == '\\') {
605 // + 4 to skip over the \??\
606 offset = 4;
607 }
608
609 return .{
610 .dll = try windows.LoadLibraryExW(path_w + offset, flags),
611 };
612 }
613
614 pub fn close(self: *WindowsDynLib) void {
615 windows.FreeLibrary(self.dll);
616 self.* = undefined;
617 }
618
619 pub fn lookup(self: *WindowsDynLib, comptime T: type, name: [:0]const u8) ?T {
620 if (windows.kernel32.GetProcAddress(self.dll, name.ptr)) |addr| {
621 return @as(T, @ptrCast(@alignCast(addr)));
622 } else {
623 return null;
624 }
625 }
626};
627
628/// Separated to avoid referencing `DlDynLib`, because its field types may not560/// Separated to avoid referencing `DlDynLib`, because its field types may not
629/// be valid on other targets.561/// be valid on other targets.
630const DlDynLibError = error{ FileNotFound, NameTooLong };562const DlDynLibError = error{ FileNotFound, NameTooLong };
...@@ -676,7 +608,6 @@ pub const DlDynLib = struct {...@@ -676,7 +608,6 @@ pub const DlDynLib = struct {
676test "dynamic_library" {608test "dynamic_library" {
677 const libname = switch (native_os) {609 const libname = switch (native_os) {
678 .linux, .freebsd, .openbsd, .illumos => "invalid_so.so",610 .linux, .freebsd, .openbsd, .illumos => "invalid_so.so",
679 .windows => "invalid_dll.dll",
680 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => "invalid_dylib.dylib",611 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => "invalid_dylib.dylib",
681 else => return error.SkipZigTest,612 else => return error.SkipZigTest,
682 };613 };
lib/std/os/windows.zig-682
...@@ -15,12 +15,6 @@ const math = std.math;...@@ -15,12 +15,6 @@ const math = std.math;
15const maxInt = std.math.maxInt;15const maxInt = std.math.maxInt;
16const UnexpectedError = std.posix.UnexpectedError;16const UnexpectedError = std.posix.UnexpectedError;
1717
18test {
19 if (builtin.os.tag == .windows) {
20 _ = @import("windows/test.zig");
21 }
22}
23
24pub const advapi32 = @import("windows/advapi32.zig");18pub const advapi32 = @import("windows/advapi32.zig");
25pub const kernel32 = @import("windows/kernel32.zig");19pub const kernel32 = @import("windows/kernel32.zig");
26pub const ntdll = @import("windows/ntdll.zig");20pub const ntdll = @import("windows/ntdll.zig");
...@@ -2365,123 +2359,6 @@ pub const OBJECT_ATTRIBUTES = extern struct {...@@ -2365,123 +2359,6 @@ pub const OBJECT_ATTRIBUTES = extern struct {
23652359
2366// ref none2360// ref none
23672361
2368pub const OpenError = error{
2369 IsDir,
2370 NotDir,
2371 FileNotFound,
2372 NoDevice,
2373 AccessDenied,
2374 PipeBusy,
2375 PathAlreadyExists,
2376 Unexpected,
2377 NameTooLong,
2378 WouldBlock,
2379 NetworkNotFound,
2380 AntivirusInterference,
2381 BadPathName,
2382 OperationCanceled,
2383};
2384
2385pub const OpenFileOptions = struct {
2386 access_mask: ACCESS_MASK,
2387 dir: ?HANDLE = null,
2388 sa: ?*SECURITY_ATTRIBUTES = null,
2389 share_access: FILE.SHARE = .VALID_FLAGS,
2390 creation: FILE.CREATE_DISPOSITION,
2391 filter: Filter = .non_directory_only,
2392 /// If false, tries to open path as a reparse point without dereferencing it.
2393 /// Defaults to true.
2394 follow_symlinks: bool = true,
2395
2396 pub const Filter = enum {
2397 /// Causes `OpenFile` to return `error.IsDir` if the opened handle would be a directory.
2398 non_directory_only,
2399 /// Causes `OpenFile` to return `error.NotDir` if the opened handle is not a directory.
2400 dir_only,
2401 /// `OpenFile` does not discriminate between opening files and directories.
2402 any,
2403 };
2404};
2405
2406pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HANDLE {
2407 if (mem.eql(u16, sub_path_w, &[_]u16{'.'}) and options.filter == .non_directory_only) {
2408 return error.IsDir;
2409 }
2410 if (mem.eql(u16, sub_path_w, &[_]u16{ '.', '.' }) and options.filter == .non_directory_only) {
2411 return error.IsDir;
2412 }
2413
2414 var result: HANDLE = undefined;
2415
2416 const path_len_bytes = math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong;
2417 var nt_name: UNICODE_STRING = .{
2418 .Length = path_len_bytes,
2419 .MaximumLength = path_len_bytes,
2420 .Buffer = @constCast(sub_path_w.ptr),
2421 };
2422 const attr: OBJECT_ATTRIBUTES = .{
2423 .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else options.dir,
2424 .Attributes = .{ .INHERIT = if (options.sa) |sa| sa.bInheritHandle != FALSE else false },
2425 .ObjectName = &nt_name,
2426 .SecurityDescriptor = if (options.sa) |ptr| ptr.lpSecurityDescriptor else null,
2427 };
2428 var io: IO_STATUS_BLOCK = undefined;
2429 while (true) {
2430 const rc = ntdll.NtCreateFile(
2431 &result,
2432 options.access_mask,
2433 &attr,
2434 &io,
2435 null,
2436 .{ .NORMAL = true },
2437 options.share_access,
2438 options.creation,
2439 .{
2440 .DIRECTORY_FILE = options.filter == .dir_only,
2441 .NON_DIRECTORY_FILE = options.filter == .non_directory_only,
2442 .IO = if (options.follow_symlinks) .SYNCHRONOUS_NONALERT else .ASYNCHRONOUS,
2443 .OPEN_REPARSE_POINT = !options.follow_symlinks,
2444 },
2445 null,
2446 0,
2447 );
2448 switch (rc) {
2449 .SUCCESS => return result,
2450 .OBJECT_NAME_INVALID => return error.BadPathName,
2451 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
2452 .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
2453 .BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found
2454 .BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't
2455 .NO_MEDIA_IN_DEVICE => return error.NoDevice,
2456 .INVALID_PARAMETER => unreachable,
2457 .SHARING_VIOLATION => return error.AccessDenied,
2458 .ACCESS_DENIED => return error.AccessDenied,
2459 .PIPE_BUSY => return error.PipeBusy,
2460 .PIPE_NOT_AVAILABLE => return error.NoDevice,
2461 .OBJECT_PATH_SYNTAX_BAD => unreachable,
2462 .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
2463 .FILE_IS_A_DIRECTORY => return error.IsDir,
2464 .NOT_A_DIRECTORY => return error.NotDir,
2465 .USER_MAPPED_FILE => return error.AccessDenied,
2466 .INVALID_HANDLE => unreachable,
2467 .DELETE_PENDING => {
2468 // This error means that there *was* a file in this location on
2469 // the file system, but it was deleted. However, the OS is not
2470 // finished with the deletion operation, and so this CreateFile
2471 // call has failed. There is not really a sane way to handle
2472 // this other than retrying the creation after the OS finishes
2473 // the deletion.
2474 const delay_one_ms: LARGE_INTEGER = -(std.time.ns_per_ms / 100);
2475 _ = ntdll.NtDelayExecution(TRUE, &delay_one_ms);
2476 continue;
2477 },
2478 .VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference,
2479 .CANCELLED => return error.OperationCanceled,
2480 else => return unexpectedStatus(rc),
2481 }
2482 }
2483}
2484
2485pub fn GetCurrentProcess() HANDLE {2362pub fn GetCurrentProcess() HANDLE {
2486 const process_pseudo_handle: usize = @bitCast(@as(isize, -1));2363 const process_pseudo_handle: usize = @bitCast(@as(isize, -1));
2487 return @ptrFromInt(process_pseudo_handle);2364 return @ptrFromInt(process_pseudo_handle);
...@@ -2670,324 +2547,6 @@ pub fn CloseHandle(hObject: HANDLE) void {...@@ -2670,324 +2547,6 @@ pub fn CloseHandle(hObject: HANDLE) void {
2670 assert(ntdll.NtClose(hObject) == .SUCCESS);2547 assert(ntdll.NtClose(hObject) == .SUCCESS);
2671}2548}
26722549
2673pub const QueryObjectNameError = error{
2674 AccessDenied,
2675 InvalidHandle,
2676 NameTooLong,
2677 Unexpected,
2678};
2679
2680pub fn QueryObjectName(handle: HANDLE, out_buffer: []u16) QueryObjectNameError![]u16 {
2681 const out_buffer_aligned = mem.alignInSlice(out_buffer, @alignOf(OBJECT_NAME_INFORMATION)) orelse return error.NameTooLong;
2682
2683 const info = @as(*OBJECT_NAME_INFORMATION, @ptrCast(out_buffer_aligned));
2684 // buffer size is specified in bytes
2685 const out_buffer_len = std.math.cast(ULONG, out_buffer_aligned.len * 2) orelse maxInt(ULONG);
2686 // last argument would return the length required for full_buffer, not exposed here
2687 return switch (ntdll.NtQueryObject(handle, .ObjectNameInformation, info, out_buffer_len, null)) {
2688 .SUCCESS => blk: {
2689 // info.Name.Buffer from ObQueryNameString is documented to be null (and MaximumLength == 0)
2690 // if the object was "unnamed", not sure if this can happen for file handles
2691 if (info.Name.MaximumLength == 0) break :blk error.Unexpected;
2692 // resulting string length is specified in bytes
2693 const path_length_unterminated = @divExact(info.Name.Length, 2);
2694 break :blk info.Name.Buffer.?[0..path_length_unterminated];
2695 },
2696 .ACCESS_DENIED => error.AccessDenied,
2697 .INVALID_HANDLE => error.InvalidHandle,
2698 // triggered when the buffer is too small for the OBJECT_NAME_INFORMATION object (.INFO_LENGTH_MISMATCH),
2699 // or if the buffer is too small for the file path returned (.BUFFER_OVERFLOW, .BUFFER_TOO_SMALL)
2700 .INFO_LENGTH_MISMATCH, .BUFFER_OVERFLOW, .BUFFER_TOO_SMALL => error.NameTooLong,
2701 else => |e| unexpectedStatus(e),
2702 };
2703}
2704
2705test QueryObjectName {
2706 if (builtin.os.tag != .windows)
2707 return;
2708
2709 //any file will do; canonicalization works on NTFS junctions and symlinks, hardlinks remain separate paths.
2710 var tmp = std.testing.tmpDir(.{});
2711 defer tmp.cleanup();
2712 const handle = tmp.dir.handle;
2713 var out_buffer: [PATH_MAX_WIDE]u16 = undefined;
2714
2715 const result_path = try QueryObjectName(handle, &out_buffer);
2716 const required_len_in_u16 = result_path.len + @divExact(@intFromPtr(result_path.ptr) - @intFromPtr(&out_buffer), 2) + 1;
2717 //insufficient size
2718 try std.testing.expectError(error.NameTooLong, QueryObjectName(handle, out_buffer[0 .. required_len_in_u16 - 1]));
2719 //exactly-sufficient size
2720 _ = try QueryObjectName(handle, out_buffer[0..required_len_in_u16]);
2721}
2722
2723pub const GetFinalPathNameByHandleError = error{
2724 AccessDenied,
2725 FileNotFound,
2726 NameTooLong,
2727 /// The volume does not contain a recognized file system. File system
2728 /// drivers might not be loaded, or the volume may be corrupt.
2729 UnrecognizedVolume,
2730 Unexpected,
2731};
2732
2733/// Specifies how to format volume path in the result of `GetFinalPathNameByHandle`.
2734/// Defaults to DOS volume names.
2735pub const GetFinalPathNameByHandleFormat = struct {
2736 volume_name: enum {
2737 /// Format as DOS volume name
2738 Dos,
2739 /// Format as NT volume name
2740 Nt,
2741 } = .Dos,
2742};
2743
2744/// Returns canonical (normalized) path of handle.
2745/// Use `GetFinalPathNameByHandleFormat` to specify whether the path is meant to include
2746/// NT or DOS volume name (e.g., `\Device\HarddiskVolume0\foo.txt` versus `C:\foo.txt`).
2747/// If DOS volume name format is selected, note that this function does *not* prepend
2748/// `\\?\` prefix to the resultant path.
2749///
2750/// TODO move this function into std.Io.Threaded and add cancelation checks
2751pub fn GetFinalPathNameByHandle(
2752 hFile: HANDLE,
2753 fmt: GetFinalPathNameByHandleFormat,
2754 out_buffer: []u16,
2755) GetFinalPathNameByHandleError![]u16 {
2756 const final_path = QueryObjectName(hFile, out_buffer) catch |err| switch (err) {
2757 // we assume InvalidHandle is close enough to FileNotFound in semantics
2758 // to not further complicate the error set
2759 error.InvalidHandle => return error.FileNotFound,
2760 else => |e| return e,
2761 };
2762
2763 switch (fmt.volume_name) {
2764 .Nt => {
2765 // the returned path is already in .Nt format
2766 return final_path;
2767 },
2768 .Dos => {
2769 // parse the string to separate volume path from file path
2770 const device_prefix = std.unicode.utf8ToUtf16LeStringLiteral("\\Device\\");
2771
2772 // We aren't entirely sure of the structure of the path returned by
2773 // QueryObjectName in all contexts/environments.
2774 // This code is written to cover the various cases that have
2775 // been encountered and solved appropriately. But note that there's
2776 // no easy way to verify that they have all been tackled!
2777 // (Unless you, the reader knows of one then please do action that!)
2778 if (!mem.startsWith(u16, final_path, device_prefix)) {
2779 // Wine seems to return NT namespaced paths starting with \??\ from QueryObjectName
2780 // (e.g. `\??\Z:\some\path\to\a\file.txt`), in which case we can just strip the
2781 // prefix to turn it into an absolute path.
2782 // https://github.com/ziglang/zig/issues/26029
2783 // https://bugs.winehq.org/show_bug.cgi?id=39569
2784 return ntToWin32Namespace(final_path, out_buffer) catch |err| switch (err) {
2785 error.NotNtPath => return error.Unexpected,
2786 error.NameTooLong => |e| return e,
2787 };
2788 }
2789
2790 const file_path_begin_index = mem.findPos(u16, final_path, device_prefix.len, &[_]u16{'\\'}) orelse unreachable;
2791 const volume_name_u16 = final_path[0..file_path_begin_index];
2792 const device_name_u16 = volume_name_u16[device_prefix.len..];
2793 const file_name_u16 = final_path[file_path_begin_index..];
2794
2795 // MUP is Multiple UNC Provider, and indicates that the path is a UNC
2796 // path. In this case, the canonical UNC path can be gotten by just
2797 // dropping the \Device\Mup\ and making sure the path begins with \\
2798 if (mem.eql(u16, device_name_u16, std.unicode.utf8ToUtf16LeStringLiteral("Mup"))) {
2799 out_buffer[0] = '\\';
2800 @memmove(out_buffer[1..][0..file_name_u16.len], file_name_u16);
2801 return out_buffer[0 .. 1 + file_name_u16.len];
2802 }
2803
2804 // Get DOS volume name. DOS volume names are actually symbolic link objects to the
2805 // actual NT volume. For example:
2806 // (NT) \Device\HarddiskVolume4 => (DOS) \DosDevices\C: == (DOS) C:
2807 const MIN_SIZE = @sizeOf(MOUNTMGR_MOUNT_POINT) + MAX_PATH;
2808 // We initialize the input buffer to all zeros for convenience since
2809 // `DeviceIoControl` with `IOCTL_MOUNTMGR_QUERY_POINTS` expects this.
2810 var input_buf: [MIN_SIZE]u8 align(@alignOf(MOUNTMGR_MOUNT_POINT)) = [_]u8{0} ** MIN_SIZE;
2811 var output_buf: [MIN_SIZE * 4]u8 align(@alignOf(MOUNTMGR_MOUNT_POINTS)) = undefined;
2812
2813 // This surprising path is a filesystem path to the mount manager on Windows.
2814 // Source: https://stackoverflow.com/questions/3012828/using-ioctl-mountmgr-query-points
2815 // This is the NT namespaced version of \\.\MountPointManager
2816 const mgmt_path_u16 = std.unicode.utf8ToUtf16LeStringLiteral("\\??\\MountPointManager");
2817 const mgmt_handle = OpenFile(mgmt_path_u16, .{
2818 .access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } },
2819 .creation = .OPEN,
2820 }) catch |err| switch (err) {
2821 error.IsDir => return error.Unexpected,
2822 error.NotDir => return error.Unexpected,
2823 error.NoDevice => return error.Unexpected,
2824 error.AccessDenied => return error.Unexpected,
2825 error.PipeBusy => return error.Unexpected,
2826 error.PathAlreadyExists => return error.Unexpected,
2827 error.WouldBlock => return error.Unexpected,
2828 error.NetworkNotFound => return error.Unexpected,
2829 error.AntivirusInterference => return error.Unexpected,
2830 error.BadPathName => return error.Unexpected,
2831 error.OperationCanceled => @panic("TODO: better integrate cancelation"),
2832 else => |e| return e,
2833 };
2834 defer CloseHandle(mgmt_handle);
2835
2836 var input_struct: *MOUNTMGR_MOUNT_POINT = @ptrCast(&input_buf[0]);
2837 input_struct.DeviceNameOffset = @sizeOf(MOUNTMGR_MOUNT_POINT);
2838 input_struct.DeviceNameLength = @intCast(volume_name_u16.len * 2);
2839 @memcpy(input_buf[@sizeOf(MOUNTMGR_MOUNT_POINT)..][0 .. volume_name_u16.len * 2], @as([*]const u8, @ptrCast(volume_name_u16.ptr)));
2840
2841 {
2842 const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_POINTS, .{ .in = &input_buf, .out = &output_buf });
2843 switch (rc) {
2844 .SUCCESS => {},
2845 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
2846 else => return unexpectedStatus(rc),
2847 }
2848 }
2849 const mount_points_struct: *const MOUNTMGR_MOUNT_POINTS = @ptrCast(&output_buf[0]);
2850
2851 const mount_points = @as(
2852 [*]const MOUNTMGR_MOUNT_POINT,
2853 @ptrCast(&mount_points_struct.MountPoints[0]),
2854 )[0..mount_points_struct.NumberOfMountPoints];
2855
2856 for (mount_points) |mount_point| {
2857 const symlink = @as(
2858 [*]const u16,
2859 @ptrCast(@alignCast(&output_buf[mount_point.SymbolicLinkNameOffset])),
2860 )[0 .. mount_point.SymbolicLinkNameLength / 2];
2861
2862 // Look for `\DosDevices\` prefix. We don't really care if there are more than one symlinks
2863 // with traditional DOS drive letters, so pick the first one available.
2864 var prefix_buf = std.unicode.utf8ToUtf16LeStringLiteral("\\DosDevices\\");
2865 const prefix = prefix_buf[0..prefix_buf.len];
2866
2867 if (mem.startsWith(u16, symlink, prefix)) {
2868 const drive_letter = symlink[prefix.len..];
2869
2870 if (out_buffer.len < drive_letter.len + file_name_u16.len) return error.NameTooLong;
2871
2872 @memcpy(out_buffer[0..drive_letter.len], drive_letter);
2873 @memmove(out_buffer[drive_letter.len..][0..file_name_u16.len], file_name_u16);
2874 const total_len = drive_letter.len + file_name_u16.len;
2875
2876 // Validate that DOS does not contain any spurious nul bytes.
2877 assert(mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
2878
2879 return out_buffer[0..total_len];
2880 } else if (mountmgrIsVolumeName(symlink)) {
2881 // If the symlink is a volume GUID like \??\Volume{383da0b0-717f-41b6-8c36-00500992b58d},
2882 // then it is a volume mounted as a path rather than a drive letter. We need to
2883 // query the mount manager again to get the DOS path for the volume.
2884
2885 // 49 is the maximum length accepted by mountmgrIsVolumeName
2886 const vol_input_size = @sizeOf(MOUNTMGR_TARGET_NAME) + (49 * 2);
2887 var vol_input_buf: [vol_input_size]u8 align(@alignOf(MOUNTMGR_TARGET_NAME)) = [_]u8{0} ** vol_input_size;
2888 // Note: If the path exceeds MAX_PATH, the Disk Management GUI doesn't accept the full path,
2889 // and instead if must be specified using a shortened form (e.g. C:\FOO~1\BAR~1\<...>).
2890 // However, just to be sure we can handle any path length, we use PATH_MAX_WIDE here.
2891 const min_output_size = @sizeOf(MOUNTMGR_VOLUME_PATHS) + (PATH_MAX_WIDE * 2);
2892 var vol_output_buf: [min_output_size]u8 align(@alignOf(MOUNTMGR_VOLUME_PATHS)) = undefined;
2893
2894 var vol_input_struct: *MOUNTMGR_TARGET_NAME = @ptrCast(&vol_input_buf[0]);
2895 vol_input_struct.DeviceNameLength = @intCast(symlink.len * 2);
2896 @memcpy(@as([*]WCHAR, &vol_input_struct.DeviceName)[0..symlink.len], symlink);
2897
2898 const rc = DeviceIoControl(mgmt_handle, IOCTL.MOUNTMGR.QUERY_DOS_VOLUME_PATH, .{ .in = &vol_input_buf, .out = &vol_output_buf });
2899 switch (rc) {
2900 .SUCCESS => {},
2901 .UNRECOGNIZED_VOLUME => return error.UnrecognizedVolume,
2902 else => return unexpectedStatus(rc),
2903 }
2904 const volume_paths_struct: *const MOUNTMGR_VOLUME_PATHS = @ptrCast(&vol_output_buf[0]);
2905 const volume_path = std.mem.sliceTo(@as(
2906 [*]const u16,
2907 &volume_paths_struct.MultiSz,
2908 )[0 .. volume_paths_struct.MultiSzLength / 2], 0);
2909
2910 if (out_buffer.len < volume_path.len + file_name_u16.len) return error.NameTooLong;
2911
2912 // `out_buffer` currently contains the memory of `file_name_u16`, so it can overlap with where
2913 // we want to place the filename before returning. Here are the possible overlapping cases:
2914 //
2915 // out_buffer: [filename]
2916 // dest: [___(a)___] [___(b)___]
2917 //
2918 // In the case of (a), we need to copy forwards, and in the case of (b) we need
2919 // to copy backwards. We also need to do this before copying the volume path because
2920 // it could overwrite the file_name_u16 memory.
2921 const file_name_dest = out_buffer[volume_path.len..][0..file_name_u16.len];
2922 @memmove(file_name_dest, file_name_u16);
2923 @memcpy(out_buffer[0..volume_path.len], volume_path);
2924 const total_len = volume_path.len + file_name_u16.len;
2925
2926 // Validate that DOS does not contain any spurious nul bytes.
2927 assert(mem.findScalar(u16, out_buffer[0..total_len], 0) == null);
2928
2929 return out_buffer[0..total_len];
2930 }
2931 }
2932
2933 // If we've ended up here, then something went wrong/is corrupted in the OS,
2934 // so error out!
2935 return error.FileNotFound;
2936 },
2937 }
2938}
2939
2940/// Equivalent to the MOUNTMGR_IS_VOLUME_NAME macro in mountmgr.h
2941fn mountmgrIsVolumeName(name: []const u16) bool {
2942 return (name.len == 48 or (name.len == 49 and name[48] == mem.nativeToLittle(u16, '\\'))) and
2943 name[0] == mem.nativeToLittle(u16, '\\') and
2944 (name[1] == mem.nativeToLittle(u16, '?') or name[1] == mem.nativeToLittle(u16, '\\')) and
2945 name[2] == mem.nativeToLittle(u16, '?') and
2946 name[3] == mem.nativeToLittle(u16, '\\') and
2947 mem.startsWith(u16, name[4..], std.unicode.utf8ToUtf16LeStringLiteral("Volume{")) and
2948 name[19] == mem.nativeToLittle(u16, '-') and
2949 name[24] == mem.nativeToLittle(u16, '-') and
2950 name[29] == mem.nativeToLittle(u16, '-') and
2951 name[34] == mem.nativeToLittle(u16, '-') and
2952 name[47] == mem.nativeToLittle(u16, '}');
2953}
2954
2955test mountmgrIsVolumeName {
2956 @setEvalBranchQuota(2000);
2957 const L = std.unicode.utf8ToUtf16LeStringLiteral;
2958 try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
2959 try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
2960 try std.testing.expect(mountmgrIsVolumeName(L("\\\\?\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
2961 try std.testing.expect(mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\")));
2962 try std.testing.expect(!mountmgrIsVolumeName(L("\\\\.\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}")));
2963 try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58d}\\foo")));
2964 try std.testing.expect(!mountmgrIsVolumeName(L("\\??\\Volume{383da0b0-717f-41b6-8c36-00500992b58}")));
2965}
2966
2967test GetFinalPathNameByHandle {
2968 if (builtin.os.tag != .windows)
2969 return;
2970
2971 //any file will do
2972 var tmp = std.testing.tmpDir(.{});
2973 defer tmp.cleanup();
2974 const handle = tmp.dir.handle;
2975 var buffer: [PATH_MAX_WIDE]u16 = undefined;
2976
2977 //check with sufficient size
2978 const nt_path = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &buffer);
2979 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &buffer);
2980
2981 const required_len_in_u16 = nt_path.len + @divExact(@intFromPtr(nt_path.ptr) - @intFromPtr(&buffer), 2) + 1;
2982 //check with insufficient size
2983 try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0 .. required_len_in_u16 - 1]));
2984 try std.testing.expectError(error.NameTooLong, GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0 .. required_len_in_u16 - 1]));
2985
2986 //check with exactly-sufficient size
2987 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, buffer[0..required_len_in_u16]);
2988 _ = try GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, buffer[0..required_len_in_u16]);
2989}
2990
2991pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 {2550pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 {
2992 return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen)));2551 return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen)));
2993}2552}
...@@ -3419,15 +2978,6 @@ test "eqlIgnoreCaseWtf16/Wtf8" {...@@ -3419,15 +2978,6 @@ test "eqlIgnoreCaseWtf16/Wtf8" {
3419 try testEqlIgnoreCase(false, "𐓏", "𐓷");2978 try testEqlIgnoreCase(false, "𐓏", "𐓷");
3420}2979}
34212980
3422pub const PathSpace = struct {
3423 data: [PATH_MAX_WIDE:0]u16,
3424 len: usize,
3425
3426 pub fn span(self: *const PathSpace) [:0]const u16 {
3427 return self.data[0..self.len :0];
3428 }
3429};
3430
3431/// The error type for `removeDotDirsSanitized`2981/// The error type for `removeDotDirsSanitized`
3432pub const RemoveDotDirsError = error{TooManyParentDirs};2982pub const RemoveDotDirsError = error{TooManyParentDirs};
34332983
...@@ -3503,205 +3053,6 @@ pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize {...@@ -3503,205 +3053,6 @@ pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize {
3503 return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]);3053 return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]);
3504}3054}
35053055
3506pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError;
3507
3508/// Same as `sliceToPrefixedFileW` but accepts a pointer
3509/// to a null-terminated WTF-8 encoded path.
3510/// https://wtf-8.codeberg.page/
3511pub fn cStrToPrefixedFileW(dir: ?HANDLE, s: [*:0]const u8) Wtf8ToPrefixedFileWError!PathSpace {
3512 return sliceToPrefixedFileW(dir, mem.sliceTo(s, 0));
3513}
3514
3515/// Same as `wToPrefixedFileW` but accepts a WTF-8 encoded path.
3516/// https://wtf-8.codeberg.page/
3517pub fn sliceToPrefixedFileW(dir: ?HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!PathSpace {
3518 var temp_path: PathSpace = undefined;
3519 temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) {
3520 error.InvalidWtf8 => return error.BadPathName,
3521 };
3522 temp_path.data[temp_path.len] = 0;
3523 return wToPrefixedFileW(dir, temp_path.span());
3524}
3525
3526pub const Wtf16ToPrefixedFileWError = error{
3527 AccessDenied,
3528 BadPathName,
3529 FileNotFound,
3530 NameTooLong,
3531 Unexpected,
3532};
3533
3534/// Converts the `path` to WTF16, null-terminated. If the path contains any
3535/// namespace prefix, or is anything but a relative path (rooted, drive relative,
3536/// etc) the result will have the NT-style prefix `\??\`.
3537///
3538/// Similar to RtlDosPathNameToNtPathName_U with a few differences:
3539/// - Does not allocate on the heap.
3540/// - Relative paths are kept as relative unless they contain too many ..
3541/// components, in which case they are resolved against the `dir` if it
3542/// is non-null, or the CWD if it is null.
3543/// - Special case device names like COM1, NUL, etc are not handled specially (TODO)
3544/// - . and space are not stripped from the end of relative paths (potential TODO)
3545pub fn wToPrefixedFileW(dir: ?HANDLE, path: [:0]const u16) Wtf16ToPrefixedFileWError!PathSpace {
3546 const nt_prefix = [_]u16{ '\\', '?', '?', '\\' };
3547 if (hasCommonNtPrefix(u16, path)) {
3548 // TODO: Figure out a way to design an API that can avoid the copy for NT,
3549 // since it is always returned fully unmodified.
3550 var path_space: PathSpace = undefined;
3551 path_space.data[0..nt_prefix.len].* = nt_prefix;
3552 const len_after_prefix = path.len - nt_prefix.len;
3553 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
3554 path_space.len = path.len;
3555 path_space.data[path_space.len] = 0;
3556 return path_space;
3557 } else {
3558 const path_type = std.fs.path.getWin32PathType(u16, path);
3559 var path_space: PathSpace = undefined;
3560 if (path_type == .local_device) {
3561 switch (getLocalDevicePathType(u16, path)) {
3562 .verbatim => {
3563 path_space.data[0..nt_prefix.len].* = nt_prefix;
3564 const len_after_prefix = path.len - nt_prefix.len;
3565 @memcpy(path_space.data[nt_prefix.len..][0..len_after_prefix], path[nt_prefix.len..]);
3566 path_space.len = path.len;
3567 path_space.data[path_space.len] = 0;
3568 return path_space;
3569 },
3570 .local_device, .fake_verbatim => {
3571 const path_byte_len = ntdll.RtlGetFullPathName_U(
3572 path.ptr,
3573 path_space.data.len * 2,
3574 &path_space.data,
3575 null,
3576 );
3577 if (path_byte_len == 0) {
3578 // TODO: This may not be the right error
3579 return error.BadPathName;
3580 } else if (path_byte_len / 2 > path_space.data.len) {
3581 return error.NameTooLong;
3582 }
3583 path_space.len = path_byte_len / 2;
3584 // Both prefixes will be normalized but retained, so all
3585 // we need to do now is replace them with the NT prefix
3586 path_space.data[0..nt_prefix.len].* = nt_prefix;
3587 return path_space;
3588 },
3589 }
3590 }
3591 relative: {
3592 if (path_type == .relative) {
3593 // TODO: Handle special case device names like COM1, AUX, NUL, CONIN$, CONOUT$, etc.
3594 // See https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
3595
3596 // TODO: Potentially strip all trailing . and space characters from the
3597 // end of the path. This is something that both RtlDosPathNameToNtPathName_U
3598 // and RtlGetFullPathName_U do. Technically, trailing . and spaces
3599 // are allowed, but such paths may not interact well with Windows (i.e.
3600 // files with these paths can't be deleted from explorer.exe, etc).
3601 // This could be something that normalizePath may want to do.
3602
3603 @memcpy(path_space.data[0..path.len], path);
3604 // Try to normalize, but if we get too many parent directories,
3605 // then we need to start over and use RtlGetFullPathName_U instead.
3606 path_space.len = normalizePath(u16, path_space.data[0..path.len]) catch |err| switch (err) {
3607 error.TooManyParentDirs => break :relative,
3608 };
3609 path_space.data[path_space.len] = 0;
3610 return path_space;
3611 }
3612 }
3613 // We now know we are going to return an absolute NT path, so
3614 // we can unconditionally prefix it with the NT prefix.
3615 path_space.data[0..nt_prefix.len].* = nt_prefix;
3616 if (path_type == .root_local_device) {
3617 // `\\.` and `\\?` always get converted to `\??\` exactly, so
3618 // we can just stop here
3619 path_space.len = nt_prefix.len;
3620 path_space.data[path_space.len] = 0;
3621 return path_space;
3622 }
3623 const path_buf_offset = switch (path_type) {
3624 // UNC paths will always start with `\\`. However, we want to
3625 // end up with something like `\??\UNC\server\share`, so to get
3626 // RtlGetFullPathName to write into the spot we want the `server`
3627 // part to end up, we need to provide an offset such that
3628 // the `\\` part gets written where the `C\` of `UNC\` will be
3629 // in the final NT path.
3630 .unc_absolute => nt_prefix.len + 2,
3631 else => nt_prefix.len,
3632 };
3633 const buf_len: u32 = @intCast(path_space.data.len - path_buf_offset);
3634 const path_to_get: [:0]const u16 = path_to_get: {
3635 // If dir is null, then we don't need to bother with GetFinalPathNameByHandle because
3636 // RtlGetFullPathName_U will resolve relative paths against the CWD for us.
3637 if (path_type != .relative or dir == null) {
3638 break :path_to_get path;
3639 }
3640 // We can also skip GetFinalPathNameByHandle if the handle matches
3641 // the handle returned by Io.Dir.cwd()
3642 if (dir.? == Io.Dir.cwd().handle) {
3643 break :path_to_get path;
3644 }
3645 // At this point, we know we have a relative path that had too many
3646 // `..` components to be resolved by normalizePath, so we need to
3647 // convert it into an absolute path and let RtlGetFullPathName_U
3648 // canonicalize it. We do this by getting the path of the `dir`
3649 // and appending the relative path to it.
3650 var dir_path_buf: [PATH_MAX_WIDE:0]u16 = undefined;
3651 const dir_path = GetFinalPathNameByHandle(dir.?, .{}, &dir_path_buf) catch |err| switch (err) {
3652 // This mapping is not correct; it is actually expected
3653 // that calling GetFinalPathNameByHandle might return
3654 // error.UnrecognizedVolume, and in fact has been observed
3655 // in the wild. The problem is that wToPrefixedFileW was
3656 // never intended to make *any* OS syscall APIs. It's only
3657 // supposed to convert a string to one that is eligible to
3658 // be used in the ntdll syscalls.
3659 //
3660 // To solve this, this function needs to no longer call
3661 // GetFinalPathNameByHandle under any conditions, or the
3662 // calling function needs to get reworked to not need to
3663 // call this function.
3664 //
3665 // This may involve making breaking API changes.
3666 error.UnrecognizedVolume => return error.Unexpected,
3667 else => |e| return e,
3668 };
3669 if (dir_path.len + 1 + path.len > PATH_MAX_WIDE) {
3670 return error.NameTooLong;
3671 }
3672 // We don't have to worry about potentially doubling up path separators
3673 // here since RtlGetFullPathName_U will handle canonicalizing it.
3674 dir_path_buf[dir_path.len] = '\\';
3675 @memcpy(dir_path_buf[dir_path.len + 1 ..][0..path.len], path);
3676 const full_len = dir_path.len + 1 + path.len;
3677 dir_path_buf[full_len] = 0;
3678 break :path_to_get dir_path_buf[0..full_len :0];
3679 };
3680 const path_byte_len = ntdll.RtlGetFullPathName_U(
3681 path_to_get.ptr,
3682 buf_len * 2,
3683 path_space.data[path_buf_offset..].ptr,
3684 null,
3685 );
3686 if (path_byte_len == 0) {
3687 // TODO: This may not be the right error
3688 return error.BadPathName;
3689 } else if (path_byte_len / 2 > buf_len) {
3690 return error.NameTooLong;
3691 }
3692 path_space.len = path_buf_offset + (path_byte_len / 2);
3693 if (path_type == .unc_absolute) {
3694 // Now add in the UNC, the `C` should overwrite the first `\` of the
3695 // FullPathName, ultimately resulting in `\??\UNC\<the rest of the path>`
3696 assert(path_space.data[path_buf_offset] == '\\');
3697 assert(path_space.data[path_buf_offset + 1] == '\\');
3698 const unc = [_]u16{ 'U', 'N', 'C' };
3699 path_space.data[nt_prefix.len..][0..unc.len].* = unc;
3700 }
3701 return path_space;
3702 }
3703}
3704
3705/// Returns true if the path starts with `\??\`, which is indicative of an NT path3056/// Returns true if the path starts with `\??\`, which is indicative of an NT path
3706/// but is not enough to fully distinguish between NT paths and Win32 paths, as3057/// but is not enough to fully distinguish between NT paths and Win32 paths, as
3707/// `\??\` is not actually a distinct prefix but rather the path to a special virtual3058/// `\??\` is not actually a distinct prefix but rather the path to a special virtual
...@@ -3725,39 +3076,6 @@ pub fn hasCommonNtPrefix(comptime T: type, path: []const T) bool {...@@ -3725,39 +3076,6 @@ pub fn hasCommonNtPrefix(comptime T: type, path: []const T) bool {
3725 return mem.startsWith(T, path, expected_prefix);3076 return mem.startsWith(T, path, expected_prefix);
3726}3077}
37273078
3728const LocalDevicePathType = enum {
3729 /// `\\.\` (path separators can be `\` or `/`)
3730 local_device,
3731 /// `\\?\`
3732 /// When converted to an NT path, everything past the prefix is left
3733 /// untouched and `\\?\` is replaced by `\??\`.
3734 verbatim,
3735 /// `\\?\` without all path separators being `\`.
3736 /// This seems to be recognized as a prefix, but the 'verbatim' aspect
3737 /// is not respected (i.e. if `//?/C:/foo` is converted to an NT path,
3738 /// it will become `\??\C:\foo` [it will be canonicalized and the //?/ won't
3739 /// be treated as part of the final path])
3740 fake_verbatim,
3741};
3742
3743/// Only relevant for Win32 -> NT path conversion.
3744/// Asserts `path` is of type `std.fs.path.Win32PathType.local_device`.
3745fn getLocalDevicePathType(comptime T: type, path: []const T) LocalDevicePathType {
3746 if (std.debug.runtime_safety) {
3747 assert(std.fs.path.getWin32PathType(T, path) == .local_device);
3748 }
3749
3750 const backslash = mem.nativeToLittle(T, '\\');
3751 const all_backslash = path[0] == backslash and
3752 path[1] == backslash and
3753 path[3] == backslash;
3754 return switch (path[2]) {
3755 mem.nativeToLittle(T, '?') => if (all_backslash) .verbatim else .fake_verbatim,
3756 mem.nativeToLittle(T, '.') => .local_device,
3757 else => unreachable,
3758 };
3759}
3760
3761/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.3079/// Similar to `RtlNtPathNameToDosPathName` but does not do any heap allocation.
3762/// The possible transformations are:3080/// The possible transformations are:
3763/// \??\C:\Some\Path -> C:\Some\Path3081/// \??\C:\Some\Path -> C:\Some\Path
lib/std/os/windows/test.zig deleted-339
...@@ -1,339 +0,0 @@
1const std = @import("../../std.zig");
2const builtin = @import("builtin");
3const windows = std.os.windows;
4const mem = std.mem;
5const testing = std.testing;
6
7/// Wrapper around RtlDosPathNameToNtPathName_U for use in comparing
8/// the behavior of RtlDosPathNameToNtPathName_U with wToPrefixedFileW
9/// Note: RtlDosPathNameToNtPathName_U is not used in the Zig implementation
10// because it allocates.
11fn RtlDosPathNameToNtPathName_U(path: [:0]const u16) !windows.PathSpace {
12 var out: windows.UNICODE_STRING = undefined;
13 const rc = windows.ntdll.RtlDosPathNameToNtPathName_U(path, &out, null, null);
14 if (rc != windows.TRUE) return error.BadPathName;
15 defer windows.ntdll.RtlFreeUnicodeString(&out);
16
17 var path_space: windows.PathSpace = undefined;
18 const out_path = out.Buffer.?[0 .. out.Length / 2];
19 @memcpy(path_space.data[0..out_path.len], out_path);
20 path_space.len = out.Length / 2;
21 path_space.data[path_space.len] = 0;
22
23 return path_space;
24}
25
26/// Test that the Zig conversion matches the expected_path (for instances where
27/// the Zig implementation intentionally diverges from what RtlDosPathNameToNtPathName_U does).
28fn testToPrefixedFileNoOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
29 const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
30 const expected_path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(expected_path);
31 const actual_path = try windows.wToPrefixedFileW(null, path_utf16);
32 std.testing.expectEqualSlices(u16, expected_path_utf16, actual_path.span()) catch |e| {
33 std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(actual_path.span()), std.unicode.fmtUtf16Le(expected_path_utf16) });
34 return e;
35 };
36}
37
38/// Test that the Zig conversion matches the expected_path and that the
39/// expected_path matches the conversion that RtlDosPathNameToNtPathName_U does.
40fn testToPrefixedFileWithOracle(comptime path: []const u8, comptime expected_path: []const u8) !void {
41 try testToPrefixedFileNoOracle(path, expected_path);
42 try testToPrefixedFileOnlyOracle(path);
43}
44
45/// Test that the Zig conversion matches the conversion that RtlDosPathNameToNtPathName_U does.
46fn testToPrefixedFileOnlyOracle(comptime path: []const u8) !void {
47 const path_utf16 = std.unicode.utf8ToUtf16LeStringLiteral(path);
48 const zig_result = try windows.wToPrefixedFileW(null, path_utf16);
49 const win32_api_result = try RtlDosPathNameToNtPathName_U(path_utf16);
50 std.testing.expectEqualSlices(u16, win32_api_result.span(), zig_result.span()) catch |e| {
51 std.debug.print("got '{f}', expected '{f}'\n", .{ std.unicode.fmtUtf16Le(zig_result.span()), std.unicode.fmtUtf16Le(win32_api_result.span()) });
52 return e;
53 };
54}
55
56test "toPrefixedFileW" {
57 if (builtin.os.tag != .windows) return error.SkipZigTest;
58
59 // Most test cases come from https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
60 // Note that these tests do not actually touch the filesystem or care about whether or not
61 // any of the paths actually exist or are otherwise valid.
62
63 // Drive Absolute
64 try testToPrefixedFileWithOracle("X:\\ABC\\DEF", "\\??\\X:\\ABC\\DEF");
65 try testToPrefixedFileWithOracle("X:\\", "\\??\\X:\\");
66 try testToPrefixedFileWithOracle("X:\\ABC\\", "\\??\\X:\\ABC\\");
67 // Trailing . and space characters are stripped
68 try testToPrefixedFileWithOracle("X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
69 try testToPrefixedFileWithOracle("X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
70 try testToPrefixedFileWithOracle("X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
71 try testToPrefixedFileWithOracle("X:\\ABC\\..\\..\\..", "\\??\\X:\\");
72 // Drive letter casing is unchanged
73 try testToPrefixedFileWithOracle("x:\\", "\\??\\x:\\");
74
75 // Drive Relative
76 // These tests depend on the CWD of the specified drive letter which can vary,
77 // so instead we just test that the Zig implementation matches the result of
78 // RtlDosPathNameToNtPathName_U.
79 // TODO: Setting the =X: environment variable didn't seem to affect
80 // RtlDosPathNameToNtPathName_U, not sure why that is but getting that
81 // to work could be an avenue to making these cases environment-independent.
82 // All -> are examples of the result if the X drive's cwd was X:\ABC
83 try testToPrefixedFileOnlyOracle("X:DEF\\GHI"); // -> \??\X:\ABC\DEF\GHI
84 try testToPrefixedFileOnlyOracle("X:"); // -> \??\X:\ABC
85 try testToPrefixedFileOnlyOracle("X:DEF. ."); // -> \??\X:\ABC\DEF
86 try testToPrefixedFileOnlyOracle("X:ABC\\..\\XYZ"); // -> \??\X:\ABC\XYZ
87 try testToPrefixedFileOnlyOracle("X:ABC\\..\\..\\.."); // -> \??\X:\
88 try testToPrefixedFileOnlyOracle("x:"); // -> \??\X:\ABC
89
90 // Rooted
91 // These tests depend on the drive letter of the CWD which can vary, so
92 // instead we just test that the Zig implementation matches the result of
93 // RtlDosPathNameToNtPathName_U.
94 // TODO: Getting the CWD path, getting the drive letter from it, and using it to
95 // construct the expected NT paths could be an avenue to making these cases
96 // environment-independent and therefore able to use testToPrefixedFileWithOracle.
97 // All -> are examples of the result if the CWD's drive letter was X
98 try testToPrefixedFileOnlyOracle("\\ABC\\DEF"); // -> \??\X:\ABC\DEF
99 try testToPrefixedFileOnlyOracle("\\"); // -> \??\X:\
100 try testToPrefixedFileOnlyOracle("\\ABC\\DEF. ."); // -> \??\X:\ABC\DEF
101 try testToPrefixedFileOnlyOracle("/ABC/DEF"); // -> \??\X:\ABC\DEF
102 try testToPrefixedFileOnlyOracle("\\ABC\\..\\XYZ"); // -> \??\X:\XYZ
103 try testToPrefixedFileOnlyOracle("\\ABC\\..\\..\\.."); // -> \??\X:\
104
105 // Relative
106 // These cases differ in functionality to RtlDosPathNameToNtPathName_U.
107 // Relative paths remain relative if they don't have enough .. components
108 // to error with TooManyParentDirs
109 try testToPrefixedFileNoOracle("ABC\\DEF", "ABC\\DEF");
110 // TODO: enable this if trailing . and spaces are stripped from relative paths
111 //try testToPrefixedFileNoOracle("ABC\\DEF. .", "ABC\\DEF");
112 try testToPrefixedFileNoOracle("ABC/DEF", "ABC\\DEF");
113 try testToPrefixedFileNoOracle("./ABC/.././DEF", "DEF");
114 // TooManyParentDirs, so resolved relative to the CWD
115 // All -> are examples of the result if the CWD was X:\ABC\DEF
116 try testToPrefixedFileOnlyOracle("..\\GHI"); // -> \??\X:\ABC\GHI
117 try testToPrefixedFileOnlyOracle("GHI\\..\\..\\.."); // -> \??\X:\
118
119 // UNC Absolute
120 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
121 try testToPrefixedFileWithOracle("\\\\server", "\\??\\UNC\\server");
122 try testToPrefixedFileWithOracle("\\\\server\\share", "\\??\\UNC\\server\\share");
123 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC. .", "\\??\\UNC\\server\\share\\ABC");
124 try testToPrefixedFileWithOracle("//server/share/ABC/DEF", "\\??\\UNC\\server\\share\\ABC\\DEF");
125 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\XYZ", "\\??\\UNC\\server\\share\\XYZ");
126 try testToPrefixedFileWithOracle("\\\\server\\share\\ABC\\..\\..\\..", "\\??\\UNC\\server\\share");
127
128 // Local Device
129 try testToPrefixedFileWithOracle("\\\\.\\COM20", "\\??\\COM20");
130 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe", "\\??\\pipe\\mypipe");
131 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\DEF. .", "\\??\\X:\\ABC\\DEF");
132 try testToPrefixedFileWithOracle("\\\\.\\X:/ABC/DEF", "\\??\\X:\\ABC\\DEF");
133 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\XYZ", "\\??\\X:\\XYZ");
134 // Can replace the first component of the path (contrary to drive absolute and UNC absolute paths)
135 try testToPrefixedFileWithOracle("\\\\.\\X:\\ABC\\..\\..\\C:\\", "\\??\\C:\\");
136 try testToPrefixedFileWithOracle("\\\\.\\pipe\\mypipe\\..\\notmine", "\\??\\pipe\\notmine");
137
138 // Special-case device names
139 // TODO: Enable once these are supported
140 // more cases to test here: https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
141 //try testToPrefixedFileWithOracle("COM1", "\\??\\COM1");
142 // Sometimes the special-cased device names are not respected
143 try testToPrefixedFileWithOracle("\\\\.\\X:\\COM1", "\\??\\X:\\COM1");
144 try testToPrefixedFileWithOracle("\\\\abc\\xyz\\COM1", "\\??\\UNC\\abc\\xyz\\COM1");
145
146 // Verbatim
147 // Left untouched except \\?\ is replaced by \??\
148 try testToPrefixedFileWithOracle("\\\\?\\X:", "\\??\\X:");
149 try testToPrefixedFileWithOracle("\\\\?\\X:\\COM1", "\\??\\X:\\COM1");
150 try testToPrefixedFileWithOracle("\\\\?\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
151 try testToPrefixedFileWithOracle("\\\\?\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
152 // NT Namespace
153 // Fully unmodified
154 try testToPrefixedFileWithOracle("\\??\\X:", "\\??\\X:");
155 try testToPrefixedFileWithOracle("\\??\\X:\\COM1", "\\??\\X:\\COM1");
156 try testToPrefixedFileWithOracle("\\??\\X:/ABC/DEF. .", "\\??\\X:/ABC/DEF. .");
157 try testToPrefixedFileWithOracle("\\??\\X:\\ABC\\..\\..\\..", "\\??\\X:\\ABC\\..\\..\\..");
158
159 // 'Fake' Verbatim
160 // If the prefix looks like the verbatim prefix but not all path separators in the
161 // prefix are backslashes, then it gets canonicalized and the prefix is dropped in favor
162 // of the NT prefix.
163 try testToPrefixedFileWithOracle("//?/C:/ABC", "\\??\\C:\\ABC");
164 // 'Fake' NT
165 // If the prefix looks like the NT prefix but not all path separators in the prefix
166 // are backslashes, then it gets canonicalized and the /??/ is not dropped but
167 // rather treated as part of the path. In other words, the path is treated
168 // as a rooted path, so the final path is resolved relative to the CWD's
169 // drive letter.
170 // The -> shows an example of the result if the CWD's drive letter was X
171 try testToPrefixedFileOnlyOracle("/??/C:/ABC"); // -> \??\X:\??\C:\ABC
172
173 // Root Local Device
174 // \\. and \\? always get converted to \??\
175 try testToPrefixedFileWithOracle("\\\\.", "\\??\\");
176 try testToPrefixedFileWithOracle("\\\\?", "\\??\\");
177 try testToPrefixedFileWithOracle("//?", "\\??\\");
178 try testToPrefixedFileWithOracle("//.", "\\??\\");
179}
180
181fn testRemoveDotDirs(str: []const u8, expected: []const u8) !void {
182 const mutable = try testing.allocator.dupe(u8, str);
183 defer testing.allocator.free(mutable);
184 const actual = mutable[0..try windows.removeDotDirsSanitized(u8, mutable)];
185 try testing.expect(mem.eql(u8, actual, expected));
186}
187fn testRemoveDotDirsError(err: anyerror, str: []const u8) !void {
188 const mutable = try testing.allocator.dupe(u8, str);
189 defer testing.allocator.free(mutable);
190 try testing.expectError(err, windows.removeDotDirsSanitized(u8, mutable));
191}
192test "removeDotDirs" {
193 try testRemoveDotDirs("", "");
194 try testRemoveDotDirs(".", "");
195 try testRemoveDotDirs(".\\", "");
196 try testRemoveDotDirs(".\\.", "");
197 try testRemoveDotDirs(".\\.\\", "");
198 try testRemoveDotDirs(".\\.\\.", "");
199
200 try testRemoveDotDirs("a", "a");
201 try testRemoveDotDirs("a\\", "a\\");
202 try testRemoveDotDirs("a\\b", "a\\b");
203 try testRemoveDotDirs("a\\.", "a\\");
204 try testRemoveDotDirs("a\\b\\.", "a\\b\\");
205 try testRemoveDotDirs("a\\.\\b", "a\\b");
206
207 try testRemoveDotDirs(".a", ".a");
208 try testRemoveDotDirs(".a\\", ".a\\");
209 try testRemoveDotDirs(".a\\.b", ".a\\.b");
210 try testRemoveDotDirs(".a\\.", ".a\\");
211 try testRemoveDotDirs(".a\\.\\.", ".a\\");
212 try testRemoveDotDirs(".a\\.\\.\\.b", ".a\\.b");
213 try testRemoveDotDirs(".a\\.\\.\\.b\\", ".a\\.b\\");
214
215 try testRemoveDotDirsError(error.TooManyParentDirs, "..");
216 try testRemoveDotDirsError(error.TooManyParentDirs, "..\\");
217 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\..\\");
218 try testRemoveDotDirsError(error.TooManyParentDirs, ".\\.\\..\\");
219
220 try testRemoveDotDirs("a\\..", "");
221 try testRemoveDotDirs("a\\..\\", "");
222 try testRemoveDotDirs("a\\..\\.", "");
223 try testRemoveDotDirs("a\\..\\.\\", "");
224 try testRemoveDotDirs("a\\..\\.\\.", "");
225 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\..");
226
227 try testRemoveDotDirs("a\\..\\.\\.\\b", "b");
228 try testRemoveDotDirs("a\\..\\.\\.\\b\\", "b\\");
229 try testRemoveDotDirs("a\\..\\.\\.\\b\\.", "b\\");
230 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\", "b\\");
231 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..", "");
232 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\", "");
233 try testRemoveDotDirs("a\\..\\.\\.\\b\\.\\..\\.", "");
234 try testRemoveDotDirsError(error.TooManyParentDirs, "a\\..\\.\\.\\b\\.\\..\\.\\..");
235
236 try testRemoveDotDirs("a\\b\\..\\", "a\\");
237 try testRemoveDotDirs("a\\b\\..\\c", "a\\c");
238}
239
240const RTL_PATH_TYPE = enum(c_int) {
241 Unknown,
242 UncAbsolute,
243 DriveAbsolute,
244 DriveRelative,
245 Rooted,
246 Relative,
247 LocalDevice,
248 RootLocalDevice,
249};
250
251pub extern "ntdll" fn RtlDetermineDosPathNameType_U(
252 Path: [*:0]const u16,
253) callconv(.winapi) RTL_PATH_TYPE;
254
255test "getWin32PathType vs RtlDetermineDosPathNameType_U" {
256 if (builtin.os.tag != .windows) return error.SkipZigTest;
257
258 var buf: std.ArrayList(u16) = .empty;
259 defer buf.deinit(std.testing.allocator);
260
261 var wtf8_buf: std.ArrayList(u8) = .empty;
262 defer wtf8_buf.deinit(std.testing.allocator);
263
264 var random = std.Random.DefaultPrng.init(std.testing.random_seed);
265 const rand = random.random();
266
267 for (0..1000) |_| {
268 buf.clearRetainingCapacity();
269 const path = try getRandomWtf16Path(std.testing.allocator, &buf, rand);
270 wtf8_buf.clearRetainingCapacity();
271 const wtf8_len = std.unicode.calcWtf8Len(path);
272 try wtf8_buf.ensureTotalCapacity(std.testing.allocator, wtf8_len);
273 wtf8_buf.items.len = wtf8_len;
274 std.debug.assert(std.unicode.wtf16LeToWtf8(wtf8_buf.items, path) == wtf8_len);
275
276 const windows_type = RtlDetermineDosPathNameType_U(path);
277 const wtf16_type = std.fs.path.getWin32PathType(u16, path);
278 const wtf8_type = std.fs.path.getWin32PathType(u8, wtf8_buf.items);
279
280 checkPathType(windows_type, wtf16_type) catch |err| {
281 std.debug.print("expected type {}, got {} for path: {f}\n", .{ windows_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
282 std.debug.print("path bytes:\n", .{});
283 std.debug.dumpHex(std.mem.sliceAsBytes(path));
284 return err;
285 };
286
287 if (wtf16_type != wtf8_type) {
288 std.debug.print("type mismatch between wtf8: {} and wtf16: {} for path: {f}\n", .{ wtf8_type, wtf16_type, std.unicode.fmtUtf16Le(path) });
289 std.debug.print("wtf-16 path bytes:\n", .{});
290 std.debug.dumpHex(std.mem.sliceAsBytes(path));
291 std.debug.print("wtf-8 path bytes:\n", .{});
292 std.debug.dumpHex(std.mem.sliceAsBytes(wtf8_buf.items));
293 return error.Wtf8Wtf16Mismatch;
294 }
295 }
296}
297
298fn checkPathType(windows_type: RTL_PATH_TYPE, zig_type: std.fs.path.Win32PathType) !void {
299 const expected_windows_type: RTL_PATH_TYPE = switch (zig_type) {
300 .unc_absolute => .UncAbsolute,
301 .drive_absolute => .DriveAbsolute,
302 .drive_relative => .DriveRelative,
303 .rooted => .Rooted,
304 .relative => .Relative,
305 .local_device => .LocalDevice,
306 .root_local_device => .RootLocalDevice,
307 };
308 if (windows_type != expected_windows_type) return error.PathTypeMismatch;
309}
310
311fn getRandomWtf16Path(allocator: std.mem.Allocator, buf: *std.ArrayList(u16), rand: std.Random) ![:0]const u16 {
312 const Choice = enum {
313 backslash,
314 slash,
315 control,
316 printable,
317 non_ascii,
318 };
319
320 const choices = rand.uintAtMostBiased(u16, 32);
321
322 for (0..choices) |_| {
323 const choice = rand.enumValue(Choice);
324 const code_unit = switch (choice) {
325 .backslash => '\\',
326 .slash => '/',
327 .control => switch (rand.uintAtMostBiased(u8, 0x20)) {
328 0x20 => '\x7F',
329 else => |b| b + 1, // no NUL
330 },
331 .printable => '!' + rand.uintAtMostBiased(u8, '~' - '!'),
332 .non_ascii => rand.intRangeAtMostBiased(u16, 0x80, 0xFFFF),
333 };
334 try buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
335 }
336
337 try buf.append(allocator, 0);
338 return buf.items[0 .. buf.items.len - 1 :0];
339}
lib/std/zig/parser_test.zig+1-1
...@@ -639,7 +639,7 @@ test "zig fmt: array types last token" {...@@ -639,7 +639,7 @@ test "zig fmt: array types last token" {
639639
640test "zig fmt: sentinel-terminated array type" {640test "zig fmt: sentinel-terminated array type" {
641 try testCanonical(641 try testCanonical(
642 \\pub fn cStrToPrefixedFileW(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 {642 \\pub fn foobar(s: [*:0]const u8) ![PATH_MAX_WIDE:0]u16 {
643 \\ return sliceToPrefixedFileW(mem.toSliceConst(u8, s));643 \\ return sliceToPrefixedFileW(mem.toSliceConst(u8, s));
644 \\}644 \\}
645 \\645 \\
test/standalone/load_dynamic_library/build.zig+1
...@@ -9,6 +9,7 @@ pub fn build(b: *std.Build) void {...@@ -9,6 +9,7 @@ pub fn build(b: *std.Build) void {
9 const target = b.graph.host;9 const target = b.graph.host;
1010
11 if (builtin.os.tag == .wasi) return;11 if (builtin.os.tag == .wasi) return;
12 if (builtin.os.tag == .windows) return;
1213
13 const lib = b.addLibrary(.{14 const lib = b.addLibrary(.{
14 .linkage = .dynamic,15 .linkage = .dynamic,