const std = @import("std"); 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: [max_path_bytes]u8, len: usize, pub fn init(path: []const u8) Path { var self: Path = undefined; assert(path.len < self.buf.len); self.len = path.len; @memcpy(self.buf[0..path.len], path); self.buf[self.len] = 0; return self; } pub fn join(segments: anytype) Path { var self: Path = undefined; self.len = 0; self.append(segments); return self; } pub fn append(self: *Path, segments: anytype) void { const info = @typeInfo(@TypeOf(segments)).@"struct"; comptime assert(info.is_tuple); inline for (info.fields) |field| { var seg: []const u8 = @field(segments, field.name); if (seg.len > 0) { 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] = '/'; self.len += 1; } @memcpy(self.buf[self.len .. self.len + seg.len], seg); self.len += seg.len; } } self.buf[self.len] = 0; } pub fn name(self: *const Path) [:0]const u8 { return self.buf[0..self.len :0]; } pub fn dir(self: *const Path) ?Path { 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: *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] == '/') rel = rel[1..]; return rel; } }; 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 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 touch(path: *const Path) !void { if (path.dir()) |dir| try mkdir_all(&dir); 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 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, } }; 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 cp_file(from: *const Path, to: *const Path) !void { try rm(to); if (to.dir()) |to_parent| try mkdir_all(&to_parent); 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_dir(from: *const Path, to: *const Path) !void { try rm(to); if (to.dir()) |to_parent| try mkdir_all(&to_parent); 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); } pub fn mv_file(from: *const Path, to: *const Path) !void { try rm(to); if (to.dir()) |to_parent| try mkdir_all(&to_parent); 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 => { try cp_file(&from_child, &to_child); }, else => { // Ignore unsupported types. }, } } } /// 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; 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; } } return true; } pub const Dir_Info = struct { exists: bool, empty: bool, }; pub fn dir_info(path: *const Path) !Dir_Info { var iter = iterate(path) catch |err| { return switch (err) { error.FileNotFound => .{ .exists = false, .empty = false }, else => err, }; }; defer iter.deinit(); const child = iter.next() catch unreachable; return .{ .exists = true, .empty = child == null, }; } 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: *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); 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 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; path = .join(.{}); try std.testing.expectEqualStrings("", path.name()); path = .join(.{ "relative", "local/dir" }); try std.testing.expectEqualStrings("relative/local/dir", path.name()); path = .join(.{ "relative/", "/local/dir" }); try std.testing.expectEqualStrings("relative/local/dir", path.name()); 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/")); }