std.Io: add dirMakePath and dirMakeOpenPath

This commit is contained in:
Andrew Kelley 2025-10-19 23:30:39 -07:00
parent 71c86e1d28
commit dc6a4f3bf1
4 changed files with 259 additions and 171 deletions

View File

@ -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,

View File

@ -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;

View File

@ -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);
}

View File

@ -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.