mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +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
2274 lines
86 KiB
Zig
2274 lines
86 KiB
Zig
const builtin = @import("builtin");
|
|
const native_os = builtin.os.tag;
|
|
|
|
const std = @import("../std.zig");
|
|
const Io = std.Io;
|
|
const testing = std.testing;
|
|
const fs = std.fs;
|
|
const mem = std.mem;
|
|
const wasi = std.os.wasi;
|
|
const windows = std.os.windows;
|
|
const posix = std.posix;
|
|
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
const Dir = std.fs.Dir;
|
|
const File = std.fs.File;
|
|
const tmpDir = testing.tmpDir;
|
|
const SymLinkFlags = std.fs.Dir.SymLinkFlags;
|
|
|
|
const PathType = enum {
|
|
relative,
|
|
absolute,
|
|
unc,
|
|
|
|
pub fn isSupported(self: PathType, target_os: std.Target.Os) bool {
|
|
return switch (self) {
|
|
.relative => true,
|
|
.absolute => std.os.isGetFdPathSupportedOnTarget(target_os),
|
|
.unc => target_os.tag == .windows,
|
|
};
|
|
}
|
|
|
|
pub const TransformError = posix.RealPathError || error{OutOfMemory};
|
|
pub const TransformFn = fn (allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8;
|
|
|
|
pub fn getTransformFn(comptime path_type: PathType) TransformFn {
|
|
switch (path_type) {
|
|
.relative => return struct {
|
|
fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
|
|
_ = allocator;
|
|
_ = dir;
|
|
return relative_path;
|
|
}
|
|
}.transform,
|
|
.absolute => return struct {
|
|
fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
|
|
// The final path may not actually exist which would cause realpath to fail.
|
|
// So instead, we get the path of the dir and join it with the relative path.
|
|
var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
|
|
const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);
|
|
return fs.path.joinZ(allocator, &.{ dir_path, relative_path });
|
|
}
|
|
}.transform,
|
|
.unc => return struct {
|
|
fn transform(allocator: mem.Allocator, dir: Dir, relative_path: [:0]const u8) TransformError![:0]const u8 {
|
|
// Any drive absolute path (C:\foo) can be converted into a UNC path by
|
|
// using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
|
|
var fd_path_buf: [fs.max_path_bytes]u8 = undefined;
|
|
const dir_path = try std.os.getFdPath(dir.fd, &fd_path_buf);
|
|
const windows_path_type = windows.getWin32PathType(u8, dir_path);
|
|
switch (windows_path_type) {
|
|
.unc_absolute => return fs.path.joinZ(allocator, &.{ dir_path, relative_path }),
|
|
.drive_absolute => {
|
|
// `C:\<...>` -> `\\127.0.0.1\C$\<...>`
|
|
const prepended = "\\\\127.0.0.1\\";
|
|
var path = try fs.path.joinZ(allocator, &.{ prepended, dir_path, relative_path });
|
|
path[prepended.len + 1] = '$';
|
|
return path;
|
|
},
|
|
else => unreachable,
|
|
}
|
|
}
|
|
}.transform,
|
|
}
|
|
}
|
|
};
|
|
|
|
const TestContext = struct {
|
|
io: Io,
|
|
path_type: PathType,
|
|
path_sep: u8,
|
|
arena: ArenaAllocator,
|
|
tmp: testing.TmpDir,
|
|
dir: std.fs.Dir,
|
|
transform_fn: *const PathType.TransformFn,
|
|
|
|
pub fn init(path_type: PathType, path_sep: u8, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext {
|
|
const tmp = tmpDir(.{ .iterate = true });
|
|
return .{
|
|
.io = testing.io,
|
|
.path_type = path_type,
|
|
.path_sep = path_sep,
|
|
.arena = ArenaAllocator.init(allocator),
|
|
.tmp = tmp,
|
|
.dir = tmp.dir,
|
|
.transform_fn = transform_fn,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *TestContext) void {
|
|
self.arena.deinit();
|
|
self.tmp.cleanup();
|
|
}
|
|
|
|
/// Returns the `relative_path` transformed into the TestContext's `path_type`,
|
|
/// with any supported path separators replaced by `path_sep`.
|
|
/// The result is allocated by the TestContext's arena and will be free'd during
|
|
/// `TestContext.deinit`.
|
|
pub fn transformPath(self: *TestContext, relative_path: [:0]const u8) ![:0]const u8 {
|
|
const allocator = self.arena.allocator();
|
|
const transformed_path = try self.transform_fn(allocator, self.dir, relative_path);
|
|
if (native_os == .windows) {
|
|
const transformed_sep_path = try allocator.dupeZ(u8, transformed_path);
|
|
std.mem.replaceScalar(u8, transformed_sep_path, switch (self.path_sep) {
|
|
'/' => '\\',
|
|
'\\' => '/',
|
|
else => unreachable,
|
|
}, self.path_sep);
|
|
return transformed_sep_path;
|
|
}
|
|
return transformed_path;
|
|
}
|
|
|
|
/// Replaces any path separators with the canonical path separator for the platform
|
|
/// (e.g. all path separators are converted to `\` on Windows).
|
|
/// If path separators are replaced, then the result is allocated by the
|
|
/// TestContext's arena and will be free'd during `TestContext.deinit`.
|
|
pub fn toCanonicalPathSep(self: *TestContext, path: [:0]const u8) ![:0]const u8 {
|
|
if (native_os == .windows) {
|
|
const allocator = self.arena.allocator();
|
|
const transformed_sep_path = try allocator.dupeZ(u8, path);
|
|
std.mem.replaceScalar(u8, transformed_sep_path, '/', '\\');
|
|
return transformed_sep_path;
|
|
}
|
|
return path;
|
|
}
|
|
};
|
|
|
|
/// `test_func` must be a function that takes a `*TestContext` as a parameter and returns `!void`.
|
|
/// `test_func` will be called once for each PathType that the current target supports,
|
|
/// and will be passed a TestContext that can transform a relative path into the path type under test.
|
|
/// The TestContext will also create a tmp directory for you (and will clean it up for you too).
|
|
fn testWithAllSupportedPathTypes(test_func: anytype) !void {
|
|
try testWithPathTypeIfSupported(.relative, '/', test_func);
|
|
try testWithPathTypeIfSupported(.absolute, '/', test_func);
|
|
try testWithPathTypeIfSupported(.unc, '/', test_func);
|
|
try testWithPathTypeIfSupported(.relative, '\\', test_func);
|
|
try testWithPathTypeIfSupported(.absolute, '\\', test_func);
|
|
try testWithPathTypeIfSupported(.unc, '\\', test_func);
|
|
}
|
|
|
|
fn testWithPathTypeIfSupported(comptime path_type: PathType, comptime path_sep: u8, test_func: anytype) !void {
|
|
if (!(comptime path_type.isSupported(builtin.os))) return;
|
|
if (!(comptime fs.path.isSep(path_sep))) return;
|
|
|
|
var ctx = TestContext.init(path_type, path_sep, testing.allocator, path_type.getTransformFn());
|
|
defer ctx.deinit();
|
|
|
|
try test_func(&ctx);
|
|
}
|
|
|
|
// For use in test setup. If the symlink creation fails on Windows with
|
|
// AccessDenied, then make the test failure silent (it is not a Zig failure).
|
|
fn setupSymlink(dir: Dir, target: []const u8, link: []const u8, flags: SymLinkFlags) !void {
|
|
return dir.symLink(target, link, flags) catch |err| switch (err) {
|
|
// Symlink requires admin privileges on windows, so this test can legitimately fail.
|
|
error.AccessDenied => if (native_os == .windows) return error.SkipZigTest else return err,
|
|
else => return err,
|
|
};
|
|
}
|
|
|
|
// For use in test setup. If the symlink creation fails on Windows with
|
|
// AccessDenied, then make the test failure silent (it is not a Zig failure).
|
|
fn setupSymlinkAbsolute(target: []const u8, link: []const u8, flags: SymLinkFlags) !void {
|
|
return fs.symLinkAbsolute(target, link, flags) catch |err| switch (err) {
|
|
error.AccessDenied => if (native_os == .windows) return error.SkipZigTest else return err,
|
|
else => return err,
|
|
};
|
|
}
|
|
|
|
test "Dir.readLink" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
// Create some targets
|
|
const file_target_path = try ctx.transformPath("file.txt");
|
|
try ctx.dir.writeFile(.{ .sub_path = file_target_path, .data = "nonsense" });
|
|
const dir_target_path = try ctx.transformPath("subdir");
|
|
try ctx.dir.makeDir(dir_target_path);
|
|
|
|
// On Windows, symlink targets always use the canonical path separator
|
|
const canonical_file_target_path = try ctx.toCanonicalPathSep(file_target_path);
|
|
const canonical_dir_target_path = try ctx.toCanonicalPathSep(dir_target_path);
|
|
|
|
// test 1: symlink to a file
|
|
try setupSymlink(ctx.dir, file_target_path, "symlink1", .{});
|
|
try testReadLink(ctx.dir, canonical_file_target_path, "symlink1");
|
|
if (builtin.os.tag == .windows) {
|
|
try testReadLinkW(testing.allocator, ctx.dir, canonical_file_target_path, "symlink1");
|
|
}
|
|
|
|
// test 2: symlink to a directory (can be different on Windows)
|
|
try setupSymlink(ctx.dir, dir_target_path, "symlink2", .{ .is_directory = true });
|
|
try testReadLink(ctx.dir, canonical_dir_target_path, "symlink2");
|
|
if (builtin.os.tag == .windows) {
|
|
try testReadLinkW(testing.allocator, ctx.dir, canonical_dir_target_path, "symlink2");
|
|
}
|
|
|
|
// test 3: relative path symlink
|
|
const parent_file = ".." ++ fs.path.sep_str ++ "target.txt";
|
|
const canonical_parent_file = try ctx.toCanonicalPathSep(parent_file);
|
|
var subdir = try ctx.dir.makeOpenPath("subdir", .{});
|
|
defer subdir.close();
|
|
try setupSymlink(subdir, canonical_parent_file, "relative-link.txt", .{});
|
|
try testReadLink(subdir, canonical_parent_file, "relative-link.txt");
|
|
if (builtin.os.tag == .windows) {
|
|
try testReadLinkW(testing.allocator, subdir, canonical_parent_file, "relative-link.txt");
|
|
}
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
|
|
var buffer: [fs.max_path_bytes]u8 = undefined;
|
|
const actual = try dir.readLink(symlink_path, buffer[0..]);
|
|
try testing.expectEqualStrings(target_path, actual);
|
|
}
|
|
|
|
fn testReadLinkW(allocator: mem.Allocator, dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
|
|
const target_path_w = try std.unicode.wtf8ToWtf16LeAlloc(allocator, target_path);
|
|
defer allocator.free(target_path_w);
|
|
// Calling the W functions directly requires the path to be NT-prefixed
|
|
const symlink_path_w = try std.os.windows.sliceToPrefixedFileW(dir.fd, symlink_path);
|
|
const wtf16_buffer = try allocator.alloc(u16, target_path_w.len);
|
|
defer allocator.free(wtf16_buffer);
|
|
const actual = try dir.readLinkW(symlink_path_w.span(), wtf16_buffer);
|
|
try testing.expectEqualSlices(u16, target_path_w, actual);
|
|
}
|
|
|
|
fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {
|
|
var buffer: [fs.max_path_bytes]u8 = undefined;
|
|
const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);
|
|
try testing.expectEqualStrings(target_path, given);
|
|
}
|
|
|
|
test "File.stat on a File that is a symlink returns Kind.sym_link" {
|
|
// This test requires getting a file descriptor of a symlink which
|
|
// is not possible on all targets
|
|
switch (builtin.target.os.tag) {
|
|
.windows, .linux => {},
|
|
else => return error.SkipZigTest,
|
|
}
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const dir_target_path = try ctx.transformPath("subdir");
|
|
try ctx.dir.makeDir(dir_target_path);
|
|
|
|
try setupSymlink(ctx.dir, dir_target_path, "symlink", .{ .is_directory = true });
|
|
|
|
var symlink = switch (builtin.target.os.tag) {
|
|
.windows => windows_symlink: {
|
|
const sub_path_w = try windows.cStrToPrefixedFileW(ctx.dir.fd, "symlink");
|
|
|
|
var result = Dir{
|
|
.fd = undefined,
|
|
};
|
|
|
|
const path_len_bytes = @as(u16, @intCast(sub_path_w.span().len * 2));
|
|
var nt_name = windows.UNICODE_STRING{
|
|
.Length = path_len_bytes,
|
|
.MaximumLength = path_len_bytes,
|
|
.Buffer = @constCast(&sub_path_w.data),
|
|
};
|
|
var attr = windows.OBJECT_ATTRIBUTES{
|
|
.Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
|
|
.RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else ctx.dir.fd,
|
|
.Attributes = 0,
|
|
.ObjectName = &nt_name,
|
|
.SecurityDescriptor = null,
|
|
.SecurityQualityOfService = null,
|
|
};
|
|
var io: windows.IO_STATUS_BLOCK = undefined;
|
|
const rc = windows.ntdll.NtCreateFile(
|
|
&result.fd,
|
|
windows.STANDARD_RIGHTS_READ | windows.FILE_READ_ATTRIBUTES | windows.FILE_READ_EA | windows.SYNCHRONIZE | windows.FILE_TRAVERSE,
|
|
&attr,
|
|
&io,
|
|
null,
|
|
windows.FILE_ATTRIBUTE_NORMAL,
|
|
windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE,
|
|
windows.FILE_OPEN,
|
|
// FILE_OPEN_REPARSE_POINT is the important thing here
|
|
windows.FILE_OPEN_REPARSE_POINT | windows.FILE_DIRECTORY_FILE | windows.FILE_SYNCHRONOUS_IO_NONALERT | windows.FILE_OPEN_FOR_BACKUP_INTENT,
|
|
null,
|
|
0,
|
|
);
|
|
|
|
switch (rc) {
|
|
.SUCCESS => break :windows_symlink result,
|
|
else => return windows.unexpectedStatus(rc),
|
|
}
|
|
},
|
|
.linux => linux_symlink: {
|
|
const sub_path_c = try posix.toPosixPath("symlink");
|
|
// the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink
|
|
// note that if O_DIRECTORY is set, then this will error with ENOTDIR
|
|
const flags: posix.O = .{
|
|
.NOFOLLOW = true,
|
|
.PATH = true,
|
|
.ACCMODE = .RDONLY,
|
|
.CLOEXEC = true,
|
|
};
|
|
const fd = try posix.openatZ(ctx.dir.fd, &sub_path_c, flags, 0);
|
|
break :linux_symlink Dir{ .fd = fd };
|
|
},
|
|
else => unreachable,
|
|
};
|
|
defer symlink.close();
|
|
|
|
const stat = try symlink.stat();
|
|
try testing.expectEqual(File.Kind.sym_link, stat.kind);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "openDir" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const allocator = ctx.arena.allocator();
|
|
const subdir_path = try ctx.transformPath("subdir");
|
|
try ctx.dir.makeDir(subdir_path);
|
|
|
|
for ([_][]const u8{ "", ".", ".." }) |sub_path| {
|
|
const dir_path = try fs.path.join(allocator, &.{ subdir_path, sub_path });
|
|
var dir = try ctx.dir.openDir(dir_path, .{});
|
|
defer dir.close();
|
|
}
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "accessAbsolute" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const base_path = try tmp.dir.realpathAlloc(testing.allocator, ".");
|
|
defer testing.allocator.free(base_path);
|
|
|
|
try fs.accessAbsolute(base_path, .{});
|
|
}
|
|
|
|
test "openDirAbsolute" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const tmp_ino = (try tmp.dir.stat()).inode;
|
|
|
|
try tmp.dir.makeDir("subdir");
|
|
const sub_path = try tmp.dir.realpathAlloc(testing.allocator, "subdir");
|
|
defer testing.allocator.free(sub_path);
|
|
|
|
// Can open sub_path
|
|
var tmp_sub = try fs.openDirAbsolute(sub_path, .{});
|
|
defer tmp_sub.close();
|
|
|
|
const sub_ino = (try tmp_sub.stat()).inode;
|
|
|
|
{
|
|
// Can open sub_path + ".."
|
|
const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, ".." });
|
|
defer testing.allocator.free(dir_path);
|
|
|
|
var dir = try fs.openDirAbsolute(dir_path, .{});
|
|
defer dir.close();
|
|
|
|
const ino = (try dir.stat()).inode;
|
|
try testing.expectEqual(tmp_ino, ino);
|
|
}
|
|
|
|
{
|
|
// Can open sub_path + "."
|
|
const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, "." });
|
|
defer testing.allocator.free(dir_path);
|
|
|
|
var dir = try fs.openDirAbsolute(dir_path, .{});
|
|
defer dir.close();
|
|
|
|
const ino = (try dir.stat()).inode;
|
|
try testing.expectEqual(sub_ino, ino);
|
|
}
|
|
|
|
{
|
|
// Can open subdir + "..", with some extra "."
|
|
const dir_path = try fs.path.join(testing.allocator, &.{ sub_path, ".", "..", "." });
|
|
defer testing.allocator.free(dir_path);
|
|
|
|
var dir = try fs.openDirAbsolute(dir_path, .{});
|
|
defer dir.close();
|
|
|
|
const ino = (try dir.stat()).inode;
|
|
try testing.expectEqual(tmp_ino, ino);
|
|
}
|
|
}
|
|
|
|
test "openDir cwd parent '..'" {
|
|
var dir = fs.cwd().openDir("..", .{}) catch |err| {
|
|
if (native_os == .wasi and err == error.PermissionDenied) {
|
|
return; // This is okay. WASI disallows escaping from the fs sandbox
|
|
}
|
|
return err;
|
|
};
|
|
defer dir.close();
|
|
}
|
|
|
|
test "openDir non-cwd parent '..'" {
|
|
switch (native_os) {
|
|
.wasi, .netbsd, .openbsd => return error.SkipZigTest,
|
|
else => {},
|
|
}
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
var subdir = try tmp.dir.makeOpenPath("subdir", .{});
|
|
defer subdir.close();
|
|
|
|
var dir = try subdir.openDir("..", .{});
|
|
defer dir.close();
|
|
|
|
const expected_path = try tmp.dir.realpathAlloc(testing.allocator, ".");
|
|
defer testing.allocator.free(expected_path);
|
|
|
|
const actual_path = try dir.realpathAlloc(testing.allocator, ".");
|
|
defer testing.allocator.free(actual_path);
|
|
|
|
try testing.expectEqualStrings(expected_path, actual_path);
|
|
}
|
|
|
|
test "readLinkAbsolute" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
// Create some targets
|
|
try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = "nonsense" });
|
|
try tmp.dir.makeDir("subdir");
|
|
|
|
// Get base abs path
|
|
var arena = ArenaAllocator.init(testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
const base_path = try tmp.dir.realpathAlloc(allocator, ".");
|
|
|
|
{
|
|
const target_path = try fs.path.join(allocator, &.{ base_path, "file.txt" });
|
|
const symlink_path = try fs.path.join(allocator, &.{ base_path, "symlink1" });
|
|
|
|
// Create symbolic link by path
|
|
try setupSymlinkAbsolute(target_path, symlink_path, .{});
|
|
try testReadLinkAbsolute(target_path, symlink_path);
|
|
}
|
|
{
|
|
const target_path = try fs.path.join(allocator, &.{ base_path, "subdir" });
|
|
const symlink_path = try fs.path.join(allocator, &.{ base_path, "symlink2" });
|
|
|
|
// Create symbolic link to a directory by path
|
|
try setupSymlinkAbsolute(target_path, symlink_path, .{ .is_directory = true });
|
|
try testReadLinkAbsolute(target_path, symlink_path);
|
|
}
|
|
}
|
|
|
|
test "Dir.Iterator" {
|
|
var tmp_dir = tmpDir(.{ .iterate = true });
|
|
defer tmp_dir.cleanup();
|
|
|
|
// First, create a couple of entries to iterate over.
|
|
const file = try tmp_dir.dir.createFile("some_file", .{});
|
|
file.close();
|
|
|
|
try tmp_dir.dir.makeDir("some_dir");
|
|
|
|
var arena = ArenaAllocator.init(testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var entries = std.array_list.Managed(Dir.Entry).init(allocator);
|
|
|
|
// Create iterator.
|
|
var iter = tmp_dir.dir.iterate();
|
|
while (try iter.next()) |entry| {
|
|
// We cannot just store `entry` as on Windows, we're re-using the name buffer
|
|
// which means we'll actually share the `name` pointer between entries!
|
|
const name = try allocator.dupe(u8, entry.name);
|
|
try entries.append(Dir.Entry{ .name = name, .kind = entry.kind });
|
|
}
|
|
|
|
try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
|
|
try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
|
|
try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
|
|
}
|
|
|
|
test "Dir.Iterator many entries" {
|
|
var tmp_dir = tmpDir(.{ .iterate = true });
|
|
defer tmp_dir.cleanup();
|
|
|
|
const num = 1024;
|
|
var i: usize = 0;
|
|
var buf: [4]u8 = undefined; // Enough to store "1024".
|
|
while (i < num) : (i += 1) {
|
|
const name = try std.fmt.bufPrint(&buf, "{}", .{i});
|
|
const file = try tmp_dir.dir.createFile(name, .{});
|
|
file.close();
|
|
}
|
|
|
|
var arena = ArenaAllocator.init(testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var entries = std.array_list.Managed(Dir.Entry).init(allocator);
|
|
|
|
// Create iterator.
|
|
var iter = tmp_dir.dir.iterate();
|
|
while (try iter.next()) |entry| {
|
|
// We cannot just store `entry` as on Windows, we're re-using the name buffer
|
|
// which means we'll actually share the `name` pointer between entries!
|
|
const name = try allocator.dupe(u8, entry.name);
|
|
try entries.append(.{ .name = name, .kind = entry.kind });
|
|
}
|
|
|
|
i = 0;
|
|
while (i < num) : (i += 1) {
|
|
const name = try std.fmt.bufPrint(&buf, "{}", .{i});
|
|
try testing.expect(contains(&entries, .{ .name = name, .kind = .file }));
|
|
}
|
|
}
|
|
|
|
test "Dir.Iterator twice" {
|
|
var tmp_dir = tmpDir(.{ .iterate = true });
|
|
defer tmp_dir.cleanup();
|
|
|
|
// First, create a couple of entries to iterate over.
|
|
const file = try tmp_dir.dir.createFile("some_file", .{});
|
|
file.close();
|
|
|
|
try tmp_dir.dir.makeDir("some_dir");
|
|
|
|
var arena = ArenaAllocator.init(testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var i: u8 = 0;
|
|
while (i < 2) : (i += 1) {
|
|
var entries = std.array_list.Managed(Dir.Entry).init(allocator);
|
|
|
|
// Create iterator.
|
|
var iter = tmp_dir.dir.iterate();
|
|
while (try iter.next()) |entry| {
|
|
// We cannot just store `entry` as on Windows, we're re-using the name buffer
|
|
// which means we'll actually share the `name` pointer between entries!
|
|
const name = try allocator.dupe(u8, entry.name);
|
|
try entries.append(Dir.Entry{ .name = name, .kind = entry.kind });
|
|
}
|
|
|
|
try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
|
|
try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
|
|
try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
|
|
}
|
|
}
|
|
|
|
test "Dir.Iterator reset" {
|
|
var tmp_dir = tmpDir(.{ .iterate = true });
|
|
defer tmp_dir.cleanup();
|
|
|
|
// First, create a couple of entries to iterate over.
|
|
const file = try tmp_dir.dir.createFile("some_file", .{});
|
|
file.close();
|
|
|
|
try tmp_dir.dir.makeDir("some_dir");
|
|
|
|
var arena = ArenaAllocator.init(testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
// Create iterator.
|
|
var iter = tmp_dir.dir.iterate();
|
|
|
|
var i: u8 = 0;
|
|
while (i < 2) : (i += 1) {
|
|
var entries = std.array_list.Managed(Dir.Entry).init(allocator);
|
|
|
|
while (try iter.next()) |entry| {
|
|
// We cannot just store `entry` as on Windows, we're re-using the name buffer
|
|
// which means we'll actually share the `name` pointer between entries!
|
|
const name = try allocator.dupe(u8, entry.name);
|
|
try entries.append(.{ .name = name, .kind = entry.kind });
|
|
}
|
|
|
|
try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
|
|
try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
|
|
try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
|
|
|
|
iter.reset();
|
|
}
|
|
}
|
|
|
|
test "Dir.Iterator but dir is deleted during iteration" {
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
// Create directory and setup an iterator for it
|
|
var subdir = try tmp.dir.makeOpenPath("subdir", .{ .iterate = true });
|
|
defer subdir.close();
|
|
|
|
var iterator = subdir.iterate();
|
|
|
|
// Create something to iterate over within the subdir
|
|
try tmp.dir.makePath("subdir" ++ fs.path.sep_str ++ "b");
|
|
|
|
// Then, before iterating, delete the directory that we're iterating.
|
|
// This is a contrived reproduction, but this could happen outside of the program, in another thread, etc.
|
|
// If we get an error while trying to delete, we can skip this test (this will happen on platforms
|
|
// like Windows which will give FileBusy if the directory is currently open for iteration).
|
|
tmp.dir.deleteTree("subdir") catch return error.SkipZigTest;
|
|
|
|
// Now, when we try to iterate, the next call should return null immediately.
|
|
const entry = try iterator.next();
|
|
try std.testing.expect(entry == null);
|
|
|
|
// On Linux, we can opt-in to receiving a more specific error by calling `nextLinux`
|
|
if (native_os == .linux) {
|
|
try std.testing.expectError(error.DirNotFound, iterator.nextLinux());
|
|
}
|
|
}
|
|
|
|
fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool {
|
|
return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind;
|
|
}
|
|
|
|
fn contains(entries: *const std.array_list.Managed(Dir.Entry), el: Dir.Entry) bool {
|
|
for (entries.items) |entry| {
|
|
if (entryEql(entry, el)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
test "Dir.realpath smoke test" {
|
|
if (!comptime std.os.isGetFdPathSupportedOnTarget(builtin.os)) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const allocator = ctx.arena.allocator();
|
|
const test_file_path = try ctx.transformPath("test_file");
|
|
const test_dir_path = try ctx.transformPath("test_dir");
|
|
var buf: [fs.max_path_bytes]u8 = undefined;
|
|
|
|
// FileNotFound if the path doesn't exist
|
|
try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(allocator, test_file_path));
|
|
try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_file_path, &buf));
|
|
try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(allocator, test_dir_path));
|
|
try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_dir_path, &buf));
|
|
|
|
// Now create the file and dir
|
|
try ctx.dir.writeFile(.{ .sub_path = test_file_path, .data = "" });
|
|
try ctx.dir.makeDir(test_dir_path);
|
|
|
|
const base_path = try ctx.transformPath(".");
|
|
const base_realpath = try ctx.dir.realpathAlloc(allocator, base_path);
|
|
const expected_file_path = try fs.path.join(
|
|
allocator,
|
|
&.{ base_realpath, "test_file" },
|
|
);
|
|
const expected_dir_path = try fs.path.join(
|
|
allocator,
|
|
&.{ base_realpath, "test_dir" },
|
|
);
|
|
|
|
// First, test non-alloc version
|
|
{
|
|
const file_path = try ctx.dir.realpath(test_file_path, &buf);
|
|
try testing.expectEqualStrings(expected_file_path, file_path);
|
|
|
|
const dir_path = try ctx.dir.realpath(test_dir_path, &buf);
|
|
try testing.expectEqualStrings(expected_dir_path, dir_path);
|
|
}
|
|
|
|
// Next, test alloc version
|
|
{
|
|
const file_path = try ctx.dir.realpathAlloc(allocator, test_file_path);
|
|
try testing.expectEqualStrings(expected_file_path, file_path);
|
|
|
|
const dir_path = try ctx.dir.realpathAlloc(allocator, test_dir_path);
|
|
try testing.expectEqualStrings(expected_dir_path, dir_path);
|
|
}
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "readFileAlloc" {
|
|
var tmp_dir = tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
var file = try tmp_dir.dir.createFile("test_file", .{ .read = true });
|
|
defer file.close();
|
|
|
|
const buf1 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(1024));
|
|
defer testing.allocator.free(buf1);
|
|
try testing.expectEqualStrings("", buf1);
|
|
|
|
const write_buf: []const u8 = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";
|
|
try file.writeAll(write_buf);
|
|
|
|
{
|
|
// max_bytes > file_size
|
|
const buf2 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(1024));
|
|
defer testing.allocator.free(buf2);
|
|
try testing.expectEqualStrings(write_buf, buf2);
|
|
}
|
|
|
|
{
|
|
// max_bytes == file_size
|
|
try testing.expectError(
|
|
error.StreamTooLong,
|
|
tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len)),
|
|
);
|
|
}
|
|
|
|
{
|
|
// max_bytes == file_size + 1
|
|
const buf2 = try tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len + 1));
|
|
defer testing.allocator.free(buf2);
|
|
try testing.expectEqualStrings(write_buf, buf2);
|
|
}
|
|
|
|
// max_bytes < file_size
|
|
try testing.expectError(
|
|
error.StreamTooLong,
|
|
tmp_dir.dir.readFileAlloc("test_file", testing.allocator, .limited(write_buf.len - 1)),
|
|
);
|
|
}
|
|
|
|
test "Dir.statFile" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const test_file_name = try ctx.transformPath("test_file");
|
|
|
|
try testing.expectError(error.FileNotFound, ctx.dir.statFile(test_file_name));
|
|
|
|
try ctx.dir.writeFile(.{ .sub_path = test_file_name, .data = "" });
|
|
|
|
const stat = try ctx.dir.statFile(test_file_name);
|
|
try testing.expectEqual(File.Kind.file, stat.kind);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "statFile on dangling symlink" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const symlink_name = try ctx.transformPath("dangling-symlink");
|
|
const symlink_target = "." ++ fs.path.sep_str ++ "doesnotexist";
|
|
|
|
try setupSymlink(ctx.dir, symlink_target, symlink_name, .{});
|
|
|
|
try std.testing.expectError(error.FileNotFound, ctx.dir.statFile(symlink_name));
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "directory operations on files" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const test_file_name = try ctx.transformPath("test_file");
|
|
|
|
var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
|
|
file.close();
|
|
|
|
try testing.expectError(error.PathAlreadyExists, ctx.dir.makeDir(test_file_name));
|
|
try testing.expectError(error.NotDir, ctx.dir.openDir(test_file_name, .{}));
|
|
try testing.expectError(error.NotDir, ctx.dir.deleteDir(test_file_name));
|
|
|
|
if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
|
|
try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(test_file_name));
|
|
try testing.expectError(error.NotDir, fs.deleteDirAbsolute(test_file_name));
|
|
}
|
|
|
|
// ensure the file still exists and is a file as a sanity check
|
|
file = try ctx.dir.openFile(test_file_name, .{});
|
|
const stat = try file.stat();
|
|
try testing.expectEqual(File.Kind.file, stat.kind);
|
|
file.close();
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "file operations on directories" {
|
|
// TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759
|
|
if (native_os == .freebsd) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const test_dir_name = try ctx.transformPath("test_dir");
|
|
|
|
try ctx.dir.makeDir(test_dir_name);
|
|
|
|
try testing.expectError(error.IsDir, ctx.dir.createFile(test_dir_name, .{}));
|
|
try testing.expectError(error.IsDir, ctx.dir.deleteFile(test_dir_name));
|
|
switch (native_os) {
|
|
.dragonfly, .netbsd => {
|
|
// no error when reading a directory. See https://github.com/ziglang/zig/issues/5732
|
|
const buf = try ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited);
|
|
testing.allocator.free(buf);
|
|
},
|
|
.wasi => {
|
|
// WASI return EBADF, which gets mapped to NotOpenForReading.
|
|
// See https://github.com/bytecodealliance/wasmtime/issues/1935
|
|
try testing.expectError(error.NotOpenForReading, ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited));
|
|
},
|
|
else => {
|
|
try testing.expectError(error.IsDir, ctx.dir.readFileAlloc(test_dir_name, testing.allocator, .unlimited));
|
|
},
|
|
}
|
|
|
|
if (native_os == .wasi and builtin.link_libc) {
|
|
// wasmtime unexpectedly succeeds here, see https://github.com/ziglang/zig/issues/20747
|
|
const handle = try ctx.dir.openFile(test_dir_name, .{ .mode = .read_write });
|
|
handle.close();
|
|
} else {
|
|
// Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.
|
|
// TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732
|
|
try testing.expectError(error.IsDir, ctx.dir.openFile(test_dir_name, .{ .mode = .read_write }));
|
|
}
|
|
|
|
if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
|
|
try testing.expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{}));
|
|
try testing.expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name));
|
|
}
|
|
|
|
// ensure the directory still exists as a sanity check
|
|
var dir = try ctx.dir.openDir(test_dir_name, .{});
|
|
dir.close();
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "makeOpenPath parent dirs do not exist" {
|
|
var tmp_dir = tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
var dir = try tmp_dir.dir.makeOpenPath("root_dir/parent_dir/some_dir", .{});
|
|
dir.close();
|
|
|
|
// double check that the full directory structure was created
|
|
var dir_verification = try tmp_dir.dir.openDir("root_dir/parent_dir/some_dir", .{});
|
|
dir_verification.close();
|
|
}
|
|
|
|
test "deleteDir" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const test_dir_path = try ctx.transformPath("test_dir");
|
|
const test_file_path = try ctx.transformPath("test_dir" ++ fs.path.sep_str ++ "test_file");
|
|
|
|
// deleting a non-existent directory
|
|
try testing.expectError(error.FileNotFound, ctx.dir.deleteDir(test_dir_path));
|
|
|
|
// deleting a non-empty directory
|
|
try ctx.dir.makeDir(test_dir_path);
|
|
try ctx.dir.writeFile(.{ .sub_path = test_file_path, .data = "" });
|
|
try testing.expectError(error.DirNotEmpty, ctx.dir.deleteDir(test_dir_path));
|
|
|
|
// deleting an empty directory
|
|
try ctx.dir.deleteFile(test_file_path);
|
|
try ctx.dir.deleteDir(test_dir_path);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "Dir.rename files" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
// Rename on Windows can hit intermittent AccessDenied errors
|
|
// when certain conditions are true about the host system.
|
|
// For now, skip this test when the path type is UNC to avoid them.
|
|
// See https://github.com/ziglang/zig/issues/17134
|
|
if (ctx.path_type == .unc) return;
|
|
|
|
const missing_file_path = try ctx.transformPath("missing_file_name");
|
|
const something_else_path = try ctx.transformPath("something_else");
|
|
|
|
try testing.expectError(error.FileNotFound, ctx.dir.rename(missing_file_path, something_else_path));
|
|
|
|
// Renaming files
|
|
const test_file_name = try ctx.transformPath("test_file");
|
|
const renamed_test_file_name = try ctx.transformPath("test_file_renamed");
|
|
var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
|
|
file.close();
|
|
try ctx.dir.rename(test_file_name, renamed_test_file_name);
|
|
|
|
// Ensure the file was renamed
|
|
try testing.expectError(error.FileNotFound, ctx.dir.openFile(test_file_name, .{}));
|
|
file = try ctx.dir.openFile(renamed_test_file_name, .{});
|
|
file.close();
|
|
|
|
// Rename to self succeeds
|
|
try ctx.dir.rename(renamed_test_file_name, renamed_test_file_name);
|
|
|
|
// Rename to existing file succeeds
|
|
const existing_file_path = try ctx.transformPath("existing_file");
|
|
var existing_file = try ctx.dir.createFile(existing_file_path, .{ .read = true });
|
|
existing_file.close();
|
|
try ctx.dir.rename(renamed_test_file_name, existing_file_path);
|
|
|
|
try testing.expectError(error.FileNotFound, ctx.dir.openFile(renamed_test_file_name, .{}));
|
|
file = try ctx.dir.openFile(existing_file_path, .{});
|
|
file.close();
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "Dir.rename directories" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
// Rename on Windows can hit intermittent AccessDenied errors
|
|
// when certain conditions are true about the host system.
|
|
// For now, skip this test when the path type is UNC to avoid them.
|
|
// See https://github.com/ziglang/zig/issues/17134
|
|
if (ctx.path_type == .unc) return;
|
|
|
|
const test_dir_path = try ctx.transformPath("test_dir");
|
|
const test_dir_renamed_path = try ctx.transformPath("test_dir_renamed");
|
|
|
|
// Renaming directories
|
|
try ctx.dir.makeDir(test_dir_path);
|
|
try ctx.dir.rename(test_dir_path, test_dir_renamed_path);
|
|
|
|
// Ensure the directory was renamed
|
|
try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
|
|
var dir = try ctx.dir.openDir(test_dir_renamed_path, .{});
|
|
|
|
// Put a file in the directory
|
|
var file = try dir.createFile("test_file", .{ .read = true });
|
|
file.close();
|
|
dir.close();
|
|
|
|
const test_dir_renamed_again_path = try ctx.transformPath("test_dir_renamed_again");
|
|
try ctx.dir.rename(test_dir_renamed_path, test_dir_renamed_again_path);
|
|
|
|
// Ensure the directory was renamed and the file still exists in it
|
|
try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_renamed_path, .{}));
|
|
dir = try ctx.dir.openDir(test_dir_renamed_again_path, .{});
|
|
file = try dir.openFile("test_file", .{});
|
|
file.close();
|
|
dir.close();
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "Dir.rename directory onto empty dir" {
|
|
// TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
|
|
if (native_os == .windows) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const test_dir_path = try ctx.transformPath("test_dir");
|
|
const target_dir_path = try ctx.transformPath("target_dir_path");
|
|
|
|
try ctx.dir.makeDir(test_dir_path);
|
|
try ctx.dir.makeDir(target_dir_path);
|
|
try ctx.dir.rename(test_dir_path, target_dir_path);
|
|
|
|
// Ensure the directory was renamed
|
|
try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
|
|
var dir = try ctx.dir.openDir(target_dir_path, .{});
|
|
dir.close();
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "Dir.rename directory onto non-empty dir" {
|
|
// TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
|
|
if (native_os == .windows) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const test_dir_path = try ctx.transformPath("test_dir");
|
|
const target_dir_path = try ctx.transformPath("target_dir_path");
|
|
|
|
try ctx.dir.makeDir(test_dir_path);
|
|
|
|
var target_dir = try ctx.dir.makeOpenPath(target_dir_path, .{});
|
|
var file = try target_dir.createFile("test_file", .{ .read = true });
|
|
file.close();
|
|
target_dir.close();
|
|
|
|
// Rename should fail with PathAlreadyExists if target_dir is non-empty
|
|
try testing.expectError(error.PathAlreadyExists, ctx.dir.rename(test_dir_path, target_dir_path));
|
|
|
|
// Ensure the directory was not renamed
|
|
var dir = try ctx.dir.openDir(test_dir_path, .{});
|
|
dir.close();
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "Dir.rename file <-> dir" {
|
|
// TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
|
|
if (native_os == .windows) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const test_file_path = try ctx.transformPath("test_file");
|
|
const test_dir_path = try ctx.transformPath("test_dir");
|
|
|
|
var file = try ctx.dir.createFile(test_file_path, .{ .read = true });
|
|
file.close();
|
|
try ctx.dir.makeDir(test_dir_path);
|
|
try testing.expectError(error.IsDir, ctx.dir.rename(test_file_path, test_dir_path));
|
|
try testing.expectError(error.NotDir, ctx.dir.rename(test_dir_path, test_file_path));
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "rename" {
|
|
var tmp_dir1 = tmpDir(.{});
|
|
defer tmp_dir1.cleanup();
|
|
|
|
var tmp_dir2 = tmpDir(.{});
|
|
defer tmp_dir2.cleanup();
|
|
|
|
// Renaming files
|
|
const test_file_name = "test_file";
|
|
const renamed_test_file_name = "test_file_renamed";
|
|
var file = try tmp_dir1.dir.createFile(test_file_name, .{ .read = true });
|
|
file.close();
|
|
try fs.rename(tmp_dir1.dir, test_file_name, tmp_dir2.dir, renamed_test_file_name);
|
|
|
|
// ensure the file was renamed
|
|
try testing.expectError(error.FileNotFound, tmp_dir1.dir.openFile(test_file_name, .{}));
|
|
file = try tmp_dir2.dir.openFile(renamed_test_file_name, .{});
|
|
file.close();
|
|
}
|
|
|
|
test "renameAbsolute" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
var tmp_dir = tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
// Get base abs path
|
|
var arena = ArenaAllocator.init(testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
const base_path = try tmp_dir.dir.realpathAlloc(allocator, ".");
|
|
|
|
try testing.expectError(error.FileNotFound, fs.renameAbsolute(
|
|
try fs.path.join(allocator, &.{ base_path, "missing_file_name" }),
|
|
try fs.path.join(allocator, &.{ base_path, "something_else" }),
|
|
));
|
|
|
|
// Renaming files
|
|
const test_file_name = "test_file";
|
|
const renamed_test_file_name = "test_file_renamed";
|
|
var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true });
|
|
file.close();
|
|
try fs.renameAbsolute(
|
|
try fs.path.join(allocator, &.{ base_path, test_file_name }),
|
|
try fs.path.join(allocator, &.{ base_path, renamed_test_file_name }),
|
|
);
|
|
|
|
// ensure the file was renamed
|
|
try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{}));
|
|
file = try tmp_dir.dir.openFile(renamed_test_file_name, .{});
|
|
const stat = try file.stat();
|
|
try testing.expectEqual(File.Kind.file, stat.kind);
|
|
file.close();
|
|
|
|
// Renaming directories
|
|
const test_dir_name = "test_dir";
|
|
const renamed_test_dir_name = "test_dir_renamed";
|
|
try tmp_dir.dir.makeDir(test_dir_name);
|
|
try fs.renameAbsolute(
|
|
try fs.path.join(allocator, &.{ base_path, test_dir_name }),
|
|
try fs.path.join(allocator, &.{ base_path, renamed_test_dir_name }),
|
|
);
|
|
|
|
// ensure the directory was renamed
|
|
try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir(test_dir_name, .{}));
|
|
var dir = try tmp_dir.dir.openDir(renamed_test_dir_name, .{});
|
|
dir.close();
|
|
}
|
|
|
|
test "openSelfExe" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
const self_exe_file = try std.fs.openSelfExe(.{});
|
|
self_exe_file.close();
|
|
}
|
|
|
|
test "selfExePath" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
var buf: [fs.max_path_bytes]u8 = undefined;
|
|
const buf_self_exe_path = try std.fs.selfExePath(&buf);
|
|
const alloc_self_exe_path = try std.fs.selfExePathAlloc(testing.allocator);
|
|
defer testing.allocator.free(alloc_self_exe_path);
|
|
try testing.expectEqualSlices(u8, buf_self_exe_path, alloc_self_exe_path);
|
|
}
|
|
|
|
test "deleteTree does not follow symlinks" {
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.makePath("b");
|
|
{
|
|
var a = try tmp.dir.makeOpenPath("a", .{});
|
|
defer a.close();
|
|
|
|
try setupSymlink(a, "../b", "b", .{ .is_directory = true });
|
|
}
|
|
|
|
try tmp.dir.deleteTree("a");
|
|
|
|
try testing.expectError(error.FileNotFound, tmp.dir.access("a", .{}));
|
|
try tmp.dir.access("b", .{});
|
|
}
|
|
|
|
test "deleteTree on a symlink" {
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
// Symlink to a file
|
|
try tmp.dir.writeFile(.{ .sub_path = "file", .data = "" });
|
|
try setupSymlink(tmp.dir, "file", "filelink", .{});
|
|
|
|
try tmp.dir.deleteTree("filelink");
|
|
try testing.expectError(error.FileNotFound, tmp.dir.access("filelink", .{}));
|
|
try tmp.dir.access("file", .{});
|
|
|
|
// Symlink to a directory
|
|
try tmp.dir.makePath("dir");
|
|
try setupSymlink(tmp.dir, "dir", "dirlink", .{ .is_directory = true });
|
|
|
|
try tmp.dir.deleteTree("dirlink");
|
|
try testing.expectError(error.FileNotFound, tmp.dir.access("dirlink", .{}));
|
|
try tmp.dir.access("dir", .{});
|
|
}
|
|
|
|
test "makePath, put some files in it, deleteTree" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const allocator = ctx.arena.allocator();
|
|
const dir_path = try ctx.transformPath("os_test_tmp");
|
|
|
|
try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" }));
|
|
try ctx.dir.writeFile(.{
|
|
.sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }),
|
|
.data = "nonsense",
|
|
});
|
|
try ctx.dir.writeFile(.{
|
|
.sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }),
|
|
.data = "blah",
|
|
});
|
|
|
|
try ctx.dir.deleteTree(dir_path);
|
|
try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "makePath, put some files in it, deleteTreeMinStackSize" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const allocator = ctx.arena.allocator();
|
|
const dir_path = try ctx.transformPath("os_test_tmp");
|
|
|
|
try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" }));
|
|
try ctx.dir.writeFile(.{
|
|
.sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }),
|
|
.data = "nonsense",
|
|
});
|
|
try ctx.dir.writeFile(.{
|
|
.sub_path = try fs.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }),
|
|
.data = "blah",
|
|
});
|
|
|
|
try ctx.dir.deleteTreeMinStackSize(dir_path);
|
|
try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "makePath in a directory that no longer exists" {
|
|
if (native_os == .windows) return error.SkipZigTest; // Windows returns FileBusy if attempting to remove an open dir
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
try tmp.parent_dir.deleteTree(&tmp.sub_path);
|
|
|
|
try testing.expectError(error.FileNotFound, tmp.dir.makePath("sub-path"));
|
|
}
|
|
|
|
test "makePath but sub_path contains pre-existing file" {
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.makeDir("foo");
|
|
try tmp.dir.writeFile(.{ .sub_path = "foo/bar", .data = "" });
|
|
|
|
try testing.expectError(error.NotDir, tmp.dir.makePath("foo/bar/baz"));
|
|
}
|
|
|
|
fn expectDir(dir: Dir, path: []const u8) !void {
|
|
var d = try dir.openDir(path, .{});
|
|
d.close();
|
|
}
|
|
|
|
test "makepath existing directories" {
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.makeDir("A");
|
|
var tmpA = try tmp.dir.openDir("A", .{});
|
|
defer tmpA.close();
|
|
try tmpA.makeDir("B");
|
|
|
|
const testPath = "A" ++ fs.path.sep_str ++ "B" ++ fs.path.sep_str ++ "C";
|
|
try tmp.dir.makePath(testPath);
|
|
|
|
try expectDir(tmp.dir, testPath);
|
|
}
|
|
|
|
test "makepath through existing valid symlink" {
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.makeDir("realfolder");
|
|
try setupSymlink(tmp.dir, "." ++ fs.path.sep_str ++ "realfolder", "working-symlink", .{});
|
|
|
|
try tmp.dir.makePath("working-symlink" ++ fs.path.sep_str ++ "in-realfolder");
|
|
|
|
try expectDir(tmp.dir, "realfolder" ++ fs.path.sep_str ++ "in-realfolder");
|
|
}
|
|
|
|
test "makepath relative walks" {
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const relPath = try fs.path.join(testing.allocator, &.{
|
|
"first", "..", "second", "..", "third", "..", "first", "A", "..", "B", "..", "C",
|
|
});
|
|
defer testing.allocator.free(relPath);
|
|
|
|
try tmp.dir.makePath(relPath);
|
|
|
|
// How .. is handled is different on Windows than non-Windows
|
|
switch (native_os) {
|
|
.windows => {
|
|
// On Windows, .. is resolved before passing the path to NtCreateFile,
|
|
// meaning everything except `first/C` drops out.
|
|
try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "C");
|
|
try testing.expectError(error.FileNotFound, tmp.dir.access("second", .{}));
|
|
try testing.expectError(error.FileNotFound, tmp.dir.access("third", .{}));
|
|
},
|
|
else => {
|
|
try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "A");
|
|
try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "B");
|
|
try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "C");
|
|
try expectDir(tmp.dir, "second");
|
|
try expectDir(tmp.dir, "third");
|
|
},
|
|
}
|
|
}
|
|
|
|
test "makepath ignores '.'" {
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
// Path to create, with "." elements:
|
|
const dotPath = try fs.path.join(testing.allocator, &.{
|
|
"first", ".", "second", ".", "third",
|
|
});
|
|
defer testing.allocator.free(dotPath);
|
|
|
|
// Path to expect to find:
|
|
const expectedPath = try fs.path.join(testing.allocator, &.{
|
|
"first", "second", "third",
|
|
});
|
|
defer testing.allocator.free(expectedPath);
|
|
|
|
try tmp.dir.makePath(dotPath);
|
|
|
|
try expectDir(tmp.dir, expectedPath);
|
|
}
|
|
|
|
fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void {
|
|
// setup, create a dir and a nested file both with maxed filenames, and walk the dir
|
|
{
|
|
var maxed_dir = try iterable_dir.makeOpenPath(maxed_filename, .{});
|
|
defer maxed_dir.close();
|
|
|
|
try maxed_dir.writeFile(.{ .sub_path = maxed_filename, .data = "" });
|
|
|
|
var walker = try iterable_dir.walk(testing.allocator);
|
|
defer walker.deinit();
|
|
|
|
var count: usize = 0;
|
|
while (try walker.next()) |entry| {
|
|
try testing.expectEqualStrings(maxed_filename, entry.basename);
|
|
count += 1;
|
|
}
|
|
try testing.expectEqual(@as(usize, 2), count);
|
|
}
|
|
|
|
// ensure that we can delete the tree
|
|
try iterable_dir.deleteTree(maxed_filename);
|
|
}
|
|
|
|
test "max file name component lengths" {
|
|
var tmp = tmpDir(.{ .iterate = true });
|
|
defer tmp.cleanup();
|
|
|
|
if (native_os == .windows) {
|
|
// U+FFFF is the character with the largest code point that is encoded as a single
|
|
// UTF-16 code unit, so Windows allows for NAME_MAX of them.
|
|
const maxed_windows_filename = ("\u{FFFF}".*) ** windows.NAME_MAX;
|
|
try testFilenameLimits(tmp.dir, &maxed_windows_filename);
|
|
} else if (native_os == .wasi) {
|
|
// On WASI, the maxed filename depends on the host OS, so in order for this test to
|
|
// work on any host, we need to use a length that will work for all platforms
|
|
// (i.e. the minimum max_name_bytes of all supported platforms).
|
|
const maxed_wasi_filename = [_]u8{'1'} ** 255;
|
|
try testFilenameLimits(tmp.dir, &maxed_wasi_filename);
|
|
} else {
|
|
const maxed_ascii_filename = [_]u8{'1'} ** std.fs.max_name_bytes;
|
|
try testFilenameLimits(tmp.dir, &maxed_ascii_filename);
|
|
}
|
|
}
|
|
|
|
test "writev, readv" {
|
|
const io = testing.io;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const line1 = "line1\n";
|
|
const line2 = "line2\n";
|
|
|
|
var buf1: [line1.len]u8 = undefined;
|
|
var buf2: [line2.len]u8 = undefined;
|
|
var write_vecs: [2][]const u8 = .{ line1, line2 };
|
|
var read_vecs: [2][]u8 = .{ &buf2, &buf1 };
|
|
|
|
var src_file = try tmp.dir.createFile("test.txt", .{ .read = true });
|
|
defer src_file.close();
|
|
|
|
var writer = src_file.writerStreaming(&.{});
|
|
|
|
try writer.interface.writeVecAll(&write_vecs);
|
|
try writer.interface.flush();
|
|
try testing.expectEqual(@as(u64, line1.len + line2.len), try src_file.getEndPos());
|
|
|
|
var reader = writer.moveToReader(io);
|
|
try reader.seekTo(0);
|
|
try reader.interface.readVecAll(&read_vecs);
|
|
try testing.expectEqualStrings(&buf1, "line2\n");
|
|
try testing.expectEqualStrings(&buf2, "line1\n");
|
|
try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1));
|
|
}
|
|
|
|
test "pwritev, preadv" {
|
|
const io = testing.io;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const line1 = "line1\n";
|
|
const line2 = "line2\n";
|
|
var lines: [2][]const u8 = .{ line1, line2 };
|
|
var buf1: [line1.len]u8 = undefined;
|
|
var buf2: [line2.len]u8 = undefined;
|
|
var read_vecs: [2][]u8 = .{ &buf2, &buf1 };
|
|
|
|
var src_file = try tmp.dir.createFile("test.txt", .{ .read = true });
|
|
defer src_file.close();
|
|
|
|
var writer = src_file.writer(&.{});
|
|
|
|
try writer.seekTo(16);
|
|
try writer.interface.writeVecAll(&lines);
|
|
try writer.interface.flush();
|
|
try testing.expectEqual(@as(u64, 16 + line1.len + line2.len), try src_file.getEndPos());
|
|
|
|
var reader = writer.moveToReader(io);
|
|
try reader.seekTo(16);
|
|
try reader.interface.readVecAll(&read_vecs);
|
|
try testing.expectEqualStrings(&buf1, "line2\n");
|
|
try testing.expectEqualStrings(&buf2, "line1\n");
|
|
try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1));
|
|
}
|
|
|
|
test "setEndPos" {
|
|
// https://github.com/ziglang/zig/issues/20747 (open fd does not have write permission)
|
|
if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest;
|
|
if (builtin.cpu.arch.isMIPS64() and (builtin.abi == .gnuabin32 or builtin.abi == .muslabin32)) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/23806
|
|
|
|
const io = testing.io;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const file_name = "afile.txt";
|
|
try tmp.dir.writeFile(.{ .sub_path = file_name, .data = "ninebytes" });
|
|
const f = try tmp.dir.openFile(file_name, .{ .mode = .read_write });
|
|
defer f.close();
|
|
|
|
const initial_size = try f.getEndPos();
|
|
var buffer: [32]u8 = undefined;
|
|
var reader = f.reader(io, &.{});
|
|
|
|
{
|
|
try f.setEndPos(initial_size);
|
|
try testing.expectEqual(initial_size, try f.getEndPos());
|
|
try reader.seekTo(0);
|
|
try testing.expectEqual(initial_size, try reader.interface.readSliceShort(&buffer));
|
|
try testing.expectEqualStrings("ninebytes", buffer[0..@intCast(initial_size)]);
|
|
}
|
|
|
|
{
|
|
const larger = initial_size + 4;
|
|
try f.setEndPos(larger);
|
|
try testing.expectEqual(larger, try f.getEndPos());
|
|
try reader.seekTo(0);
|
|
try testing.expectEqual(larger, try reader.interface.readSliceShort(&buffer));
|
|
try testing.expectEqualStrings("ninebytes\x00\x00\x00\x00", buffer[0..@intCast(larger)]);
|
|
}
|
|
|
|
{
|
|
const smaller = initial_size - 5;
|
|
try f.setEndPos(smaller);
|
|
try testing.expectEqual(smaller, try f.getEndPos());
|
|
try reader.seekTo(0);
|
|
try testing.expectEqual(smaller, try reader.interface.readSliceShort(&buffer));
|
|
try testing.expectEqualStrings("nine", buffer[0..@intCast(smaller)]);
|
|
}
|
|
|
|
try f.setEndPos(0);
|
|
try testing.expectEqual(0, try f.getEndPos());
|
|
try reader.seekTo(0);
|
|
try testing.expectEqual(0, try reader.interface.readSliceShort(&buffer));
|
|
}
|
|
|
|
test "access file" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const dir_path = try ctx.transformPath("os_test_tmp");
|
|
const file_path = try ctx.transformPath("os_test_tmp" ++ fs.path.sep_str ++ "file.txt");
|
|
|
|
try ctx.dir.makePath(dir_path);
|
|
try testing.expectError(error.FileNotFound, ctx.dir.access(file_path, .{}));
|
|
|
|
try ctx.dir.writeFile(.{ .sub_path = file_path, .data = "" });
|
|
try ctx.dir.access(file_path, .{});
|
|
try ctx.dir.deleteTree(dir_path);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "sendfile" {
|
|
const io = testing.io;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.makePath("os_test_tmp");
|
|
|
|
var dir = try tmp.dir.openDir("os_test_tmp", .{});
|
|
defer dir.close();
|
|
|
|
const line1 = "line1\n";
|
|
const line2 = "second line\n";
|
|
var vecs = [_][]const u8{ line1, line2 };
|
|
|
|
var src_file = try dir.createFile("sendfile1.txt", .{ .read = true });
|
|
defer src_file.close();
|
|
{
|
|
var fw = src_file.writer(&.{});
|
|
try fw.interface.writeVecAll(&vecs);
|
|
}
|
|
|
|
var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true });
|
|
defer dest_file.close();
|
|
|
|
const header1 = "header1\n";
|
|
const header2 = "second header\n";
|
|
const trailer1 = "trailer1\n";
|
|
const trailer2 = "second trailer\n";
|
|
var headers: [2][]const u8 = .{ header1, header2 };
|
|
var trailers: [2][]const u8 = .{ trailer1, trailer2 };
|
|
|
|
var written_buf: [100]u8 = undefined;
|
|
var file_reader = src_file.reader(io, &.{});
|
|
var fallback_buffer: [50]u8 = undefined;
|
|
var file_writer = dest_file.writer(&fallback_buffer);
|
|
try file_writer.interface.writeVecAll(&headers);
|
|
try file_reader.seekTo(1);
|
|
try testing.expectEqual(10, try file_writer.interface.sendFileAll(&file_reader, .limited(10)));
|
|
try file_writer.interface.writeVecAll(&trailers);
|
|
try file_writer.interface.flush();
|
|
var fr = file_writer.moveToReader(io);
|
|
try fr.seekTo(0);
|
|
const amt = try fr.interface.readSliceShort(&written_buf);
|
|
try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]);
|
|
}
|
|
|
|
test "sendfile with buffered data" {
|
|
const io = testing.io;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.makePath("os_test_tmp");
|
|
|
|
var dir = try tmp.dir.openDir("os_test_tmp", .{});
|
|
defer dir.close();
|
|
|
|
var src_file = try dir.createFile("sendfile1.txt", .{ .read = true });
|
|
defer src_file.close();
|
|
|
|
try src_file.writeAll("AAAABBBB");
|
|
|
|
var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true });
|
|
defer dest_file.close();
|
|
|
|
var src_buffer: [32]u8 = undefined;
|
|
var file_reader = src_file.reader(io, &src_buffer);
|
|
|
|
try file_reader.seekTo(0);
|
|
try file_reader.interface.fill(8);
|
|
|
|
var fallback_buffer: [32]u8 = undefined;
|
|
var file_writer = dest_file.writer(&fallback_buffer);
|
|
|
|
try std.testing.expectEqual(4, try file_writer.interface.sendFileAll(&file_reader, .limited(4)));
|
|
|
|
var written_buf: [8]u8 = undefined;
|
|
var fr = file_writer.moveToReader(io);
|
|
try fr.seekTo(0);
|
|
const amt = try fr.interface.readSliceShort(&written_buf);
|
|
|
|
try std.testing.expectEqual(4, amt);
|
|
try std.testing.expectEqualSlices(u8, "AAAA", written_buf[0..amt]);
|
|
}
|
|
|
|
test "copyFile" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";
|
|
const src_file = try ctx.transformPath("tmp_test_copy_file.txt");
|
|
const dest_file = try ctx.transformPath("tmp_test_copy_file2.txt");
|
|
const dest_file2 = try ctx.transformPath("tmp_test_copy_file3.txt");
|
|
|
|
try ctx.dir.writeFile(.{ .sub_path = src_file, .data = data });
|
|
defer ctx.dir.deleteFile(src_file) catch {};
|
|
|
|
try ctx.dir.copyFile(src_file, ctx.dir, dest_file, .{});
|
|
defer ctx.dir.deleteFile(dest_file) catch {};
|
|
|
|
try ctx.dir.copyFile(src_file, ctx.dir, dest_file2, .{ .override_mode = File.default_mode });
|
|
defer ctx.dir.deleteFile(dest_file2) catch {};
|
|
|
|
try expectFileContents(ctx.dir, dest_file, data);
|
|
try expectFileContents(ctx.dir, dest_file2, data);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void {
|
|
const contents = try dir.readFileAlloc(file_path, testing.allocator, .limited(1000));
|
|
defer testing.allocator.free(contents);
|
|
|
|
try testing.expectEqualSlices(u8, data, contents);
|
|
}
|
|
|
|
test "AtomicFile" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const allocator = ctx.arena.allocator();
|
|
const test_out_file = try ctx.transformPath("tmp_atomic_file_test_dest.txt");
|
|
const test_content =
|
|
\\ hello!
|
|
\\ this is a test file
|
|
;
|
|
|
|
{
|
|
var buffer: [100]u8 = undefined;
|
|
var af = try ctx.dir.atomicFile(test_out_file, .{ .write_buffer = &buffer });
|
|
defer af.deinit();
|
|
try af.file_writer.interface.writeAll(test_content);
|
|
try af.finish();
|
|
}
|
|
const content = try ctx.dir.readFileAlloc(test_out_file, allocator, .limited(9999));
|
|
try testing.expectEqualStrings(test_content, content);
|
|
|
|
try ctx.dir.deleteFile(test_out_file);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "open file with exclusive nonblocking lock twice" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
|
|
|
|
const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
|
|
defer file1.close();
|
|
|
|
const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
|
|
try testing.expectError(error.WouldBlock, file2);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "open file with shared and exclusive nonblocking lock" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
|
|
|
|
const file1 = try ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
|
|
defer file1.close();
|
|
|
|
const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
|
|
try testing.expectError(error.WouldBlock, file2);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "open file with exclusive and shared nonblocking lock" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
|
|
|
|
const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
|
|
defer file1.close();
|
|
|
|
const file2 = ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
|
|
try testing.expectError(error.WouldBlock, file2);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "open file with exclusive lock twice, make sure second lock waits" {
|
|
if (builtin.single_threaded) return error.SkipZigTest;
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const filename = try ctx.transformPath("file_lock_test.txt");
|
|
|
|
const file = try ctx.dir.createFile(filename, .{ .lock = .exclusive });
|
|
errdefer file.close();
|
|
|
|
const S = struct {
|
|
fn checkFn(dir: *fs.Dir, path: []const u8, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void {
|
|
started.set();
|
|
const file1 = try dir.createFile(path, .{ .lock = .exclusive });
|
|
|
|
locked.set();
|
|
file1.close();
|
|
}
|
|
};
|
|
|
|
var started: std.Thread.ResetEvent = .unset;
|
|
var locked: std.Thread.ResetEvent = .unset;
|
|
|
|
const t = try std.Thread.spawn(.{}, S.checkFn, .{
|
|
&ctx.dir,
|
|
filename,
|
|
&started,
|
|
&locked,
|
|
});
|
|
defer t.join();
|
|
|
|
// Wait for the spawned thread to start trying to acquire the exclusive file lock.
|
|
// Then wait a bit to make sure that can't acquire it since we currently hold the file lock.
|
|
started.wait();
|
|
try testing.expectError(error.Timeout, locked.timedWait(10 * std.time.ns_per_ms));
|
|
|
|
// Release the file lock which should unlock the thread to lock it and set the locked event.
|
|
file.close();
|
|
locked.wait();
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "open file with exclusive nonblocking lock twice (absolute paths)" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
var random_bytes: [12]u8 = undefined;
|
|
std.crypto.random.bytes(&random_bytes);
|
|
|
|
var random_b64: [fs.base64_encoder.calcSize(random_bytes.len)]u8 = undefined;
|
|
_ = fs.base64_encoder.encode(&random_b64, &random_bytes);
|
|
|
|
const sub_path = random_b64 ++ "-zig-test-absolute-paths.txt";
|
|
|
|
const gpa = testing.allocator;
|
|
|
|
const cwd = try std.process.getCwdAlloc(gpa);
|
|
defer gpa.free(cwd);
|
|
|
|
const filename = try fs.path.resolve(gpa, &.{ cwd, sub_path });
|
|
defer gpa.free(filename);
|
|
|
|
defer fs.deleteFileAbsolute(filename) catch {}; // createFileAbsolute can leave files on failures
|
|
const file1 = try fs.createFileAbsolute(filename, .{
|
|
.lock = .exclusive,
|
|
.lock_nonblocking = true,
|
|
});
|
|
|
|
const file2 = fs.createFileAbsolute(filename, .{
|
|
.lock = .exclusive,
|
|
.lock_nonblocking = true,
|
|
});
|
|
file1.close();
|
|
try testing.expectError(error.WouldBlock, file2);
|
|
}
|
|
|
|
test "read from locked file" {
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const filename = try ctx.transformPath("read_lock_file_test.txt");
|
|
|
|
{
|
|
const f = try ctx.dir.createFile(filename, .{ .read = true });
|
|
defer f.close();
|
|
var buffer: [1]u8 = undefined;
|
|
_ = try f.read(&buffer);
|
|
}
|
|
{
|
|
const f = try ctx.dir.createFile(filename, .{
|
|
.read = true,
|
|
.lock = .exclusive,
|
|
});
|
|
defer f.close();
|
|
const f2 = try ctx.dir.openFile(filename, .{});
|
|
defer f2.close();
|
|
var buffer: [1]u8 = undefined;
|
|
if (builtin.os.tag == .windows) {
|
|
try std.testing.expectError(error.LockViolation, f2.read(&buffer));
|
|
} else {
|
|
try std.testing.expectEqual(0, f2.read(&buffer));
|
|
}
|
|
}
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "walker" {
|
|
var tmp = tmpDir(.{ .iterate = true });
|
|
defer tmp.cleanup();
|
|
|
|
// iteration order of walker is undefined, so need lookup maps to check against
|
|
|
|
const expected_paths = std.StaticStringMap(usize).initComptime(.{
|
|
.{ "dir1", 1 },
|
|
.{ "dir2", 1 },
|
|
.{ "dir3", 1 },
|
|
.{ "dir4", 1 },
|
|
.{ "dir3" ++ fs.path.sep_str ++ "sub1", 2 },
|
|
.{ "dir3" ++ fs.path.sep_str ++ "sub2", 2 },
|
|
.{ "dir3" ++ fs.path.sep_str ++ "sub2" ++ fs.path.sep_str ++ "subsub1", 3 },
|
|
});
|
|
|
|
const expected_basenames = std.StaticStringMap(void).initComptime(.{
|
|
.{"dir1"},
|
|
.{"dir2"},
|
|
.{"dir3"},
|
|
.{"dir4"},
|
|
.{"sub1"},
|
|
.{"sub2"},
|
|
.{"subsub1"},
|
|
});
|
|
|
|
for (expected_paths.keys()) |key| {
|
|
try tmp.dir.makePath(key);
|
|
}
|
|
|
|
var walker = try tmp.dir.walk(testing.allocator);
|
|
defer walker.deinit();
|
|
|
|
var num_walked: usize = 0;
|
|
while (try walker.next()) |entry| {
|
|
testing.expect(expected_basenames.has(entry.basename)) catch |err| {
|
|
std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)});
|
|
return err;
|
|
};
|
|
testing.expect(expected_paths.has(entry.path)) catch |err| {
|
|
std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
|
|
return err;
|
|
};
|
|
testing.expectEqual(expected_paths.get(entry.path).?, entry.depth()) catch |err| {
|
|
std.debug.print("path reported unexpected depth: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
|
|
return err;
|
|
};
|
|
// make sure that the entry.dir is the containing dir
|
|
var entry_dir = try entry.dir.openDir(entry.basename, .{});
|
|
defer entry_dir.close();
|
|
num_walked += 1;
|
|
}
|
|
try testing.expectEqual(expected_paths.kvs.len, num_walked);
|
|
}
|
|
|
|
test "selective walker, skip entries that start with ." {
|
|
var tmp = tmpDir(.{ .iterate = true });
|
|
defer tmp.cleanup();
|
|
|
|
const paths_to_create: []const []const u8 = &.{
|
|
"dir1/foo/.git/ignored",
|
|
".hidden/bar",
|
|
"a/b/c",
|
|
"a/baz",
|
|
};
|
|
|
|
// iteration order of walker is undefined, so need lookup maps to check against
|
|
|
|
const expected_paths = std.StaticStringMap(usize).initComptime(.{
|
|
.{ "dir1", 1 },
|
|
.{ "dir1" ++ fs.path.sep_str ++ "foo", 2 },
|
|
.{ "a", 1 },
|
|
.{ "a" ++ fs.path.sep_str ++ "b", 2 },
|
|
.{ "a" ++ fs.path.sep_str ++ "b" ++ fs.path.sep_str ++ "c", 3 },
|
|
.{ "a" ++ fs.path.sep_str ++ "baz", 2 },
|
|
});
|
|
|
|
const expected_basenames = std.StaticStringMap(void).initComptime(.{
|
|
.{"dir1"},
|
|
.{"foo"},
|
|
.{"a"},
|
|
.{"b"},
|
|
.{"c"},
|
|
.{"baz"},
|
|
});
|
|
|
|
for (paths_to_create) |path| {
|
|
try tmp.dir.makePath(path);
|
|
}
|
|
|
|
var walker = try tmp.dir.walkSelectively(testing.allocator);
|
|
defer walker.deinit();
|
|
|
|
var num_walked: usize = 0;
|
|
while (try walker.next()) |entry| {
|
|
if (entry.basename[0] == '.') continue;
|
|
if (entry.kind == .directory) {
|
|
try walker.enter(entry);
|
|
}
|
|
|
|
testing.expect(expected_basenames.has(entry.basename)) catch |err| {
|
|
std.debug.print("found unexpected basename: {f}\n", .{std.ascii.hexEscape(entry.basename, .lower)});
|
|
return err;
|
|
};
|
|
testing.expect(expected_paths.has(entry.path)) catch |err| {
|
|
std.debug.print("found unexpected path: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
|
|
return err;
|
|
};
|
|
testing.expectEqual(expected_paths.get(entry.path).?, entry.depth()) catch |err| {
|
|
std.debug.print("path reported unexpected depth: {f}\n", .{std.ascii.hexEscape(entry.path, .lower)});
|
|
return err;
|
|
};
|
|
|
|
// make sure that the entry.dir is the containing dir
|
|
var entry_dir = try entry.dir.openDir(entry.basename, .{});
|
|
defer entry_dir.close();
|
|
num_walked += 1;
|
|
}
|
|
try testing.expectEqual(expected_paths.kvs.len, num_walked);
|
|
}
|
|
|
|
test "walker without fully iterating" {
|
|
var tmp = tmpDir(.{ .iterate = true });
|
|
defer tmp.cleanup();
|
|
|
|
var walker = try tmp.dir.walk(testing.allocator);
|
|
defer walker.deinit();
|
|
|
|
// Create 2 directories inside the tmp directory, but then only iterate once before breaking.
|
|
// This ensures that walker doesn't try to close the initial directory when not fully iterating.
|
|
|
|
try tmp.dir.makePath("a");
|
|
try tmp.dir.makePath("b");
|
|
|
|
var num_walked: usize = 0;
|
|
while (try walker.next()) |_| {
|
|
num_walked += 1;
|
|
break;
|
|
}
|
|
try testing.expectEqual(@as(usize, 1), num_walked);
|
|
}
|
|
|
|
test "'.' and '..' in fs.Dir functions" {
|
|
if (native_os == .windows and builtin.cpu.arch == .aarch64) {
|
|
// https://github.com/ziglang/zig/issues/17134
|
|
return error.SkipZigTest;
|
|
}
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const io = ctx.io;
|
|
const subdir_path = try ctx.transformPath("./subdir");
|
|
const file_path = try ctx.transformPath("./subdir/../file");
|
|
const copy_path = try ctx.transformPath("./subdir/../copy");
|
|
const rename_path = try ctx.transformPath("./subdir/../rename");
|
|
const update_path = try ctx.transformPath("./subdir/../update");
|
|
|
|
try ctx.dir.makeDir(subdir_path);
|
|
try ctx.dir.access(subdir_path, .{});
|
|
var created_subdir = try ctx.dir.openDir(subdir_path, .{});
|
|
created_subdir.close();
|
|
|
|
const created_file = try ctx.dir.createFile(file_path, .{});
|
|
created_file.close();
|
|
try ctx.dir.access(file_path, .{});
|
|
|
|
try ctx.dir.copyFile(file_path, ctx.dir, copy_path, .{});
|
|
try ctx.dir.rename(copy_path, rename_path);
|
|
const renamed_file = try ctx.dir.openFile(rename_path, .{});
|
|
renamed_file.close();
|
|
try ctx.dir.deleteFile(rename_path);
|
|
|
|
try ctx.dir.writeFile(.{ .sub_path = update_path, .data = "something" });
|
|
var dir = ctx.dir.adaptToNewApi();
|
|
const prev_status = try dir.updateFile(io, file_path, dir, update_path, .{});
|
|
try testing.expectEqual(Io.Dir.PrevStatus.stale, prev_status);
|
|
|
|
try ctx.dir.deleteDir(subdir_path);
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "'.' and '..' in absolute functions" {
|
|
if (native_os == .wasi) return error.SkipZigTest;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
var arena = ArenaAllocator.init(testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
const base_path = try tmp.dir.realpathAlloc(allocator, ".");
|
|
|
|
const subdir_path = try fs.path.join(allocator, &.{ base_path, "./subdir" });
|
|
try fs.makeDirAbsolute(subdir_path);
|
|
try fs.accessAbsolute(subdir_path, .{});
|
|
var created_subdir = try fs.openDirAbsolute(subdir_path, .{});
|
|
created_subdir.close();
|
|
|
|
const created_file_path = try fs.path.join(allocator, &.{ subdir_path, "../file" });
|
|
const created_file = try fs.createFileAbsolute(created_file_path, .{});
|
|
created_file.close();
|
|
try fs.accessAbsolute(created_file_path, .{});
|
|
|
|
const copied_file_path = try fs.path.join(allocator, &.{ subdir_path, "../copy" });
|
|
try fs.copyFileAbsolute(created_file_path, copied_file_path, .{});
|
|
const renamed_file_path = try fs.path.join(allocator, &.{ subdir_path, "../rename" });
|
|
try fs.renameAbsolute(copied_file_path, renamed_file_path);
|
|
const renamed_file = try fs.openFileAbsolute(renamed_file_path, .{});
|
|
renamed_file.close();
|
|
try fs.deleteFileAbsolute(renamed_file_path);
|
|
|
|
try fs.deleteDirAbsolute(subdir_path);
|
|
}
|
|
|
|
test "chmod" {
|
|
if (native_os == .windows or native_os == .wasi)
|
|
return error.SkipZigTest;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const file = try tmp.dir.createFile("test_file", .{ .mode = 0o600 });
|
|
defer file.close();
|
|
try testing.expectEqual(@as(File.Mode, 0o600), (try file.stat()).mode & 0o7777);
|
|
|
|
try file.chmod(0o644);
|
|
try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777);
|
|
|
|
try tmp.dir.makeDir("test_dir");
|
|
var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true });
|
|
defer dir.close();
|
|
|
|
try dir.chmod(0o700);
|
|
try testing.expectEqual(@as(File.Mode, 0o700), (try dir.stat()).mode & 0o7777);
|
|
}
|
|
|
|
test "chown" {
|
|
if (native_os == .windows or native_os == .wasi)
|
|
return error.SkipZigTest;
|
|
|
|
var tmp = tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const file = try tmp.dir.createFile("test_file", .{});
|
|
defer file.close();
|
|
try file.chown(null, null);
|
|
|
|
try tmp.dir.makeDir("test_dir");
|
|
|
|
var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true });
|
|
defer dir.close();
|
|
try dir.chown(null, null);
|
|
}
|
|
|
|
test "delete a setAsCwd directory on Windows" {
|
|
if (native_os != .windows) return error.SkipZigTest;
|
|
|
|
var tmp = tmpDir(.{});
|
|
// Set tmp dir as current working directory.
|
|
try tmp.dir.setAsCwd();
|
|
tmp.dir.close();
|
|
try testing.expectError(error.FileBusy, tmp.parent_dir.deleteTree(&tmp.sub_path));
|
|
// Now set the parent dir as the current working dir for clean up.
|
|
try tmp.parent_dir.setAsCwd();
|
|
try tmp.parent_dir.deleteTree(&tmp.sub_path);
|
|
// Close the parent "tmp" so we don't leak the HANDLE.
|
|
tmp.parent_dir.close();
|
|
}
|
|
|
|
test "invalid UTF-8/WTF-8 paths" {
|
|
const expected_err = switch (native_os) {
|
|
.wasi => error.BadPathName,
|
|
.windows => error.BadPathName,
|
|
else => return error.SkipZigTest,
|
|
};
|
|
|
|
try testWithAllSupportedPathTypes(struct {
|
|
fn impl(ctx: *TestContext) !void {
|
|
const io = ctx.io;
|
|
// This is both invalid UTF-8 and WTF-8, since \xFF is an invalid start byte
|
|
const invalid_path = try ctx.transformPath("\xFF");
|
|
|
|
try testing.expectError(expected_err, ctx.dir.openFile(invalid_path, .{}));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.createFile(invalid_path, .{}));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.makeDir(invalid_path));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.makePath(invalid_path));
|
|
try testing.expectError(expected_err, ctx.dir.makeOpenPath(invalid_path, .{}));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.openDir(invalid_path, .{}));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.deleteFile(invalid_path));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.deleteDir(invalid_path));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.rename(invalid_path, invalid_path));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.symLink(invalid_path, invalid_path, .{}));
|
|
if (native_os == .wasi) {
|
|
try testing.expectError(expected_err, ctx.dir.symLinkWasi(invalid_path, invalid_path, .{}));
|
|
}
|
|
|
|
try testing.expectError(expected_err, ctx.dir.readLink(invalid_path, &[_]u8{}));
|
|
if (native_os == .wasi) {
|
|
try testing.expectError(expected_err, ctx.dir.readLinkWasi(invalid_path, &[_]u8{}));
|
|
}
|
|
|
|
try testing.expectError(expected_err, ctx.dir.readFile(invalid_path, &[_]u8{}));
|
|
try testing.expectError(expected_err, ctx.dir.readFileAlloc(invalid_path, testing.allocator, .limited(0)));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.deleteTree(invalid_path));
|
|
try testing.expectError(expected_err, ctx.dir.deleteTreeMinStackSize(invalid_path));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.writeFile(.{ .sub_path = invalid_path, .data = "" }));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.access(invalid_path, .{}));
|
|
|
|
var dir = ctx.dir.adaptToNewApi();
|
|
try testing.expectError(expected_err, dir.updateFile(io, invalid_path, dir, invalid_path, .{}));
|
|
try testing.expectError(expected_err, ctx.dir.copyFile(invalid_path, ctx.dir, invalid_path, .{}));
|
|
|
|
try testing.expectError(expected_err, ctx.dir.statFile(invalid_path));
|
|
|
|
if (native_os != .wasi) {
|
|
try testing.expectError(expected_err, ctx.dir.realpath(invalid_path, &[_]u8{}));
|
|
try testing.expectError(expected_err, ctx.dir.realpathAlloc(testing.allocator, invalid_path));
|
|
}
|
|
|
|
try testing.expectError(expected_err, fs.rename(ctx.dir, invalid_path, ctx.dir, invalid_path));
|
|
|
|
if (native_os != .wasi and ctx.path_type != .relative) {
|
|
try testing.expectError(expected_err, fs.copyFileAbsolute(invalid_path, invalid_path, .{}));
|
|
try testing.expectError(expected_err, fs.makeDirAbsolute(invalid_path));
|
|
try testing.expectError(expected_err, fs.deleteDirAbsolute(invalid_path));
|
|
try testing.expectError(expected_err, fs.renameAbsolute(invalid_path, invalid_path));
|
|
try testing.expectError(expected_err, fs.openDirAbsolute(invalid_path, .{}));
|
|
try testing.expectError(expected_err, fs.openFileAbsolute(invalid_path, .{}));
|
|
try testing.expectError(expected_err, fs.accessAbsolute(invalid_path, .{}));
|
|
try testing.expectError(expected_err, fs.createFileAbsolute(invalid_path, .{}));
|
|
try testing.expectError(expected_err, fs.deleteFileAbsolute(invalid_path));
|
|
try testing.expectError(expected_err, fs.deleteTreeAbsolute(invalid_path));
|
|
var readlink_buf: [fs.max_path_bytes]u8 = undefined;
|
|
try testing.expectError(expected_err, fs.readLinkAbsolute(invalid_path, &readlink_buf));
|
|
try testing.expectError(expected_err, fs.symLinkAbsolute(invalid_path, invalid_path, .{}));
|
|
try testing.expectError(expected_err, fs.realpathAlloc(testing.allocator, invalid_path));
|
|
}
|
|
}
|
|
}.impl);
|
|
}
|
|
|
|
test "read file non vectored" {
|
|
const io = std.testing.io;
|
|
|
|
var tmp_dir = testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
const contents = "hello, world!\n";
|
|
|
|
const file = try tmp_dir.dir.createFile("input.txt", .{ .read = true });
|
|
defer file.close();
|
|
{
|
|
var file_writer: std.fs.File.Writer = .init(file, &.{});
|
|
try file_writer.interface.writeAll(contents);
|
|
try file_writer.interface.flush();
|
|
}
|
|
|
|
var file_reader: std.Io.File.Reader = .initAdapted(file, io, &.{});
|
|
|
|
var write_buffer: [100]u8 = undefined;
|
|
var w: std.Io.Writer = .fixed(&write_buffer);
|
|
|
|
var i: usize = 0;
|
|
while (true) {
|
|
i += file_reader.interface.stream(&w, .limited(3)) catch |err| switch (err) {
|
|
error.EndOfStream => break,
|
|
else => |e| return e,
|
|
};
|
|
}
|
|
try testing.expectEqualStrings(contents, w.buffered());
|
|
try testing.expectEqual(contents.len, i);
|
|
}
|
|
|
|
test "seek keeping partial buffer" {
|
|
const io = std.testing.io;
|
|
|
|
var tmp_dir = testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
const contents = "0123456789";
|
|
|
|
const file = try tmp_dir.dir.createFile("input.txt", .{ .read = true });
|
|
defer file.close();
|
|
{
|
|
var file_writer: std.fs.File.Writer = .init(file, &.{});
|
|
try file_writer.interface.writeAll(contents);
|
|
try file_writer.interface.flush();
|
|
}
|
|
|
|
var read_buffer: [3]u8 = undefined;
|
|
var file_reader: Io.File.Reader = .initAdapted(file, io, &read_buffer);
|
|
|
|
try testing.expectEqual(0, file_reader.logicalPos());
|
|
|
|
var buf: [4]u8 = undefined;
|
|
try file_reader.interface.readSliceAll(&buf);
|
|
|
|
if (file_reader.interface.bufferedLen() != 3) {
|
|
// Pass the test if the OS doesn't give us vectored reads.
|
|
return;
|
|
}
|
|
|
|
try testing.expectEqual(4, file_reader.logicalPos());
|
|
try testing.expectEqual(7, file_reader.pos);
|
|
try file_reader.seekTo(6);
|
|
try testing.expectEqual(6, file_reader.logicalPos());
|
|
try testing.expectEqual(7, file_reader.pos);
|
|
|
|
try testing.expectEqualStrings("0123", &buf);
|
|
|
|
const n = try file_reader.interface.readSliceShort(&buf);
|
|
try testing.expectEqual(4, n);
|
|
|
|
try testing.expectEqualStrings("6789", &buf);
|
|
}
|
|
|
|
test "seekBy" {
|
|
const io = testing.io;
|
|
|
|
var tmp_dir = testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
try tmp_dir.dir.writeFile(.{ .sub_path = "blah.txt", .data = "let's test seekBy" });
|
|
const f = try tmp_dir.dir.openFile("blah.txt", .{ .mode = .read_only });
|
|
defer f.close();
|
|
var reader = f.readerStreaming(io, &.{});
|
|
try reader.seekBy(2);
|
|
|
|
var buffer: [20]u8 = undefined;
|
|
const n = try reader.interface.readSliceShort(&buffer);
|
|
try testing.expectEqual(15, n);
|
|
try testing.expectEqualStrings("t's test seekBy", buffer[0..15]);
|
|
}
|
|
|
|
test "seekTo flushes buffered data" {
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const io = std.testing.io;
|
|
|
|
const contents = "data";
|
|
|
|
const file = try tmp.dir.createFile("seek.bin", .{ .read = true });
|
|
defer file.close();
|
|
{
|
|
var buf: [16]u8 = undefined;
|
|
var file_writer = std.fs.File.writer(file, &buf);
|
|
|
|
try file_writer.interface.writeAll(contents);
|
|
try file_writer.seekTo(8);
|
|
try file_writer.interface.flush();
|
|
}
|
|
|
|
var read_buffer: [16]u8 = undefined;
|
|
var file_reader: std.Io.File.Reader = .initAdapted(file, io, &read_buffer);
|
|
|
|
var buf: [4]u8 = undefined;
|
|
try file_reader.interface.readSliceAll(&buf);
|
|
try std.testing.expectEqualStrings(contents, &buf);
|
|
}
|
|
|
|
test "File.Writer sendfile with buffered contents" {
|
|
const io = testing.io;
|
|
|
|
var tmp_dir = testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
{
|
|
try tmp_dir.dir.writeFile(.{ .sub_path = "a", .data = "bcd" });
|
|
const in = try tmp_dir.dir.openFile("a", .{});
|
|
defer in.close();
|
|
const out = try tmp_dir.dir.createFile("b", .{});
|
|
defer out.close();
|
|
|
|
var in_buf: [2]u8 = undefined;
|
|
var in_r = in.reader(io, &in_buf);
|
|
_ = try in_r.getSize(); // Catch seeks past end by populating size
|
|
try in_r.interface.fill(2);
|
|
|
|
var out_buf: [1]u8 = undefined;
|
|
var out_w = out.writerStreaming(&out_buf);
|
|
try out_w.interface.writeByte('a');
|
|
try testing.expectEqual(3, try out_w.interface.sendFileAll(&in_r, .unlimited));
|
|
try out_w.interface.flush();
|
|
}
|
|
|
|
var check = try tmp_dir.dir.openFile("b", .{});
|
|
defer check.close();
|
|
var check_buf: [4]u8 = undefined;
|
|
var check_r = check.reader(io, &check_buf);
|
|
try testing.expectEqualStrings("abcd", try check_r.interface.take(4));
|
|
try testing.expectError(error.EndOfStream, check_r.interface.takeByte());
|
|
}
|