//! Check-in comments written to an unlinked temp file as a derive links
//! them, then mapped read-only. A long history's comments are the largest
//! thing a full derive holds -- about half its heap on the Linux kernel --
//! and nothing in the walk reads them back, so they can live in file pages
//! the kernel may drop instead of heap it may not.
//!
//! The file is created in the cache directory and unlinked at once, so a
//! crashed process leaves nothing behind and two derives never share one.
const std = @import("std");
/// Where one appended text lives in the spill.
pub const Span = struct {
off: u64,
len: u32,
};
pub const TextSpill = struct {
io: std.Io,
file: std.Io.File,
len: u64 = 0,
map: ?std.Io.File.MemoryMap = null,
done: bool = false,
/// A spill in `dir`'s subdirectory `sub`, or null when none can be made
/// there -- a read-only store, say. The caller then keeps text on the heap.
pub fn create(io: std.Io, dir: std.Io.Dir, sub: []const u8) ?TextSpill {
var nonce: [8]u8 = undefined;
io.random(&nonce);
var name_buf: [std.fs.max_path_bytes]u8 = undefined;
const name = std.fmt.bufPrint(&name_buf, "{s}/text-{x}", .{ sub, &nonce }) catch return null;
const file = dir.createFile(io, name, .{ .read = true, .exclusive = true }) catch return null;
dir.deleteFile(io, name) catch {
file.close(io);
dir.deleteFile(io, name) catch {};
return null;
};
return .{ .io = io, .file = file };
}
pub fn deinit(self: *TextSpill) void {
if (self.map) |*m| m.destroy(self.io);
self.file.close(self.io);
self.* = undefined;
}
/// Append `bytes`; null when the write fails, and the caller keeps that
/// text itself. Only before `seal`.
pub fn append(self: *TextSpill, bytes: []const u8) ?Span {
std.debug.assert(!self.done);
const len = std.math.cast(u32, bytes.len) orelse return null;
const at = self.len;
if (len != 0) {
self.file.writePositionalAll(self.io, bytes, at) catch return null;
}
self.len += len;
return .{ .off = at, .len = len };
}
/// Map what was appended, so `get` can answer. False when the map fails;
/// spans then cannot be read and the caller must not have relied on them.
pub fn seal(self: *TextSpill) bool {
std.debug.assert(!self.done);
if (self.len != 0) {
const len = std.math.cast(usize, self.len) orelse return false;
self.map = self.file.createMemoryMap(self.io, .{
.len = len,
.protection = .{ .read = true },
.populate = false,
}) catch return false;
}
self.done = true;
return true;
}
pub fn sealed(self: *const TextSpill) bool {
return self.done;
}
/// The text at `span`. Only after a successful `seal`.
pub fn get(self: *const TextSpill, span: Span) []const u8 {
if (span.len == 0) return "";
const m = self.map.?;
const off: usize = @intCast(span.off);
return m.memory[off..][0..span.len];
}
};
test "text spill: appended texts read back after seal, and the file is already unlinked" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.createDir(io, "c", .default_dir);
var s = TextSpill.create(io, tmp.dir, "c") orelse return error.TestCreateFailed;
defer s.deinit();
const a = s.append("first comment").?;
const b = s.append("").?;
const c = s.append("third").?;
try std.testing.expect(!s.sealed());
try std.testing.expect(s.seal());
try std.testing.expectEqualStrings("first comment", s.get(a));
try std.testing.expectEqualStrings("", s.get(b));
try std.testing.expectEqualStrings("third", s.get(c));
var dir = try tmp.dir.openDir(io, "c", .{ .iterate = true });
defer dir.close(io);
var it = dir.iterate();
try std.testing.expect((try it.next(io)) == null);
}
test "text spill: nowhere to write is null, not an error" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try std.testing.expect(TextSpill.create(io, tmp.dir, "missing") == null);
}