diff --git a/src/cli/dump.zig b/src/cli/dump.zig index c86fa45..0880797 100644 --- a/src/cli/dump.zig +++ b/src/cli/dump.zig @@ -405,7 +405,7 @@ fn formatTimestamp(buf: *[23]u8, epoch_secs: u64) []const u8 { const ds = es.getDaySeconds(); const result = std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.000", .{ yd.year, - @as(u32, @intFromEnum(md.month)) + 1, + @as(u32, @intFromEnum(md.month)), @as(u32, md.day_index) + 1, ds.getHoursIntoDay(), ds.getMinutesIntoHour(), @@ -918,6 +918,13 @@ test "dump: parsePeriod valid durations" { try testing.expectEqual(@as(u64, 86400 * 30), parsePeriod("30d").?); } +test "dump: formatTimestamp month is 1-based (June prints 06 not 07)" { + var buf: [23]u8 = undefined; + // 1717200000 == 2024-06-01T00:00:00 UTC. + const out = formatTimestamp(&buf, 1717200000); + try testing.expectEqualStrings("2024-06-01T00:00:00.000", out); +} + test "dump: parsePeriod rejects invalid" { try testing.expectEqual(@as(?u64, null), parsePeriod("0m")); try testing.expectEqual(@as(?u64, null), parsePeriod("abc")); diff --git a/src/cli/install/plan.zig b/src/cli/install/plan.zig index a7170b1..7511b50 100644 --- a/src/cli/install/plan.zig +++ b/src/cli/install/plan.zig @@ -100,8 +100,12 @@ pub fn dirIsNonEmpty(path: []const u8) bool { } // Atomic copy (temp + rename) preserving source mode. No partial dst on failure. +// copyFileAbsolute creates dst via open(O_CREAT) subject to umask, so the source +// permission bits are re-applied explicitly to keep the result deterministic. pub fn copyFile(src: []const u8, dst: []const u8) !void { + const src_stat = try std.fs.cwd().statFile(src); try std.fs.copyFileAbsolute(src, dst, .{}); + try std.posix.fchmodat(std.posix.AT.FDCWD, dst, @intCast(src_stat.mode & 0o777), 0); } // Write to {dst}.new then rename(2) over dst — avoids ETXTBSY when dst is currently executing. diff --git a/src/cli/install/tests.zig b/src/cli/install/tests.zig index 386e44e..8a7f583 100644 --- a/src/cli/install/tests.zig +++ b/src/cli/install/tests.zig @@ -26,6 +26,7 @@ const parseYesNoDefaultYes = plan_mod.parseYesNoDefaultYes; const planSystemctlUser = plan_mod.planSystemctlUser; const installWillStartUserService = plan_mod.installWillStartUserService; const atomicInstallBinary = plan_mod.atomicInstallBinary; +const copyFile = plan_mod.copyFile; const userInGroup = plan_mod.userInGroup; const ensureDirAll = plan_mod.ensureDirAll; const SystemctlUserMode = plan_mod.SystemctlUserMode; @@ -2716,6 +2717,36 @@ test "install: atomicInstallBinary replaces destination atomically" { try testing.expectEqual(@as(u32, 0o755), stat.mode & 0o777); } +test "install: copyFile preserves source mode regardless of umask" { + const testing = std.testing; + const allocator = testing.allocator; + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + const dir = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(dir); + + const src_path = try std.fmt.allocPrint(allocator, "{s}/src.toml", .{dir}); + defer allocator.free(src_path); + const dst_path = try std.fmt.allocPrint(allocator, "{s}/dst.toml", .{dir}); + defer allocator.free(dst_path); + + { + var f = try std.fs.createFileAbsolute(src_path, .{ .mode = 0o644 }); + defer f.close(); + try f.writeAll("name = \"x\"\n"); + } + try std.posix.fchmodat(std.posix.AT.FDCWD, src_path, 0o644, 0); + + // Restrictive umask would mask 0o644 down to 0o600 without an explicit chmod. + const prev_umask = std.os.linux.syscall1(.umask, 0o077); + defer _ = std.os.linux.syscall1(.umask, prev_umask); + + try copyFile(src_path, dst_path); + + const stat = try std.fs.cwd().statFile(dst_path); + try testing.expectEqual(@as(u32, 0o644), stat.mode & 0o777); +} + test "install: atomicInstallBinary rename succeeds while dst has open readers" { // Verifies rename(2) over an open read fd succeeds — regression lock for the atomic-rename path. const testing = std.testing; diff --git a/src/event_loop.zig b/src/event_loop.zig index 2add44b..bf742a8 100644 --- a/src/event_loop.zig +++ b/src/event_loop.zig @@ -114,7 +114,7 @@ fn armRumbleStopFd(fd: posix.fd_t, deadline_ns: ?i128) void { }; const rc = linux.timerfd_settime(fd, .{ .ABSTIME = true }, &spec, null); if (rc != 0) { - const errno = std.posix.errno(rc); + const errno = linux.E.init(rc); rumble_log.debug("TIMERFD: timerfd_settime FAILED errno={s} deadline={d}", .{ @tagName(errno), target, }); diff --git a/src/io/hidraw.zig b/src/io/hidraw.zig index 9dd9fe0..926ec6f 100644 --- a/src/io/hidraw.zig +++ b/src/io/hidraw.zig @@ -139,6 +139,7 @@ pub const HidrawDevice = struct { if (dev_vid != vid or dev_pid != pid) continue; const owned = try allocator.dupe(u8, path); + errdefer allocator.free(owned); try paths.append(allocator, owned); } diff --git a/src/log.zig b/src/log.zig index 951b870..dab9ef0 100644 --- a/src/log.zig +++ b/src/log.zig @@ -319,7 +319,7 @@ fn wallClockTimestamp(buf: *[32]u8) []const u8 { const result = std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}", .{ yd.year, - @as(u32, @intFromEnum(md.month)) + 1, + @as(u32, @intFromEnum(md.month)), @as(u32, md.day_index) + 1, ds.getHoursIntoDay(), ds.getMinutesIntoHour(), diff --git a/src/supervisor.zig b/src/supervisor.zig index 421b8f5..73674ca 100644 --- a/src/supervisor.zig +++ b/src/supervisor.zig @@ -1801,13 +1801,16 @@ pub const Supervisor = struct { return; }; parsed_ptr.* = parsed; - const new_mapper = Mapper.init(&parsed_ptr.value, m.instance.loop.macro_timer_fd, self.allocator) catch { + var new_mapper = Mapper.init(&parsed_ptr.value, m.instance.loop.macro_timer_fd, self.allocator) catch { parsed_ptr.deinit(); self.allocator.destroy(parsed_ptr); cs.sendResponse(fd, "ERR switch-failed\n"); return; }; const stem_copy = self.allocator.dupe(u8, resolved_stem) catch { + new_mapper.deinit(); + parsed_ptr.deinit(); + self.allocator.destroy(parsed_ptr); cs.sendResponse(fd, "ERR oom\n"); return; }; @@ -1818,6 +1821,9 @@ pub const Supervisor = struct { .path_stem = stem_copy, }) catch { self.allocator.free(stem_copy); + new_mapper.deinit(); + parsed_ptr.deinit(); + self.allocator.destroy(parsed_ptr); cs.sendResponse(fd, "ERR oom\n"); return; };