Skip to content
zit Artifact [92f664802c]
Login

Artifact [92f664802c]

Artifact 92f664802ca91619e0302a20be767aaec87084587ab6190e6e7ef0fffcdbf62d:


//! 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);
}