aboutsummaryrefslogtreecommitdiff
path: root/src/fs.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/fs.zig')
-rw-r--r--src/fs.zig500
1 files changed, 362 insertions, 138 deletions
diff --git a/src/fs.zig b/src/fs.zig
index f1bee2d..8519ac8 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -3,8 +3,13 @@ const assert = std.debug.assert;
const fatal = @import("cli.zig").fatal;
+pub const max_path_bytes = std.Io.Dir.max_path_bytes;
+
+const default_file_mode: std.posix.mode_t = 0o644;
+const default_dir_mode: std.posix.mode_t = 0o755;
+
pub const Path = struct {
- buf: [std.fs.max_path_bytes]u8,
+ buf: [max_path_bytes]u8,
len: usize,
pub fn init(path: []const u8) Path {
@@ -29,12 +34,12 @@ pub const Path = struct {
inline for (info.fields) |field| {
var seg: []const u8 = @field(segments, field.name);
if (seg.len > 0) {
- if (self.len > 0 and seg[0] == std.fs.path.sep) seg = seg[1..];
- if (seg[seg.len - 1] == std.fs.path.sep) seg = seg[0 .. seg.len - 1];
+ if (self.len > 0 and seg[0] == '/') seg = seg[1..];
+ if (seg[seg.len - 1] == '/') seg = seg[0 .. seg.len - 1];
}
if (seg.len > 0) {
if (self.len > 0) {
- self.buf[self.len] = std.fs.path.sep;
+ self.buf[self.len] = '/';
self.len += 1;
}
@memcpy(self.buf[self.len .. self.len + seg.len], seg);
@@ -48,146 +53,189 @@ pub const Path = struct {
return self.buf[0..self.len :0];
}
- pub fn basename(self: *const Path) []const u8 {
- return std.fs.path.basename(self.name());
- }
-
pub fn dir(self: *const Path) ?Path {
- const dirname = std.fs.path.dirname(self.name()) orelse return null;
- return .init(dirname);
+ if (self.len == 0) return null;
+ const path = self.buf[0..if (self.buf[self.len - 1] == '/') self.len - 1 else self.len];
+ const off = std.mem.findScalarLast(u8, path, '/') orelse return .init("/");
+ return .init(path[0..off]);
}
/// Returns the relative name of `self` regarding to `root`.
/// Both paths must be absolute.
- pub fn relname(self: *const Path, root: Path) ?[]const u8 {
- assert(std.fs.path.isAbsolute(root.name()));
- assert(std.fs.path.isAbsolute(self.name()));
+ pub fn relname(self: *const Path, root: *const Path) ?[]const u8 {
+ assert(std.Io.Dir.path.isAbsolute(root.name()));
+ assert(std.Io.Dir.path.isAbsolute(self.name()));
if (!std.mem.startsWith(u8, self.name(), root.name())) return null;
if (self.len == root.len) return "";
var rel = self.buf[root.len..self.len];
- if (rel[0] == std.fs.path.sep) rel = rel[1..];
+ if (rel[0] == '/') rel = rel[1..];
return rel;
}
};
-pub fn mkdir_all(path: Path) !void {
- try std.fs.cwd().makePath(path.name());
+pub fn mkdir(path: *const Path) !void {
+ const res = std.posix.system.mkdirat(std.posix.AT.FDCWD, path.name(), default_dir_mode);
+ try result_to_error(res);
}
-pub fn touch(path: Path) !void {
- const cwd = std.fs.cwd();
- if (std.fs.path.dirname(path.name())) |dir| {
- try cwd.makePath(dir);
- }
- const f = try cwd.createFileZ(path.name(), .{ .mode = 0o644 });
- f.close();
+pub fn mkdir_all(path: *const Path) !void {
+ const info = try dir_info(path);
+ if (info.exists) return;
+ if (path.dir()) |parent| try mkdir_all(&parent);
+ try mkdir(path);
}
-pub fn rm(path: Path) !void {
- std.fs.deleteTreeAbsolute(path.name()) catch |err| {
- if (err != error.FileNotFound) return err;
- };
-}
+pub fn touch(path: *const Path) !void {
+ if (path.dir()) |dir| try mkdir_all(&dir);
-pub fn cp_file(from_path: Path, to_path: Path) !void {
- const from = from_path.name();
- const to = to_path.name();
- std.fs.deleteTreeAbsolute(to) catch |err| {
- if (err != error.FileNotFound) return err;
- };
- if (to_path.dir()) |to_parent| try mkdir_all(to_parent);
- try std.fs.copyFileAbsolute(from, to, .{});
+ const res = std.posix.system.openat(std.posix.AT.FDCWD, path.name(), .{ .CREAT = true }, default_file_mode);
+ try result_to_error(res);
+ _ = std.posix.system.close(@intCast(res));
}
-pub fn cp_dir(allocator: std.mem.Allocator, from_path: Path, to_path: Path) !void {
- const from = from_path.name();
- const to = to_path.name();
- std.fs.deleteTreeAbsolute(to) catch |err| {
- if (err != error.FileNotFound) return err;
+pub fn rm(path: *const Path) !void {
+ var iter = iterate(path) catch |err| {
+ switch (err) {
+ error.NotDir => {
+ const res = std.posix.system.unlinkat(std.posix.AT.FDCWD, path.name(), 0);
+ try result_to_error(res);
+ return;
+ },
+ error.FileNotFound => return,
+ else => return err,
+ }
};
- try mkdir_all(to_path);
- try cp_tree(allocator, from, to);
+ defer iter.deinit();
+
+ while (try iter.next()) |entry| {
+ const child_path: Path = .join(.{ path.name(), entry.name });
+ switch (entry.type) {
+ .dir => {
+ try rm(&child_path);
+ },
+ else => {
+ const res = std.posix.system.unlinkat(std.posix.AT.FDCWD, child_path.name(), 0);
+ if (res == -1) return result_to_error(res);
+ },
+ }
+ }
+
+ const res = std.posix.system.rmdir(path.name());
+ try result_to_error(res);
}
-pub fn mv_dir(allocator: std.mem.Allocator, from_path: Path, to_path: Path) !void {
- const from = from_path.name();
- const to = to_path.name();
- std.fs.deleteTreeAbsolute(to) catch |err| {
- if (err != error.FileNotFound) return err;
- };
- if (to_path.dir()) |to_parent| try mkdir_all(to_parent);
+pub fn cp_file(from: *const Path, to: *const Path) !void {
+ try rm(to);
+ if (to.dir()) |to_parent| try mkdir_all(&to_parent);
- std.fs.renameAbsoluteZ(from, to) catch |err| {
- if (err != error.RenameAcrossMountPoints) return err;
- try std.fs.makeDirAbsoluteZ(to);
- try cp_tree(allocator, from, to);
- try rm(from_path);
- };
+ var res = std.posix.system.openat(std.posix.AT.FDCWD, from.name(), .{});
+ try result_to_error(res);
+ const from_fd: std.posix.fd_t = @intCast(res);
+ defer _ = std.posix.system.close(from_fd);
+
+ res = std.posix.system.openat(std.posix.AT.FDCWD, to.name(), .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, default_file_mode);
+ try result_to_error(res);
+ const to_fd: std.posix.fd_t = @intCast(res);
+ defer _ = std.posix.system.close(to_fd);
+
+ var buf: [4096]u8 = undefined;
+ while (true) {
+ var rres = std.posix.system.read(from_fd, &buf, buf.len);
+ if (rres < 0) {
+ const err = result_to_error(rres);
+ if (err == error.Interrupted) continue;
+ return err;
+ }
+ if (rres == 0) break;
+ const bytes_read: usize = @intCast(rres);
+ var bytes_written: usize = 0;
+ while (bytes_written < bytes_read) {
+ const wbuf = buf[bytes_written..bytes_read];
+ rres = std.posix.system.write(to_fd, wbuf.ptr, wbuf.len);
+ if (rres < 0) {
+ const err = result_to_error(rres);
+ if (err == error.Interrupted) continue;
+ }
+ bytes_written += @intCast(rres);
+ }
+ }
}
-pub fn mv_file(from_path: Path, to_path: Path) !void {
- const from = from_path.name();
- const to = to_path.name();
- std.fs.deleteTreeAbsolute(to) catch |err| {
- if (err != error.FileNotFound) return err;
- };
- if (to_path.dir()) |to_parent| try mkdir_all(to_parent);
+pub fn mv_dir(from: *const Path, to: *const Path) !void {
+ try rm(to);
+ if (to.dir()) |to_parent| try mkdir_all(&to_parent);
- std.fs.renameAbsoluteZ(from, to) catch |err| {
- if (err != error.RenameAcrossMountPoints) return err;
- try std.fs.copyFileAbsolute(from, to, .{});
- try rm(from_path);
- };
+ const res = std.posix.system.renameat(std.posix.AT.FDCWD, from.name(), std.posix.AT.FDCWD, to.name());
+ if (res == 0) return;
+
+ const err = result_to_error(res);
+ if (err != error.CrossDevice) return err;
+ try mkdir(to);
+ try cp_tree(from, to);
+ try rm(from);
}
-fn cp_tree(allocator: std.mem.Allocator, from: [:0]const u8, to: [:0]const u8) !void {
- var from_dir = try std.fs.openDirAbsoluteZ(from, .{ .iterate = true });
- defer from_dir.close();
+pub fn mv_file(from: *const Path, to: *const Path) !void {
+ try rm(to);
+ if (to.dir()) |to_parent| try mkdir_all(&to_parent);
- var walker = try from_dir.walk(allocator);
- defer walker.deinit();
- while (try walker.next()) |entry| {
- const dest: Path = .join(.{ to, entry.path });
- switch (entry.kind) {
- .directory => try std.fs.makeDirAbsoluteZ(dest.name()),
+ const res = std.posix.system.renameat(std.posix.AT.FDCWD, from.name(), std.posix.AT.FDCWD, to.name());
+ if (res == 0) return;
+
+ const err = result_to_error(res);
+ if (err != error.CrossDevice) return err;
+ try cp_file(from, to);
+ try rm(from);
+}
+
+fn cp_tree(from: *const Path, to: *const Path) !void {
+ var iter = try iterate(from);
+ defer iter.deinit();
+ while (try iter.next()) |entry| {
+ const from_child: Path = .join(.{ from.name(), entry.name });
+ const to_child: Path = .join(.{ to.name(), entry.name });
+ switch (entry.type) {
+ .dir => {
+ try mkdir(&to_child);
+ try cp_tree(&from_child, &to_child);
+ },
.file => {
- const src: Path = .join(.{ from, entry.path });
- try std.fs.copyFileAbsolute(src.name(), dest.name(), .{});
+ try cp_file(&from_child, &to_child);
+ },
+ else => {
+ // Ignore unsupported types.
},
- else => |k| fatal("cannot copy {s}", .{@tagName(k)}),
}
}
}
-pub const File_Iterator = struct {
- iter: std.fs.Dir.Iterator,
-
- pub fn deinit(self: *File_Iterator) void {
- self.iter.dir.close();
- }
+/// Check if a path is relative and does not break out its root.
+pub fn is_relative(path: []const u8) bool {
+ if (std.Io.Dir.path.isAbsolute(path)) return false;
- pub fn next(self: *File_Iterator) ?[]const u8 {
- while (true) {
- const entry = self.iter.next() catch unreachable orelse return null;
- if (entry.kind == .file) return entry.name;
+ var rest = path;
+ var depth: usize = 0;
+ while (rest.len > 0) {
+ var segment: []const u8 = undefined;
+ if (std.mem.findScalar(u8, rest, '/')) |sep| {
+ segment = rest[0..sep];
+ rest = rest[sep + 1 ..];
+ } else {
+ segment = rest;
+ rest = rest[rest.len..];
+ }
+ if (segment.len == 0 or (segment.len == 1 and segment[0] == '.')) continue;
+ if (segment.len == 2 and segment[0] == '.' and segment[1] == '.') {
+ if (depth == 0) return false;
+ depth -= 1;
+ } else {
+ depth += 1;
+ }
+ if (segment.len != 1 or segment[0] != '.') {
+ continue;
}
}
-};
-
-/// Iterate files of a directory with the given path. The path must be absolute.
-/// If the directory does not exist, null will be returned.
-pub fn iterate_files(path: Path) !?File_Iterator {
- var dir = std.fs.openDirAbsoluteZ(path.name(), .{ .iterate = true }) catch |err| {
- return switch (err) {
- error.FileNotFound => null,
- error.BadPathName, error.InvalidUtf8 => error.BadPathName,
- error.NameTooLong => unreachable,
- else => err,
- };
- };
- return .{
- .iter = dir.iterate(),
- };
+ return true;
}
pub const Dir_Info = struct {
@@ -195,18 +243,15 @@ pub const Dir_Info = struct {
empty: bool,
};
-pub fn dir_info(path: Path) !Dir_Info {
- var dir = std.fs.openDirAbsoluteZ(path.name(), .{ .iterate = true }) catch |err| {
+pub fn dir_info(path: *const Path) !Dir_Info {
+ var iter = iterate(path) catch |err| {
return switch (err) {
error.FileNotFound => .{ .exists = false, .empty = false },
- error.BadPathName, error.InvalidUtf8 => error.BadPathName,
- error.NameTooLong => unreachable,
else => err,
};
};
- defer dir.close();
+ defer iter.deinit();
- var iter = dir.iterate();
const child = iter.next() catch unreachable;
return .{
.exists = true,
@@ -214,44 +259,212 @@ pub fn dir_info(path: Path) !Dir_Info {
};
}
-pub fn file_exists(path: Path) !bool {
- std.fs.accessAbsoluteZ(path.name(), .{}) catch |err| {
- switch (err) {
- error.FileNotFound => return false,
- error.PermissionDenied => return false,
- error.NameTooLong => unreachable,
- else => return err,
- }
- };
- return true;
+pub fn path_exists(path: *const Path) bool {
+ return std.posix.system.access(path.name(), std.posix.R_OK) == 0;
}
/// Read the whole file into a buffer and returns the buffer. The caller is responsible
/// to deallocate the memory with the given allocator.
/// If the file does not exist, null will be returned.
-pub fn read_file(allocator: std.mem.Allocator, path: Path) !?[]const u8 {
- var f = std.fs.openFileAbsoluteZ(path.name(), .{}) catch |err| {
- return switch (err) {
- error.FileNotFound => null,
- error.NameTooLong => unreachable,
- else => err,
- };
- };
- defer f.close();
+pub fn read_file(allocator: std.mem.Allocator, path: *const Path) ![]u8 {
+ const res = std.posix.system.open(path.name(), .{});
+ try result_to_error(res);
+
+ const fd: std.posix.fd_t = @intCast(res);
+ defer _ = std.posix.system.close(fd);
- const stats = try f.stat();
- const buf = try allocator.alloc(u8, @intCast(stats.size));
+ var stat: std.posix.Stat = undefined;
+ if (std.posix.system.fstat(fd, &stat) == -1) {
+ return error.Unexpected;
+ }
+
+ const buf = try allocator.alloc(u8, @intCast(stat.size));
errdefer allocator.free(buf);
- var r = f.reader(buf);
- var off: usize = 0;
- while (off < buf.len) {
- const n = try r.readStreaming(buf[off..]);
- off += n;
+ var bytes_read: usize = 0;
+ while (bytes_read < buf.len) {
+ const rbuf = buf[bytes_read..];
+ const n = std.posix.system.read(fd, rbuf.ptr, rbuf.len);
+ result_to_error(n) catch |err| {
+ if (err == error.Interrupted) continue;
+ return err;
+ };
+ bytes_read += @intCast(n);
}
return buf;
}
+pub const Iterator = struct {
+ pub const Entry = struct {
+ name: []const u8,
+ type: enum { dir, file, unsupported },
+ };
+
+ fd: std.posix.fd_t,
+ buf: [4096]u8 = undefined,
+ len: usize = 0,
+ off: usize = 0,
+
+ pub fn deinit(self: *Iterator) void {
+ _ = std.posix.system.close(self.fd);
+ }
+
+ pub fn next(self: *Iterator) !?Entry {
+ while (true) {
+ if (self.off < self.len) {
+ const dirent = @as(*align(1) std.posix.system.dirent, @ptrCast(&self.buf[self.off]));
+ const name = @as([*]u8, @ptrCast(&dirent.name))[0..dirent.namlen];
+
+ self.off += @as(usize, @intCast(dirent.reclen));
+ if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) {
+ continue;
+ }
+
+ return .{
+ .name = name,
+ .type = switch (dirent.type) {
+ std.posix.DT.DIR => .dir,
+ std.posix.DT.REG => .file,
+ else => .unsupported,
+ },
+ };
+ }
+
+ var base: i64 = undefined;
+ const res = std.posix.system.getdirentries(self.fd, &self.buf, self.buf.len, &base);
+ try result_to_error(res);
+ if (res == 0) return null;
+ self.len = @intCast(res);
+ self.off = 0;
+ }
+ }
+};
+
+pub fn iterate(path: *const Path) !Iterator {
+ const res = std.posix.system.openat(std.posix.AT.FDCWD, path.name(), .{ .DIRECTORY = true });
+ try result_to_error(res);
+ return .{ .fd = @intCast(res) };
+}
+
+pub const File_Writer = struct {
+ fd: std.posix.fd_t,
+ interface: std.Io.Writer,
+
+ pub fn deinit(self: *File_Writer) void {
+ _ = std.posix.system.close(self.fd);
+ }
+
+ fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
+ const self: *File_Writer = @alignCast(@fieldParentPtr("interface", w));
+ var iovec: [8]std.posix.iovec_const = undefined;
+ var iovlen: usize = 0;
+ if (w.end > 0) {
+ iovec[iovlen] = .{ .base = w.buffer.ptr, .len = w.end };
+ iovlen += 1;
+ }
+ var off: usize = 0;
+ while (iovlen < iovec.len and off < data.len - 1) {
+ if (data[off].len > 0) {
+ iovec[iovlen] = .{ .base = data[off].ptr, .len = data[off].len };
+ iovlen += 1;
+ }
+ off += 1;
+ }
+ if (data[data.len - 1].len > 0) {
+ off = 0;
+ const buf = data[data.len - 1];
+ while (iovlen < iovec.len and off < splat) {
+ iovec[iovlen] = .{ .base = buf.ptr, .len = buf.len };
+ iovlen += 1;
+ off += 1;
+ }
+ }
+
+ if (iovlen == 0) return 0;
+ while (true) {
+ const res = std.posix.system.writev(self.fd, &iovec, @intCast(iovlen));
+ if (res < 0) {
+ switch (std.posix.errno(res)) {
+ .INTR => continue,
+ else => return error.WriteFailed,
+ }
+ }
+ return w.consume(@intCast(res));
+ }
+ }
+};
+
+pub fn file_writer(path: *const Path, buf: []u8, opts: struct { create: bool = false }) !File_Writer {
+ const res = std.posix.system.openat(std.posix.AT.FDCWD, path.name(), .{ .ACCMODE = .WRONLY, .CREAT = opts.create }, default_file_mode);
+ try result_to_error(res);
+ return .{
+ .fd = @intCast(res),
+ .interface = .{
+ .vtable = &.{ .drain = File_Writer.drain },
+ .buffer = buf,
+ },
+ };
+}
+
+pub const File_Reader = struct {
+ fd: std.posix.fd_t,
+ interface: std.Io.Reader,
+
+ pub fn deinit(self: *File_Reader) void {
+ _ = std.posix.system.close(self.fd);
+ }
+
+ fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize {
+ const self: *File_Reader = @alignCast(@fieldParentPtr("interface", r));
+ const buf = limit.slice(try w.writableSliceGreedy(1));
+ const n: usize = while (true) {
+ const res = std.posix.system.read(self.fd, buf.ptr, buf.len);
+ if (res < 0) {
+ switch (std.posix.errno(res)) {
+ .SUCCESS => unreachable,
+ .INTR => continue,
+ else => return error.ReadFailed,
+ }
+ }
+ if (res == 0) return error.EndOfStream;
+ break @intCast(res);
+ };
+ w.advance(n);
+ return n;
+ }
+};
+
+pub fn file_reader(path: *const Path, buf: []u8) !File_Reader {
+ const res = std.posix.system.openat(std.posix.AT.FDCWD, path.name(), .{ .ACCMODE = .RDONLY });
+ try result_to_error(res);
+ return .{
+ .fd = @intCast(res),
+ .interface = .{
+ .vtable = &.{ .stream = File_Reader.stream },
+ .buffer = buf,
+ .seek = 0,
+ .end = 0,
+ },
+ };
+}
+
+fn result_to_error(res: anytype) !void {
+ return switch (std.posix.system.errno(res)) {
+ .SUCCESS => {},
+ .NAMETOOLONG => unreachable,
+ .NOENT => error.FileNotFound,
+ .EXIST => error.FileAlreadyExists,
+ .ACCES => error.AccessDenied,
+ .LOOP => error.TooManySymlinks,
+ .MFILE, .NFILE => error.TooManyFiles,
+ .ISDIR => error.IsDir,
+ .NOTDIR => error.NotDir,
+ .INTR => error.Interrupted,
+ .XDEV => error.CrossDevice,
+ else => error.Unexpected,
+ };
+}
+
test "path join" {
var path: Path = undefined;
@@ -267,3 +480,14 @@ test "path join" {
path = .join(.{ "/absolute", "local/dir" });
try std.testing.expectEqualStrings("/absolute/local/dir", path.name());
}
+
+test "is relative" {
+ try std.testing.expectEqual(true, is_relative("."));
+ try std.testing.expectEqual(false, is_relative(".."));
+ try std.testing.expectEqual(true, is_relative("foo/.."));
+ try std.testing.expectEqual(true, is_relative("foo/../."));
+ try std.testing.expectEqual(true, is_relative("foo/../.bar/./.."));
+ try std.testing.expectEqual(true, is_relative("./.foo/"));
+ try std.testing.expectEqual(false, is_relative("./foo/../.."));
+ try std.testing.expectEqual(false, is_relative("/.foo/"));
+}