diff --git a/lib/std/Io.zig b/lib/std/Io.zig index b16661be97..a3cb2cf29f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -660,7 +660,9 @@ pub const VTable = struct { conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, - dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, + dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void, + dirMakePath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void, + dirMakeOpenPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.MakeOpenPathError!Dir, dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 7336ab24af..ab130dca8e 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -348,6 +348,20 @@ pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!Make } } +pub const MakeOpenPathError = MakeError || OpenError || StatPathError; + +/// Performs the equivalent of `makePath` followed by `openDir`, atomically if possible. +/// +/// When this operation is canceled, it may leave the file system in a +/// partially modified state. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn makeOpenPath(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) MakeOpenPathError!Dir { + return io.vtable.dirMakeOpenPath(io.userdata, dir, sub_path, options); +} + pub const Stat = File.Stat; pub const StatError = File.StatError; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 174d4b76f3..6097fc6df3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -168,6 +168,15 @@ pub fn io(t: *Threaded) Io { .wasi => dirMakeWasi, else => dirMakePosix, }, + .dirMakePath = switch (builtin.os.tag) { + .windows => dirMakePathWindows, + else => dirMakePathPosix, + }, + .dirMakeOpenPath = switch (builtin.os.tag) { + .windows => dirMakeOpenPathWindows, + .wasi => dirMakeOpenPathWasi, + else => dirMakeOpenPathPosix, + }, .dirStat = dirStat, .dirStatPath = switch (builtin.os.tag) { .linux => dirStatPathLinux, @@ -197,7 +206,7 @@ pub fn io(t: *Threaded) Io { else => dirOpenFilePosix, }, .dirOpenDir = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => dirOpenDirWindows, .wasi => dirOpenDirWasi, else => dirOpenDirPosix, }, @@ -991,6 +1000,153 @@ fn dirMakeWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode windows.CloseHandle(sub_dir_handle); } +fn dirMakePathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} + +fn dirMakePathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} + +fn dirMakeOpenPathPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = t.io(); + return dir.openDir(t_io, sub_path, options) catch |err| switch (err) { + error.FileNotFound => { + try dir.makePath(t_io, sub_path); + return dir.openDir(t_io, sub_path, options); + }, + else => |e| return e, + }; +} + +fn dirMakeOpenPathWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const w = windows; + const access_mask = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE | + (if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); + + var it = try std.fs.path.componentIterator(sub_path); + // If there are no components in the path, then create a dummy component with the full path. + var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{ + .name = "", + .path = sub_path, + }; + + while (true) { + try t.checkCancel(); + + const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path); + const sub_path_w = sub_path_w_array.span(); + const is_last = it.peekNext() == null; + const create_disposition: u32 = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE; + + var result: Io.Dir = .{ .handle = undefined }; + + const path_len_bytes: u16 = @intCast(sub_path_w.len * 2); + var nt_name: w.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr: w.OBJECT_ATTRIBUTES = .{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io_status_block: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &result.handle, + access_mask, + &attr, + &io_status_block, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, + create_disposition, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => { + component = it.next() orelse return result; + w.CloseHandle(result.handle); + continue; + }, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_COLLISION => { + assert(!is_last); + // stat the file and return an error if it's not a directory + // this is important because otherwise a dangling symlink + // could cause an infinite loop + check_dir: { + // workaround for windows, see https://github.com/ziglang/zig/issues/16738 + const fstat = dir.statPath(t.io(), component.path, .{ + .follow_symlinks = options.follow_symlinks, + }) catch |stat_err| switch (stat_err) { + error.IsDir => break :check_dir, + else => |e| return e, + }; + if (fstat.kind != .directory) return error.NotDir; + } + + component = it.next().?; + continue; + }, + + .OBJECT_NAME_NOT_FOUND, + .OBJECT_PATH_NOT_FOUND, + => { + component = it.previous() orelse return error.FileNotFound; + continue; + }, + + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => |err| return w.statusBug(err), + else => return w.unexpectedStatus(rc), + } + } +} + +fn dirMakeOpenPathWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} + fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); @@ -1859,6 +2015,75 @@ fn dirOpenDirPosix( @panic("TODO"); } +fn dirOpenDirWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const w = windows; + const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = sub_path_w_array.span(); + + // TODO remove some of these flags if options.access_sub_paths is false + const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE; + const access_mask: u32 = if (options.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; + + const path_len_bytes: u16 = @intCast(sub_path_w.len * 2); + var nt_name: w.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr: w.OBJECT_ATTRIBUTES = .{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io_status_block: w.IO_STATUS_BLOCK = undefined; + var result: Io.Dir = .{ .handle = undefined }; + const rc = w.ntdll.NtCreateFile( + &result.handle, + access_mask, + &attr, + &io_status_block, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, + w.FILE_OPEN, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => return result, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_NAME_COLLISION => |err| return w.statusBug(err), + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => |err| return w.statusBug(err), + else => return w.unexpectedStatus(rc), + } +} + +const MakeOpenDirAccessMaskWOptions = struct { + no_follow: bool, + create_disposition: u32, +}; + fn dirClose(userdata: ?*anyopaque, dir: Io.Dir) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; @@ -2304,17 +2529,17 @@ fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestam const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; switch (clock) { - .realtime => { + .real => { // RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds // and uses the NTFS/Windows epoch, which is 1601-01-01. return .{ .nanoseconds = @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100 }; }, - .monotonic, .uptime => { + .awake, .boot => { // QPC on windows doesn't fail on >= XP/2000 and includes time suspended. - return .{ .timestamp = windows.QueryPerformanceCounter() }; + return .{ .nanoseconds = windows.QueryPerformanceCounter() }; }, - .process_cputime_id, - .thread_cputime_id, + .cpu_process, + .cpu_thread, => return error.UnsupportedClock, } } @@ -2360,9 +2585,9 @@ fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); const ms = ms: { - const duration_and_clock = (try timeout.toDurationFromNow(t.io())) orelse + const d = (try timeout.toDurationFromNow(t.io())) orelse break :ms std.math.maxInt(windows.DWORD); - break :ms std.math.lossyCast(windows.DWORD, duration_and_clock.duration.toMilliseconds()); + break :ms std.math.lossyCast(windows.DWORD, d.raw.toMilliseconds()); }; windows.kernel32.Sleep(ms); } diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 1b2182f0f7..1f179241b0 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -898,85 +898,11 @@ pub fn makePathStatus(self: Dir, sub_path: []const u8) MakePathError!MakePathSta return Io.Dir.makePathStatus(.{ .handle = self.fd }, io, sub_path); } -/// Windows only. Calls makeOpenDirAccessMaskW iteratively to make an entire path -/// (i.e. creating any parent directories that do not exist). -/// Opens the dir if the path already exists and is a directory. -/// This function is not atomic, and if it returns an error, the file system may -/// have been modified regardless. -/// `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) (MakeError || OpenError || StatFileError)!Dir { - const w = windows; - var it = try fs.path.componentIterator(sub_path); - // If there are no components in the path, then create a dummy component with the full path. - var component = it.last() orelse fs.path.NativeComponentIterator.Component{ - .name = "", - .path = sub_path, - }; - - while (true) { - const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); - const is_last = it.peekNext() == null; - var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ - .no_follow = no_follow, - .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, - }) catch |err| switch (err) { - error.FileNotFound => |e| { - component = it.previous() orelse return e; - continue; - }, - error.PathAlreadyExists => result: { - assert(!is_last); - // stat the file and return an error if it's not a directory - // this is important because otherwise a dangling symlink - // could cause an infinite loop - check_dir: { - // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = self.statFile(component.path) catch |stat_err| switch (stat_err) { - error.IsDir => break :check_dir, - else => |e| return e, - }; - if (fstat.kind != .directory) return error.NotDir; - } - break :result null; - }, - else => |e| return e, - }; - - component = it.next() orelse return result.?; - - // Don't leak the intermediate file handles - if (result) |*dir| { - dir.close(); - } - } -} - -/// This function performs `makePath`, followed by `openDir`. -/// If supported by the OS, this operation is atomic. It is not atomic on -/// all operating systems. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenOptions) (MakeError || OpenError || StatFileError)!Dir { - return switch (native_os) { - .windows => { - const w = windows; - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE | - (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); - - return self.makeOpenPathAccessMaskW(sub_path, base_flags, !open_dir_options.follow_symlinks); - }, - else => { - return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { - error.FileNotFound => { - try self.makePath(sub_path); - return self.openDir(sub_path, open_dir_options); - }, - else => |e| return e, - }; - }, - }; +/// Deprecated in favor of `Io.Dir.makeOpenPath`. +pub fn makeOpenPath(dir: Dir, sub_path: []const u8, options: OpenOptions) Io.Dir.MakeOpenPathError!Dir { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.Dir.makeOpenPath(dir.adaptToNewApi(), io, sub_path, options)); } pub const RealPathError = posix.RealPathError || error{Canceled}; @@ -1145,8 +1071,9 @@ pub const OpenOptions = Io.Dir.OpenOptions; pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir { switch (native_os) { .windows => { - const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.openDirW(sub_path_w.span().ptr, args); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args)); }, .wasi => if (!builtin.link_libc) { var threaded: Io.Threaded = .init_single_threaded; @@ -1163,8 +1090,7 @@ pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenError!Dir { switch (native_os) { .windows => { - const sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.openDirW(sub_path_w.span().ptr, args); + @compileError("use std.Io instead"); }, // Use the libc API when libc is linked because it implements things // such as opening absolute directory paths. @@ -1215,28 +1141,6 @@ pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenErr return self.openDirFlagsZ(sub_path_c, symlink_flags); } -/// Same as `openDir` except the path parameter is WTF-16 LE encoded, NT-prefixed. -/// This function asserts the target OS is Windows. -pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenOptions) OpenError!Dir { - const w = windows; - // TODO remove some of these flags if args.access_sub_paths is false - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE; - const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; - const dir = self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ - .no_follow = !args.follow_symlinks, - .create_disposition = w.FILE_OPEN, - }) catch |err| switch (err) { - error.ReadOnlyFileSystem => unreachable, - error.DiskQuota => unreachable, - error.NoSpaceLeft => unreachable, - error.PathAlreadyExists => unreachable, - error.LinkQuotaExceeded => unreachable, - else => |e| return e, - }; - return dir; -} - /// Asserts `flags` has `DIRECTORY` set. fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: posix.O) OpenError!Dir { assert(flags.DIRECTORY); @@ -1257,63 +1161,6 @@ fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: posix.O) OpenError return Dir{ .fd = fd }; } -const MakeOpenDirAccessMaskWOptions = struct { - no_follow: bool, - create_disposition: u32, -}; - -fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) (MakeError || OpenError)!Dir { - const w = windows; - - var result = Dir{ - .fd = undefined, - }; - - const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(sub_path_w), - }; - var attr = w.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; - var io: w.IO_STATUS_BLOCK = undefined; - const rc = w.ntdll.NtCreateFile( - &result.fd, - access_mask, - &attr, - &io, - null, - w.FILE_ATTRIBUTE_NORMAL, - w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, - flags.create_disposition, - w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, - null, - 0, - ); - - switch (rc) { - .SUCCESS => return result, - .OBJECT_NAME_INVALID => return error.BadPathName, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NOT_A_DIRECTORY => return error.NotDir, - // This can happen if the directory has 'List folder contents' permission set to 'Deny' - // and the directory is trying to be opened for iteration. - .ACCESS_DENIED => return error.AccessDenied, - .INVALID_PARAMETER => unreachable, - else => return w.unexpectedStatus(rc), - } -} - pub const DeleteFileError = posix.UnlinkError; /// Delete a file name and possibly the file it refers to, based on an open directory handle.