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