diff options
Diffstat (limited to 'src/fs.zig')
| -rw-r--r-- | src/fs.zig | 500 |
1 files changed, 362 insertions, 138 deletions
@@ -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/")); +} |