From 44e5ad763794a438ecfd50c8b7f6ea760ea82da5 Mon Sep 17 00:00:00 2001 From: tsne Date: Wed, 1 Jan 2025 14:58:28 +0100 Subject: initial commit --- src/fs.zig | 269 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 src/fs.zig (limited to 'src/fs.zig') diff --git a/src/fs.zig b/src/fs.zig new file mode 100644 index 0000000..f1bee2d --- /dev/null +++ b/src/fs.zig @@ -0,0 +1,269 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const fatal = @import("cli.zig").fatal; + +pub const Path = struct { + buf: [std.fs.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] == std.fs.path.sep) seg = seg[1..]; + if (seg[seg.len - 1] == std.fs.path.sep) seg = seg[0 .. seg.len - 1]; + } + if (seg.len > 0) { + if (self.len > 0) { + self.buf[self.len] = std.fs.path.sep; + 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 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); + } + + /// 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())); + 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..]; + return rel; + } +}; + +pub fn mkdir_all(path: Path) !void { + try std.fs.cwd().makePath(path.name()); +} + +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 rm(path: Path) !void { + std.fs.deleteTreeAbsolute(path.name()) catch |err| { + if (err != error.FileNotFound) return err; + }; +} + +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, .{}); +} + +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; + }; + try mkdir_all(to_path); + try cp_tree(allocator, from, to); +} + +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); + + 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); + }; +} + +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); + + std.fs.renameAbsoluteZ(from, to) catch |err| { + if (err != error.RenameAcrossMountPoints) return err; + try std.fs.copyFileAbsolute(from, to, .{}); + try rm(from_path); + }; +} + +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(); + + 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()), + .file => { + const src: Path = .join(.{ from, entry.path }); + try std.fs.copyFileAbsolute(src.name(), dest.name(), .{}); + }, + 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(); + } + + 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; + } + } +}; + +/// 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(), + }; +} + +pub const Dir_Info = struct { + exists: bool, + empty: bool, +}; + +pub fn dir_info(path: Path) !Dir_Info { + var dir = std.fs.openDirAbsoluteZ(path.name(), .{ .iterate = true }) 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(); + + var iter = dir.iterate(); + const child = iter.next() catch unreachable; + return .{ + .exists = true, + .empty = child == null, + }; +} + +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; +} + +/// 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(); + + const stats = try f.stat(); + const buf = try allocator.alloc(u8, @intCast(stats.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; + } + return buf; +} + +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()); +} -- cgit v1.2.3