const std = @import("std"); const c = @cImport({ @cInclude("stdlib.h"); @cInclude("time.h"); }); const exe = @import("exe.zig"); const log = std.log.scoped(.fs); pub const FileSwapper = struct { allocator: std.mem.Allocator, target_path: []const u8, tmp_path: []const u8, tmp: std.fs.File, pub fn createAt(alloc: std.mem.Allocator, path: []const u8) !FileSwapper { const target_dir = std.fs.path.dirname(path) orelse "."; const target_name = std.fs.path.basename(path); const tmp_name = try std.fmt.allocPrint( alloc, ".{s}", .{target_name}, ); defer alloc.free(tmp_name); const tmp_path = try std.fs.path.join( alloc, &.{ target_dir, tmp_name }, ); const f = try std.fs.cwd().createFile(tmp_path, .{}); return .{ .allocator = alloc, .target_path = path, .tmp_path = tmp_path, .tmp = f, }; } pub fn file(self: FileSwapper) std.fs.File { return self.tmp; } pub fn writer(self: FileSwapper, buffer: []u8) std.fs.File.Writer { return self.tmp.writer(buffer); } pub fn diff(self: FileSwapper) !bool { var target_found = true; std.fs.cwd().access(self.target_path, .{}) catch |e| switch (e) { error.FileNotFound => target_found = false, else => |err| return err, }; // exit early if target_path was not found if (!target_found) return error.TargetNotFound; var p = std.process.Child.init(&.{ "diff", "-q", self.tmp_path, self.target_path, }, std.heap.page_allocator); try p.spawn(); switch (try p.wait()) { .Exited => |exit_code| switch (exit_code) { 0 => return false, 1 => return true, else => return error.UnknownDiffError, }, else => return error.UnknownDiffError, } } pub fn swap(self: FileSwapper) !void { std.log.debug("swapping {s} -> {s}", .{ self.tmp_path, self.target_path }); // flush writes self.tmp.sync() catch @panic("sync tmp file"); // swap file into final location // NOTE: the tmp fd is still valid after this try std.fs.rename( std.fs.cwd(), self.tmp_path, std.fs.cwd(), self.target_path, ); } pub fn undo(self: FileSwapper) !void { try std.posix.unlink(self.tmp_path); } pub fn close(self: FileSwapper) void { var found = true; std.fs.cwd().access(self.tmp_path, .{}) catch |e| switch (e) { error.FileNotFound => found = false, else => @panic("access tmp path"), }; if (found) self.swap() catch @panic("failed to swap target file"); self.allocator.free(self.tmp_path); self.tmp.close(); } }; pub fn replaceExtension(alloc: std.mem.Allocator, p: []const u8, new: []const u8) ![]const u8 { const dirname = std.fs.path.dirname(p) orelse "."; const basename = try stripExtension(std.fs.path.basename(p)); return try joinPathAndFilename( alloc, &.{dirname}, &.{ basename, ".", new }, ); } // also trims off base paths pub fn stripExtension(path: []const u8) ![]const u8 { const ext = std.fs.path.extension(path); const ext_idx = std.mem.indexOfPosLinear(u8, path, 0, ext); std.debug.assert(ext_idx != null); return path[0..ext_idx.?]; } pub fn joinPathAndFilename(alloc: std.mem.Allocator, paths: []const []const u8, filename_parts: []const []const u8) ![]const u8 { var buf: [std.os.linux.PATH_MAX]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&buf); const filename = try std.mem.concat(fba.allocator(), u8, filename_parts); var p = try std.ArrayList([]const u8).initCapacity(fba.allocator(), paths.len + 1); p.appendSliceAssumeCapacity(paths); p.appendAssumeCapacity(filename); std.debug.assert(paths.len + 1 == p.items.len); return try std.fs.path.join(alloc, p.items); } pub fn getGitTime(path: []const u8) !std.posix.timespec { var buf = std.mem.zeroes([12]u8); var p = std.process.Child.init( &.{ "git", "log", "-1", "--pretty=format:%at", "-z", "--", path }, std.heap.page_allocator, ); p.stdout_behavior = .Pipe; try p.spawn(); _ = try p.stdout.?.readAll(&buf); const result = try p.wait(); std.debug.assert(result == .Exited and result.Exited == 0); // parse result as ulong const gitAuthorDate: c_ulong = c.strtoul(&buf, null, 10); if (gitAuthorDate == 0) return error.NoGitTime; // calculate timespec return .{ .sec = std.math.cast(isize, gitAuthorDate) orelse std.math.maxInt(isize), .nsec = 0, }; } pub fn getMtime(path: []const u8) !std.posix.timespec { const fd = try std.fs.cwd().openFile(path, .{ .mode = .read_only }); defer fd.close(); const meta = try fd.stat(); const mtime = meta.mtime; return .{ .sec = std.math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse std.math.maxInt(isize), .nsec = std.math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse std.math.maxInt(isize), }; } pub fn setMtime(path: []const u8, ts: std.posix.timespec) !void { const fd = try std.fs.cwd().openFile(path, .{ .mode = .write_only }); defer fd.close(); try std.posix.futimens( fd.handle, &[2]std.os.linux.timespec{ ts, ts }, ); } // use libc strftime to format time string pub fn formatTimespec(alloc: std.mem.Allocator, timespec: std.posix.timespec, fmt: [*c]const u8) ![]const u8 { const mtime_str = try alloc.alloc(u8, std.os.linux.PATH_MAX); var tm: c.struct_tm = undefined; _ = c.localtime_r(×pec.sec, &tm); const rc = c.strftime( mtime_str.ptr, mtime_str.len, fmt, &tm, ); switch (std.posix.errno(rc)) { .SUCCESS => {}, else => |err| return std.posix.unexpectedErrno(err), } return try alloc.realloc(mtime_str, rc); }