//! The narrow file capability the storage engine reads and writes
//! through. Five operations, one error, nothing else — not a wrapper over
//! all of `std.Io`. Kept internal (not re-exported from root.zig) until a
//! real consumer (LogStore) needs it; its unit tests reach `zig build
//! test` through the dst module, which imports this file directly.
//!
//! There is no RFC section behind this file: it is native storage-engine
//! design, not a port. The oracle is this capability's own crash-recovery
//! invariants, exercised by the DST sweep in sim/storage_sweep.zig: every
//! acknowledged put stays readable and hash-verifies; a torn tail
//! truncates to exactly the last complete frame; the rebuilt index
//! matches the write-time model; and corruption always surfaces as an
//! error, never as silently wrong bytes.
const std = @import("std");
/// The device failed. Corruption is deliberately NOT a `StorageError`: a
/// corrupt read returns wrong bytes successfully (short of hardware I/O
/// failure), and the frame layer above is what turns "wrong bytes" into a
/// decision — the crash-recovery invariant that corruption is always an
/// error, never silent data. One variant is the whole set — every other
/// outcome (short read, no-op truncate, EOF) is defined as ordinary data,
/// not an error.
pub const StorageError = error{Io};
/// A capability, not a resource: `context` is caller-owned and outlives
/// every call made through the vtable.
pub const StorageFile = struct {
context: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
append: *const fn (ctx: *anyopaque, bytes: []const u8) StorageError!void,
/// Never errors on short read — returns the count actually read,
/// `0..buf.len`, with 0 meaning "at or past EOF": the error is
/// defined out of existence for EOF rather than threaded through
/// every caller.
pread: *const fn (ctx: *anyopaque, offset: u64, buf: []u8) StorageError!usize,
sync: *const fn (ctx: *anyopaque) StorageError!void,
size: *const fn (ctx: *anyopaque) StorageError!u64,
/// A no-op (same length) or a repeat truncate is legal, never an
/// error.
truncate: *const fn (ctx: *anyopaque, len: u64) StorageError!void,
};
pub fn append(self: StorageFile, bytes: []const u8) StorageError!void {
return self.vtable.append(self.context, bytes);
}
pub fn pread(self: StorageFile, offset: u64, buf: []u8) StorageError!usize {
return self.vtable.pread(self.context, offset, buf);
}
pub fn sync(self: StorageFile) StorageError!void {
return self.vtable.sync(self.context);
}
pub fn size(self: StorageFile) StorageError!u64 {
return self.vtable.size(self.context);
}
pub fn truncate(self: StorageFile, len: u64) StorageError!void {
return self.vtable.truncate(self.context, len);
}
};
/// Production binding: a real file reached only through the injected
/// `std.Io` (design-seams rule — no ambient filesystem access). `end`
/// tracks the append watermark so `append` never needs a `size` round trip.
pub const FileStorage = struct {
io: std.Io,
file: std.Io.File,
end: u64,
/// Precondition: `initial_size` equals `file`'s current length. Not
/// verified here — doing so would mean a stat syscall inside a plain
/// constructor, and TigerStyle assertions must not carry I/O side
/// effects. A caller that gets this wrong sees the mismatch surface
/// downstream, as a wrong `size()`/`pread` result.
pub fn init(io: std.Io, file: std.Io.File, initial_size: u64) FileStorage {
return .{ .io = io, .file = file, .end = initial_size };
}
pub fn storageFile(self: *FileStorage) StorageFile {
return .{ .context = self, .vtable = &vtable };
}
const vtable: StorageFile.VTable = .{
.append = appendImpl,
.pread = preadImpl,
.sync = syncImpl,
.size = sizeImpl,
.truncate = truncateImpl,
};
fn appendImpl(ctx: *anyopaque, bytes: []const u8) StorageError!void {
const self: *FileStorage = @ptrCast(@alignCast(ctx));
self.file.writePositionalAll(self.io, bytes, self.end) catch return error.Io;
self.end += bytes.len;
}
fn preadImpl(ctx: *anyopaque, offset: u64, buf: []u8) StorageError!usize {
const self: *FileStorage = @ptrCast(@alignCast(ctx));
return self.file.readPositionalAll(self.io, buf, offset) catch error.Io;
}
fn syncImpl(ctx: *anyopaque) StorageError!void {
const self: *FileStorage = @ptrCast(@alignCast(ctx));
self.file.sync(self.io) catch return error.Io;
}
fn sizeImpl(ctx: *anyopaque) StorageError!u64 {
const self: *FileStorage = @ptrCast(@alignCast(ctx));
return self.end;
}
fn truncateImpl(ctx: *anyopaque, len: u64) StorageError!void {
const self: *FileStorage = @ptrCast(@alignCast(ctx));
self.file.setLength(self.io, len) catch return error.Io;
// setLength makes the file exactly `len` bytes — it both shrinks and
// extends — so the append watermark is `len`, not `@min(end, len)`,
// which would leave `end` stale (and the next append overwriting the
// extended tail) whenever `len` grows the file.
self.end = len;
}
};
test "storage §B: append then pread round-trips through the prod binding" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var raw = try tmp.dir.createFile(io, "storage-test.bin", .{ .read = true });
defer raw.close(io);
var fs = FileStorage.init(io, raw, 0);
const file = fs.storageFile();
try file.append("hello");
var buf: [5]u8 = undefined;
const n = try file.pread(0, &buf);
try std.testing.expectEqual(@as(usize, 5), n);
try std.testing.expectEqualStrings("hello", &buf);
}
test "storage §B: size tracks appended length and truncate cuts the tail exactly" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var raw = try tmp.dir.createFile(io, "storage-test.bin", .{ .read = true });
defer raw.close(io);
var fs = FileStorage.init(io, raw, 0);
const file = fs.storageFile();
try file.append("0123456789");
try std.testing.expectEqual(@as(u64, 10), try file.size());
try file.truncate(4);
try std.testing.expectEqual(@as(u64, 4), try file.size());
var buf: [4]u8 = undefined;
const n = try file.pread(0, &buf);
try std.testing.expectEqual(@as(usize, 4), n);
try std.testing.expectEqualStrings("0123", &buf);
}
test "storage §B: sync on a healthy file returns without error" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var raw = try tmp.dir.createFile(io, "storage-test.bin", .{ .read = true });
defer raw.close(io);
var fs = FileStorage.init(io, raw, 0);
const file = fs.storageFile();
try file.append("payload");
try file.sync();
}
test "storage §B: pread at/after EOF returns a short count, not an error" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var raw = try tmp.dir.createFile(io, "storage-test.bin", .{ .read = true });
defer raw.close(io);
var fs = FileStorage.init(io, raw, 0);
const file = fs.storageFile();
try file.append("ab");
var buf: [4]u8 = undefined;
const n = try file.pread(2, &buf); // exactly at EOF
try std.testing.expectEqual(@as(usize, 0), n);
const n2 = try file.pread(10, &buf); // well past EOF
try std.testing.expectEqual(@as(usize, 0), n2);
}