mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 06:13:07 +00:00
Previously, fs.path handled a few of the Windows path types, but not all of them, and only a few of them correctly/consistently. This commit aims to make `std.fs.path` correct and consistent in handling all possible Win32 path types. This commit also slightly nudges the codebase towards a separation of Win32 paths and NT paths, as NT paths are not actually distinguishable from Win32 paths from looking at their contents alone (i.e. `\Device\Foo` could be an NT path or a Win32 rooted path, no way to tell without external context). This commit formalizes `std.fs.path` being fully concerned with Win32 paths, and having no special detection/handling of NT paths. Resources on Windows path types, and Win32 vs NT paths: - https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html - https://chrisdenton.github.io/omnipath/Overview.html - https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file API additions/changes/deprecations - `std.os.windows.getWin32PathType` was added (it is analogous to `RtlDetermineDosPathNameType_U`), while `std.os.windows.getNamespacePrefix` and `std.os.windows.getUnprefixedPathType` were deleted. `getWin32PathType` forms the basis on which the updated `std.fs.path` functions operate. - `std.fs.path.parsePath`, `std.fs.path.parsePathPosix`, and `std.fs.path.parsePathWindows` were added, while `std.fs.path.windowsParsePath` was deprecated. The new `parsePath` functions provide the "root" and the "kind" of a path, which is platform-specific. The now-deprecated `windowsParsePath` did not handle all possible path types, while the new `parsePathWindows` does. - `std.fs.path.diskDesignator` has been deprecated in favor of `std.fs.path.parsePath`, and same deal with `diskDesignatorWindows` -> `parsePathWindows` - `relativeWindows` is now a compile error when *not* targeting Windows, while `relativePosix` is now a compile error when targeting Windows. This is because those functions read/use the CWD path which will behave improperly when used from a system with different path semantics (e.g. calling `relativePosix` from a Windows system with a CWD like `C:\foo\bar` will give you a bogus result since that'd be treated as a single relative component when using POSIX semantics). This also allows `relativeWindows` to use Windows-specific APIs for getting the CWD and environment variables to cut down on allocations. - `componentIterator`/`ComponentIterator.init` have been made infallible. These functions used to be able to error on UNC paths with an empty server component, and on paths that were assumed to be NT paths, but now: + We follow the lead of `RtlDetermineDosPathNameType_U`/`RtlGetFullPathName_U` in how it treats a UNC path with an empty server name (e.g. `\\\share`) and allow it, even if it'll be invalid at the time of usage + Now that `std.fs.path` assumes paths are Win32 paths and not NT paths, we don't have to worry about NT paths Behavior changes - `std.fs.path` generally: any combinations of mixed path separators for UNC paths are universally supported, e.g. `\/server/share`, `/\server\share`, `/\server/\\//share` are all seen as equivalent UNC paths - `resolveWindows` handles all path types more appropriately/consistently. + `//` and `//foo` used to be treated as a relative path, but are now seen as UNC paths + If a rooted/drive-relative path cannot be resolved against anything more definite, the result will remain a rooted/drive-relative path. + I've created [a script to generate the results of a huge number of permutations of different path types](https://gist.github.com/squeek502/9eba7f19cad0d0d970ccafbc30f463bf) (the result of running the script is also included for anyone that'd like to vet the behavior). - `dirnameWindows` now treats the drive-relative root as the dirname of a drive-relative path with a component, e.g. `dirname("C:foo")` is now `C:`, whereas before it would return null. `dirnameWindows` also handles local device paths appropriately now. - `basenameWindows` now handles all path types more appropriately. The most notable change here is `//a` being treated as a partial UNC path now and therefore `basename` will return `""` for it, whereas before it would return `"a"` - `relativeWindows` will now do its best to resolve against the most appropriate CWD for each path, e.g. relative for `D:foo` will look at the CWD to check if the drive letter matches, and if not, look at the special environment variable `=D:` to get the shell-defined CWD for that drive, and if that doesn't exist, then it'll resolve against `D:\`. Implementation details - `resolveWindows` previously looped through the paths twice to build up the relevant info before doing the actual resolution. Now, `resolveWindows` iterates backwards once and keeps track of which paths are actually relevant using a bit set, which also allows it to break from the loop when it's no longer possible for earlier paths to matter. - A standalone test was added to test parts of `relativeWindows` since the CWD resolution logic depends on CWD information from the PEB and environment variables Edge cases worth noting - A strange piece of trivia that I found out while working on this is that it's technically possible to have a drive letter that it outside the intended A-Z range, or even outside the ASCII range entirely. Since we deal with both WTF-8 and WTF-16 paths, `path[0]`/`path[1]`/`path[2]` will not always refer to the same bits of information, so to get consistent behavior, some decision about how to deal with this edge case had to be made. I've made the choice to conform with how `RtlDetermineDosPathNameType_U` works, i.e. treat the first WTF-16 code unit as the drive letter. This means that when working with WTF-8, checking for drive-relative/drive-absolute paths is a bit more complicated. For more details, see the lengthy comment in `std.os.windows.getWin32PathType` - `relativeWindows` will now almost always be able to return either a fully-qualified absolute path or a relative path, but there's one scenario where it may return a rooted path: when the CWD gotten from the PEB is not a drive-absolute or UNC path (if that's actually feasible/possible?). An alternative approach to this scenario might be to resolve against the `HOMEDRIVE` env var if available, and/or default to `C:\` as a last resort in order to guarantee the result of `relative` is never a rooted path. - Partial UNC paths (e.g. `\\server` instead of `\\server\share`) are a bit awkward to handle, generally. Not entirely sure how best to handle them, so there may need to be another pass in the future to iron out any issues that arise. As of now the behavior is: + For `relative`, any part of a UNC disk designator is treated as the "root" and therefore isn't applicable for relative paths, e.g. calling `relative` with `\\server` and `\\server\share` will result in `\\server\share` rather than just `share` and if `relative` is called with `\\server\foo` and `\\server\bar` the result will be `\\server\bar` rather than `..\bar` + For `resolve`, any part of a UNC disk designator is also treated as the "root", but relative and rooted paths are still elligable for filling in missing portions of the disk designator, e.g. `resolve` with `\\server` and `foo` or `\foo` will result in `\\server\foo` Fixes #25703 Closes #25702
393 lines
15 KiB
Zig
393 lines
15 KiB
Zig
const Dir = @This();
|
|
|
|
const builtin = @import("builtin");
|
|
const native_os = builtin.os.tag;
|
|
|
|
const std = @import("../std.zig");
|
|
const Io = std.Io;
|
|
const File = Io.File;
|
|
|
|
handle: Handle,
|
|
|
|
pub const Mode = Io.File.Mode;
|
|
pub const default_mode: Mode = 0o755;
|
|
|
|
/// Returns a handle to the current working directory.
|
|
///
|
|
/// It is not opened with iteration capability. Iterating over the result is
|
|
/// illegal behavior.
|
|
///
|
|
/// Closing the returned `Dir` is checked illegal behavior.
|
|
///
|
|
/// On POSIX targets, this function is comptime-callable.
|
|
pub fn cwd() Dir {
|
|
return switch (native_os) {
|
|
.windows => .{ .handle = std.os.windows.peb().ProcessParameters.CurrentDirectory.Handle },
|
|
.wasi => .{ .handle = std.options.wasiCwd() },
|
|
else => .{ .handle = std.posix.AT.FDCWD },
|
|
};
|
|
}
|
|
|
|
pub const Handle = std.posix.fd_t;
|
|
|
|
pub const PathNameError = error{
|
|
NameTooLong,
|
|
/// File system cannot encode the requested file name bytes.
|
|
/// Could be due to invalid WTF-8 on Windows, invalid UTF-8 on WASI,
|
|
/// invalid characters on Windows, etc. Filesystem and operating specific.
|
|
BadPathName,
|
|
};
|
|
|
|
pub const AccessError = error{
|
|
AccessDenied,
|
|
PermissionDenied,
|
|
FileNotFound,
|
|
InputOutput,
|
|
SystemResources,
|
|
FileBusy,
|
|
SymLinkLoop,
|
|
ReadOnlyFileSystem,
|
|
} || PathNameError || Io.Cancelable || Io.UnexpectedError;
|
|
|
|
pub const AccessOptions = packed struct {
|
|
follow_symlinks: bool = true,
|
|
read: bool = false,
|
|
write: bool = false,
|
|
execute: bool = false,
|
|
};
|
|
|
|
/// Test accessing `sub_path`.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this
|
|
/// function. For example, instead of testing if a file exists and then opening
|
|
/// it, just open it and handle the error for file not found.
|
|
pub fn access(dir: Dir, io: Io, sub_path: []const u8, options: AccessOptions) AccessError!void {
|
|
return io.vtable.dirAccess(io.userdata, dir, sub_path, options);
|
|
}
|
|
|
|
pub const OpenError = error{
|
|
FileNotFound,
|
|
NotDir,
|
|
AccessDenied,
|
|
PermissionDenied,
|
|
SymLinkLoop,
|
|
ProcessFdQuotaExceeded,
|
|
SystemFdQuotaExceeded,
|
|
NoDevice,
|
|
SystemResources,
|
|
DeviceBusy,
|
|
/// On Windows, `\\server` or `\\server\share` was not found.
|
|
NetworkNotFound,
|
|
} || PathNameError || Io.Cancelable || Io.UnexpectedError;
|
|
|
|
pub const OpenOptions = struct {
|
|
/// `true` means the opened directory can be used as the `Dir` parameter
|
|
/// for functions which operate based on an open directory handle. When `false`,
|
|
/// such operations are Illegal Behavior.
|
|
access_sub_paths: bool = true,
|
|
/// `true` means the opened directory can be scanned for the files and sub-directories
|
|
/// of the result. It means the `iterate` function can be called.
|
|
iterate: bool = false,
|
|
/// `false` means it won't dereference the symlinks.
|
|
follow_symlinks: bool = true,
|
|
};
|
|
|
|
/// Opens a directory at the given path. The directory is a system resource that remains
|
|
/// open until `close` is called on the result.
|
|
///
|
|
/// The directory cannot be iterated unless the `iterate` option is set to `true`.
|
|
///
|
|
/// 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 openDir(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) OpenError!Dir {
|
|
return io.vtable.dirOpenDir(io.userdata, dir, sub_path, options);
|
|
}
|
|
|
|
pub fn close(dir: Dir, io: Io) void {
|
|
return io.vtable.dirClose(io.userdata, dir);
|
|
}
|
|
|
|
/// Opens a file for reading or writing, without attempting to create a new file.
|
|
///
|
|
/// To create a new file, see `createFile`.
|
|
///
|
|
/// Allocates a resource to be released with `File.close`.
|
|
///
|
|
/// 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 openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File {
|
|
return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags);
|
|
}
|
|
|
|
/// Creates, opens, or overwrites a file with write access.
|
|
///
|
|
/// Allocates a resource to be dellocated with `File.close`.
|
|
///
|
|
/// 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 createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File {
|
|
return io.vtable.dirCreateFile(io.userdata, dir, sub_path, flags);
|
|
}
|
|
|
|
pub const WriteFileOptions = struct {
|
|
/// 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.
|
|
sub_path: []const u8,
|
|
data: []const u8,
|
|
flags: File.CreateFlags = .{},
|
|
};
|
|
|
|
pub const WriteFileError = File.WriteError || File.OpenError || Io.Cancelable;
|
|
|
|
/// Writes content to the file system, using the file creation flags provided.
|
|
pub fn writeFile(dir: Dir, io: Io, options: WriteFileOptions) WriteFileError!void {
|
|
var file = try dir.createFile(io, options.sub_path, options.flags);
|
|
defer file.close(io);
|
|
try file.writeAll(io, options.data);
|
|
}
|
|
|
|
pub const PrevStatus = enum {
|
|
stale,
|
|
fresh,
|
|
};
|
|
|
|
pub const UpdateFileError = File.OpenError;
|
|
|
|
/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If
|
|
/// they are equal, does nothing. Otherwise, atomically copies `source_path` to
|
|
/// `dest_path`, creating the parent directory hierarchy as needed. The
|
|
/// destination file gains the mtime, atime, and mode of the source file so
|
|
/// that the next call to `updateFile` will not need a copy.
|
|
///
|
|
/// Returns the previous status of the file before updating.
|
|
///
|
|
/// * On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
|
|
/// * On WASI, both paths should be encoded as valid UTF-8.
|
|
/// * On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
|
|
pub fn updateFile(
|
|
source_dir: Dir,
|
|
io: Io,
|
|
source_path: []const u8,
|
|
dest_dir: Dir,
|
|
/// If directories in this path do not exist, they are created.
|
|
dest_path: []const u8,
|
|
options: std.fs.Dir.CopyFileOptions,
|
|
) !PrevStatus {
|
|
var src_file = try source_dir.openFile(io, source_path, .{});
|
|
defer src_file.close(io);
|
|
|
|
const src_stat = try src_file.stat(io);
|
|
const actual_mode = options.override_mode orelse src_stat.mode;
|
|
check_dest_stat: {
|
|
const dest_stat = blk: {
|
|
var dest_file = dest_dir.openFile(io, dest_path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound => break :check_dest_stat,
|
|
else => |e| return e,
|
|
};
|
|
defer dest_file.close(io);
|
|
|
|
break :blk try dest_file.stat(io);
|
|
};
|
|
|
|
if (src_stat.size == dest_stat.size and
|
|
src_stat.mtime.nanoseconds == dest_stat.mtime.nanoseconds and
|
|
actual_mode == dest_stat.mode)
|
|
{
|
|
return .fresh;
|
|
}
|
|
}
|
|
|
|
if (std.fs.path.dirname(dest_path)) |dirname| {
|
|
try dest_dir.makePath(io, dirname);
|
|
}
|
|
|
|
var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available.
|
|
var atomic_file = try std.fs.Dir.atomicFile(.adaptFromNewApi(dest_dir), dest_path, .{
|
|
.mode = actual_mode,
|
|
.write_buffer = &buffer,
|
|
});
|
|
defer atomic_file.deinit();
|
|
|
|
var src_reader: File.Reader = .initSize(src_file, io, &.{}, src_stat.size);
|
|
const dest_writer = &atomic_file.file_writer.interface;
|
|
|
|
_ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) {
|
|
error.ReadFailed => return src_reader.err.?,
|
|
error.WriteFailed => return atomic_file.file_writer.err.?,
|
|
};
|
|
try atomic_file.flush();
|
|
try atomic_file.file_writer.file.updateTimes(src_stat.atime, src_stat.mtime);
|
|
try atomic_file.renameIntoPlace();
|
|
return .stale;
|
|
}
|
|
|
|
pub const ReadFileError = File.OpenError || File.Reader.Error;
|
|
|
|
/// Read all of file contents using a preallocated buffer.
|
|
///
|
|
/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len`
|
|
/// the situation is ambiguous. It could either mean that the entire file was read, and
|
|
/// it exactly fits the buffer, or it could mean the buffer was not big enough for the
|
|
/// entire file.
|
|
///
|
|
/// * On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
|
|
/// * On WASI, `file_path` should be encoded as valid UTF-8.
|
|
/// * On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
|
|
pub fn readFile(dir: Dir, io: Io, file_path: []const u8, buffer: []u8) ReadFileError![]u8 {
|
|
var file = try dir.openFile(io, file_path, .{});
|
|
defer file.close(io);
|
|
|
|
var reader = file.reader(io, &.{});
|
|
const n = reader.interface.readSliceShort(buffer) catch |err| switch (err) {
|
|
error.ReadFailed => return reader.err.?,
|
|
};
|
|
|
|
return buffer[0..n];
|
|
}
|
|
|
|
pub const MakeError = error{
|
|
/// In WASI, this error may occur when the file descriptor does
|
|
/// not hold the required rights to create a new directory relative to it.
|
|
AccessDenied,
|
|
PermissionDenied,
|
|
DiskQuota,
|
|
PathAlreadyExists,
|
|
SymLinkLoop,
|
|
LinkQuotaExceeded,
|
|
FileNotFound,
|
|
SystemResources,
|
|
NoSpaceLeft,
|
|
NotDir,
|
|
ReadOnlyFileSystem,
|
|
NoDevice,
|
|
/// On Windows, `\\server` or `\\server\share` was not found.
|
|
NetworkNotFound,
|
|
} || PathNameError || Io.Cancelable || Io.UnexpectedError;
|
|
|
|
/// Creates a single directory with a relative or absolute path.
|
|
///
|
|
/// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
|
|
/// * 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.
|
|
///
|
|
/// Related:
|
|
/// * `makePath`
|
|
/// * `makeDirAbsolute`
|
|
pub fn makeDir(dir: Dir, io: Io, sub_path: []const u8) MakeError!void {
|
|
return io.vtable.dirMake(io.userdata, dir, sub_path, default_mode);
|
|
}
|
|
|
|
pub const MakePathError = MakeError || StatPathError;
|
|
|
|
/// Calls makeDir iteratively to make an entire path, creating any parent
|
|
/// directories that do not exist.
|
|
///
|
|
/// Returns success 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.
|
|
///
|
|
/// Fails on an empty path with `error.BadPathName` as that is not a path that
|
|
/// can be created.
|
|
///
|
|
/// On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
|
|
/// 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.
|
|
///
|
|
/// Paths containing `..` components are handled differently depending on the platform:
|
|
/// - On Windows, `..` are resolved before the path is passed to NtCreateFile, meaning
|
|
/// a `sub_path` like "first/../second" will resolve to "second" and only a
|
|
/// `./second` directory will be created.
|
|
/// - On other platforms, `..` are not resolved before the path is passed to `mkdirat`,
|
|
/// meaning a `sub_path` like "first/../second" will create both a `./first`
|
|
/// and a `./second` directory.
|
|
pub fn makePath(dir: Dir, io: Io, sub_path: []const u8) MakePathError!void {
|
|
_ = try makePathStatus(dir, io, sub_path);
|
|
}
|
|
|
|
pub const MakePathStatus = enum { existed, created };
|
|
|
|
/// Same as `makePath` except returns whether the path already existed or was
|
|
/// successfully created.
|
|
pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus {
|
|
var it = std.fs.path.componentIterator(sub_path);
|
|
var status: MakePathStatus = .existed;
|
|
var component = it.last() orelse return error.BadPathName;
|
|
while (true) {
|
|
if (makeDir(dir, io, component.path)) |_| {
|
|
status = .created;
|
|
} else |err| switch (err) {
|
|
error.PathAlreadyExists => {
|
|
// 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 = statPath(dir, io, component.path, .{}) catch |stat_err| switch (stat_err) {
|
|
error.IsDir => break :check_dir,
|
|
else => |e| return e,
|
|
};
|
|
if (fstat.kind != .directory) return error.NotDir;
|
|
}
|
|
},
|
|
error.FileNotFound => |e| {
|
|
component = it.previous() orelse return e;
|
|
continue;
|
|
},
|
|
else => |e| return e,
|
|
}
|
|
component = it.next() orelse return status;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
pub fn stat(dir: Dir, io: Io) StatError!Stat {
|
|
return io.vtable.dirStat(io.userdata, dir);
|
|
}
|
|
|
|
pub const StatPathError = File.OpenError || File.StatError;
|
|
|
|
pub const StatPathOptions = struct {
|
|
follow_symlinks: bool = true,
|
|
};
|
|
|
|
/// Returns metadata for a file inside the directory.
|
|
///
|
|
/// On Windows, this requires three syscalls. On other operating systems, it
|
|
/// only takes one.
|
|
///
|
|
/// Symlinks are followed.
|
|
///
|
|
/// `sub_path` may be absolute, in which case `self` is ignored.
|
|
///
|
|
/// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
|
|
/// * 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 statPath(dir: Dir, io: Io, sub_path: []const u8, options: StatPathOptions) StatPathError!Stat {
|
|
return io.vtable.dirStatPath(io.userdata, dir, sub_path, options);
|
|
}
|