Skip to content
zit Artifact [e4dc055837]
Login

Artifact [e4dc055837]

Artifact e4dc055837d3bd111cc6d193b362f7ed7ef79e9b6d4fdde281af87add8a67ecc:


//! An `std.Io` whose file reads and writes each move at most `max_chunk`
//! bytes, passing everything else through to the `Io` it wraps.
//!
//! Darwin's read, write, pread, pwrite and their vectored forms refuse a
//! request over `INT_MAX` bytes with EINVAL, and Zig 0.17's threaded `Io`
//! hands its slices to them unclamped, so a single `readPositionalAll` of a
//! cache file past 2 GiB fails (debug builds panic). The callers already loop
//! on short reads and writes, so taking at most `max_chunk` per call is
//! invisible to them. Linux clamps on its own and is unaffected.

const std = @import("std");
const Io = std.Io;

/// Under Darwin's `INT_MAX` limit with room to spare, and far above any
/// buffer a single call usefully moves.
pub const max_chunk: usize = 1 << 30;

/// The wrapped `Io`'s vtable. One per process, set by `wrap`, because the
/// wrapped entries receive the inner `Io`'s userdata unchanged and so cannot
/// carry a pointer of their own.
var inner_vtable: ?*const Io.VTable = null;
var vtable: Io.VTable = undefined;

/// `inner` with every file read and write clamped to `max_chunk` bytes per
/// call. Call once per process.
pub fn wrap(inner: Io) Io {
    std.debug.assert(inner_vtable == null);
    inner_vtable = inner.vtable;
    vtable = inner.vtable.*;
    vtable.fileReadPositional = fileReadPositional;
    vtable.fileWritePositional = fileWritePositional;
    vtable.operate = operate;
    return .{ .userdata = inner.userdata, .vtable = &vtable };
}

fn fileReadPositional(ud: ?*anyopaque, file: Io.File, data: []const []u8, offset: u64) Io.File.ReadPositionalError!usize {
    var one: [1][]u8 = undefined;
    return inner_vtable.?.fileReadPositional(ud, file, clampRead(data, &one, max_chunk), offset);
}

fn fileWritePositional(ud: ?*anyopaque, file: Io.File, header: []const u8, data: []const []const u8, splat: usize, offset: u64) Io.File.WritePositionalError!usize {
    const w: ClampedWrite = .init(header, data, splat, max_chunk);
    return inner_vtable.?.fileWritePositional(ud, file, w.header, w.data, w.splat, offset);
}

fn operate(ud: ?*anyopaque, op: Io.Operation) Io.Cancelable!Io.Operation.Result {
    var one: [1][]u8 = undefined;
    switch (op) {
        .file_read_streaming => |r| {
            var clamped = r;
            clamped.data = clampRead(r.data, &one, max_chunk);
            return inner_vtable.?.operate(ud, .{ .file_read_streaming = clamped });
        },
        .file_write_streaming => |r| {
            const w: ClampedWrite = .init(r.header, r.data, r.splat, max_chunk);
            var clamped = r;
            clamped.header = w.header;
            clamped.data = w.data;
            clamped.splat = w.splat;
            return inner_vtable.?.operate(ud, .{ .file_write_streaming = clamped });
        },
        else => return inner_vtable.?.operate(ud, op),
    }
}

/// `data` itself when it totals at most `limit` bytes, else its first
/// non-empty slice cut to `limit`, held in `one`. A read may always return
/// fewer bytes than asked.
fn clampRead(data: []const []u8, one: *[1][]u8, limit: usize) []const []u8 {
    var total: usize = 0;
    for (data) |d| total +|= d.len;
    if (total <= limit) return data;
    for (data) |d| if (d.len != 0) {
        one[0] = d[0..@min(d.len, limit)];
        return one;
    };
    unreachable;
}

const no_data = [_][]const u8{""};

/// A write's header, data and splat, cut to at most `limit` bytes when they
/// would move more. The cut keeps a prefix of the bytes in order (header,
/// then data, the last element `splat` times), written as the header alone,
/// so the count the write returns is a valid count of the original.
const ClampedWrite = struct {
    header: []const u8,
    data: []const []const u8,
    splat: usize,

    fn init(header: []const u8, data: []const []const u8, splat: usize, limit: usize) ClampedWrite {
        if (total(header, data, splat) <= limit) return .{ .header = header, .data = data, .splat = splat };
        const first: []const u8 = if (header.len != 0) header else for (data, 0..) |d, i| {
            if (d.len == 0) continue;
            if (i + 1 == data.len and splat == 0) continue;
            break d;
        } else unreachable;
        return .{ .header = first[0..@min(first.len, limit)], .data = &no_data, .splat = 1 };
    }

    fn total(header: []const u8, data: []const []const u8, splat: usize) usize {
        var sum: usize = header.len;
        if (data.len == 0) return sum;
        for (data[0 .. data.len - 1]) |d| sum +|= d.len;
        return sum +| (data[data.len - 1].len *| splat);
    }
};

test "clampRead: under the limit passes through, past it takes the first slice's prefix" {
    var a: [4]u8 = undefined;
    var b: [4]u8 = undefined;
    var one: [1][]u8 = undefined;
    const both = [_][]u8{ &a, &b };
    try std.testing.expectEqual(@as(usize, 2), clampRead(&both, &one, 8).len);
    const cut = clampRead(&both, &one, 6);
    try std.testing.expectEqual(@as(usize, 1), cut.len);
    try std.testing.expectEqual(@as(usize, 4), cut[0].len);
    try std.testing.expectEqual(@as(usize, 3), clampRead(&both, &one, 3)[0].len);
    var empty: [0]u8 = .{};
    const lead = [_][]u8{ &empty, &a, &b };
    try std.testing.expectEqual(@as([*]u8, &a), clampRead(&lead, &one, 2)[0].ptr);
}

test "ClampedWrite: under the limit is untouched, past it writes a prefix in byte order" {
    const data = [_][]const u8{ "ab", "c" };
    const kept: ClampedWrite = .init("h", &data, 3, 6);
    try std.testing.expectEqualStrings("h", kept.header);
    try std.testing.expectEqual(@as(usize, 3), kept.splat);
    try std.testing.expectEqual(@as(usize, 2), kept.data.len);

    const head: ClampedWrite = .init("header", &data, 3, 4);
    try std.testing.expectEqualStrings("head", head.header);
    try std.testing.expectEqual(@as(usize, 1), head.splat);
    try std.testing.expectEqualStrings("", head.data[0]);

    const body: ClampedWrite = .init("", &data, 9, 1);
    try std.testing.expectEqualStrings("a", body.header);
    // A last element splatted zero times adds nothing; the earlier piece is
    // what gets cut.
    const skipped: ClampedWrite = .init("", &[_][]const u8{ "xyz", "zz" }, 0, 2);
    try std.testing.expectEqualStrings("xy", skipped.header);
}