//! Run log owned by `spec/PRODUCT.md` section 11 (`TJ-LOG-01` through //! `TJ-LOG-06`): an append-only sequence of framed UTF-8 JSON records, //! one line each: //! //! ```text //! \n //! ``` //! //! Creation failure enters review-only mode. Write or flush failure before //! mutation admission blocks the action; failure after mutation pauses //! execution and disables further mutation. Authority fields never truncate. const std = @import("std"); const spec_data = @import("spec_data"); pub const payload_bytes_max: u32 = @intCast(spec_data.limit_value.log_payload_bytes); pub const escaped_value_bytes_max: u32 = @intCast(spec_data.limit_value.escaped_log_value_bytes); pub const write_attempts_max: u32 = @intCast(spec_data.limit_value.log_write_attempts); pub const eintr_retries_max: u32 = @intCast(spec_data.limit_value.log_eintr_retries); pub const audit_events_per_action_max: u32 = @intCast(spec_data.limit_value.audit_events_per_action); /// CRC-32/ISO-HDLC (the standard reflected CRC-32, polynomial 0xEDB88320), /// matching the frame format required by `TJ-LOG-04`. pub fn crc32(bytes: []const u8) u32 { const table = comptime blk: { @setEvalBranchQuota(100_000); var computed: [256]u32 = undefined; for (0..256) |index| { var value: u32 = @intCast(index); for (0..8) |_| { const masked = value & 1 != 0; value >>= 1; if (masked) value ^= 0xEDB88320; } computed[index] = value; } break :blk computed; }; var crc: u32 = 0xFFFFFFFF; for (bytes) |byte| { crc = (crc >> 8) ^ table[(crc ^ byte) & 0xFF]; } return ~crc; } pub const Frame = struct { bytes: [payload_bytes_max + 32]u8 = undefined, len: usize = 0, }; /// Renders one complete frame (` \n`) into `frame`. /// Returns null when the payload exceeds the registered bound. pub fn renderFrame(payload: []const u8, frame: *Frame) bool { if (payload.len == 0 or payload.len > payload_bytes_max) return false; const header = std.fmt.bufPrint(&frame.bytes, "{d} {x:0>8} ", .{ payload.len, crc32(payload), }) catch return false; if (header.len + payload.len + 1 > frame.bytes.len) return false; @memcpy(frame.bytes[header.len..][0..payload.len], payload); frame.bytes[header.len + payload.len] = '\n'; frame.len = header.len + payload.len + 1; return true; } /// One open, append-only run log. Exactly one descriptor stays open for the /// whole run; the log is never rotated or replaced during that run. pub const RunLog = struct { file: std.Io.File = undefined, opened: bool = false, sequence: u64 = 0, /// Set when creation, open, or the initial flush failed: review-only /// mode. Every mutation is unavailable and a warning stays persistent. broken: bool = false, /// Set when a partial frame exists or a post-mutation write failed: /// the audit log is incomplete and mutation is disabled for the run. audit_incomplete: bool = false, /// Opens or creates `path` with append-only, no-follow, close-on-exec /// semantics and flushes the file and its parent on creation /// (`TJ-LOG-01`). Any failure returns a broken log in review-only mode. pub fn open(path: []const u8) RunLog { var log = RunLog{ .broken = true }; const flags: std.posix.O = .{ .ACCMODE = .WRONLY, .CREAT = true, .APPEND = true, .NOFOLLOW = true, .CLOEXEC = true, }; const fd = std.posix.openat(std.posix.AT.FDCWD, path, flags, 0o600) catch return log; log.file = .{ .handle = fd, .flags = .{ .nonblocking = false } }; log.opened = true; log.flushRaw() catch { log.broken = true; log.audit_incomplete = true; _ = std.os.linux.close(fd); log.opened = false; return log; }; // Flush the parent directory so a freshly created log file entry // survives a crash before the first frame is written. if (std.mem.lastIndexOfScalar(u8, path, '/')) |slash| { const parent_fd = std.posix.openat( std.posix.AT.FDCWD, path[0..slash], .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true }, 0, ) catch { log.broken = true; log.audit_incomplete = true; _ = std.os.linux.close(fd); log.opened = false; return log; }; defer _ = std.os.linux.close(parent_fd); _ = std.posix.system.fdatasync(parent_fd); } log.broken = false; return log; } /// Appends one complete frame and flushes it durably. Returns false when /// the frame could not be written completely; the caller then fails /// closed (`TJ-LOG-06`). A partial frame marks the audit log incomplete /// and disables mutation for the rest of the run. pub fn append(self: *RunLog, frame: *const Frame) bool { std.debug.assert(frame.len > 0); if (self.broken) return false; var written: usize = 0; var retries: u32 = 0; while (written < frame.len) { const rc = std.posix.system.write( self.file.handle, frame.bytes[written..].ptr, frame.len - written, ); if (rc < 0) { if (std.posix.errno(rc) == .INTR) { if (retries >= eintr_retries_max) { if (written > 0) { self.audit_incomplete = true; self.broken = true; } return false; } retries += 1; continue; } if (written > 0) { self.audit_incomplete = true; self.broken = true; } return false; } const sent: usize = @intCast(rc); std.debug.assert(sent > 0); written += sent; } self.sequence += 1; self.flushRaw() catch { self.audit_incomplete = true; return false; }; return true; } fn flushRaw(self: *RunLog) !void { var attempts: u32 = 0; while (attempts < write_attempts_max) : (attempts += 1) { const rc = std.posix.system.fdatasync(self.file.handle); if (rc == 0) return; const err = std.posix.errno(rc); if (err == .INTR) continue; if (err == .OPNOTSUPP or err == .INVAL) { // `fsync` is accepted only when `fdatasync` is unsupported // and succeeds (`TJ-LOG-03`). const rc2 = std.posix.system.fsync(self.file.handle); if (rc2 == 0) return; if (std.posix.errno(rc2) == .INTR) continue; return error.FlushFailed; } return error.FlushFailed; } return error.FlushFailed; } pub fn close(self: *RunLog) void { if (!self.opened) return; _ = std.os.linux.close(self.file.handle); self.opened = false; } }; /// Losslessly escapes one JSON string value into `out`; returns null when /// the escaped form does not fit, which the caller must treat as an /// authority overflow rather than truncating. pub fn escapeInto(value: []const u8, out: []u8) ?[]const u8 { var written: usize = 0; for (value) |byte| { switch (byte) { '"' => { if (written + 2 > out.len) return null; @memcpy(out[written..][0..2], "\\\""); written += 2; }, '\\' => { if (written + 2 > out.len) return null; @memcpy(out[written..][0..2], "\\\\"); written += 2; }, '\n' => { if (written + 2 > out.len) return null; @memcpy(out[written..][0..2], "\\n"); written += 2; }, '\r' => { if (written + 2 > out.len) return null; @memcpy(out[written..][0..2], "\\r"); written += 2; }, '\t' => { if (written + 2 > out.len) return null; @memcpy(out[written..][0..2], "\\t"); written += 2; }, else => { if (byte < 0x20) { const text = std.fmt.bufPrint(out[written..], "\\u{x:0>4}", .{byte}) catch return null; written += text.len; } else { if (written + 1 > out.len) return null; out[written] = byte; written += 1; } }, } } return out[0..written]; } comptime { // ISO-HDLC check values verify the frame checksum at compile time. std.debug.assert(crc32("123456789") == 0xCBF43926); std.debug.assert(crc32("") == 0x00000000); // Escape rendering round-trips the bounded format. var escape_out: [64]u8 = undefined; std.debug.assert(escapeInto("ab", &escape_out) != null); }