tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

io_v2_files.zig (19727B)


      1 //! Private native regular-file backend, not a v2 facility dispatcher. Single
      2 //! owner; the embedding supplies scope admission and exclusive registry leases.
      3 //! These synchronous bounded calls make no cancellation/deadline guarantee.
      4 //! Never use this for standard streams, FIFOs or devices. No v1 policy/dispatch.
      5 const std = @import("std");
      6 const c = @cImport({
      7     @cUndef("_FORTIFY_SOURCE");
      8     @cDefine("_FORTIFY_SOURCE", "0");
      9     @cInclude("uv.h");
     10     @cInclude("sys/stat.h");
     11     @cInclude("fcntl.h");
     12 });
     13 pub const limit = std.math.maxInt(i64);
     14 pub const chunk_limit = 4096;
     15 pub const Mode = enum { reader, writer, appender, update };
     16 pub const Error = error{ InvalidArgument, InvalidBound, UnsupportedFileType, WrongAccess, UnsupportedMode, Closed, HostFailure };
     17 // Error codes are copied before request cleanup. A failed acquisition retains its
     18 // primary error AND any close error; cleanup does not replace the primary fault.
     19 pub const Status = struct { code: c_int = 0, cleanup_code: c_int = 0 };
     20 pub const File = struct {
     21     fd: ?c.uv_file,
     22     mode: Mode,
     23     cursor: u64 = 0,
     24     fn descriptor(self: *const File) Error!c.uv_file {
     25         return self.fd orelse error.Closed;
     26     }
     27     pub fn close(self: *File, status: *Status) Error!void {
     28         status.* = .{};
     29         const fd = try self.descriptor();
     30         self.fd = null; // Never retry an uncertain close against a reused FD.
     31         var req: c.uv_fs_t = undefined;
     32         const rc = c.uv_fs_close(null, &req, fd, null);
     33         c.uv_fs_req_cleanup(&req);
     34         try check(status, rc);
     35     }
     36     pub fn read(self: *File, status: *Status, bytes: []u8) Error!usize {
     37         status.* = .{};
     38         if (bytes.len == 0 or bytes.len > chunk_limit) return error.InvalidBound;
     39         const fd = try self.descriptor();
     40         if (self.mode != .reader and self.mode != .update) return error.WrongAccess;
     41         const count = @min(bytes.len, limit - self.cursor);
     42         if (count == 0) {
     43             _ = try self.size(status);
     44             return 0;
     45         }
     46         var buffer = c.uv_buf_init(bytes.ptr, @intCast(count));
     47         var req: c.uv_fs_t = undefined;
     48         const rc = c.uv_fs_read(null, &req, fd, &buffer, 1, @intCast(self.cursor), null);
     49         c.uv_fs_req_cleanup(&req);
     50         try check(status, rc);
     51         self.cursor += @intCast(rc);
     52         return @intCast(rc);
     53     }
     54     pub fn write(self: *File, status: *Status, bytes: []const u8) Error!usize {
     55         status.* = .{};
     56         if (bytes.len == 0 or bytes.len > chunk_limit) return error.InvalidBound;
     57         const fd = try self.descriptor();
     58         if (self.mode == .reader) return error.WrongAccess;
     59         if (self.mode != .appender and bytes.len > limit - self.cursor) return error.InvalidBound;
     60         var buffer = c.uv_buf_init(@constCast(bytes.ptr), @intCast(bytes.len));
     61         var req: c.uv_fs_t = undefined;
     62         const offset: i64 = if (self.mode == .appender) -1 else @intCast(self.cursor);
     63         const rc = c.uv_fs_write(null, &req, fd, &buffer, 1, offset, null);
     64         c.uv_fs_req_cleanup(&req);
     65         try check(status, rc);
     66         if (rc == 0) {
     67             status.code = c.UV_EIO;
     68             return error.HostFailure;
     69         }
     70         if (self.mode != .appender) self.cursor += @intCast(rc);
     71         return @intCast(rc);
     72     }
     73     pub fn seek(self: *File, offset: u64) Error!void {
     74         if (offset > limit) return error.InvalidBound;
     75         _ = try self.descriptor();
     76         if (self.mode == .appender) return error.UnsupportedMode;
     77         self.cursor = offset;
     78     }
     79     pub fn position(self: *const File) Error!u64 {
     80         _ = try self.descriptor();
     81         if (self.mode == .appender) return error.UnsupportedMode;
     82         return self.cursor;
     83     }
     84     pub fn size(self: *const File, status: *Status) Error!u64 {
     85         status.* = .{};
     86         var req: c.uv_fs_t = undefined;
     87         const rc = c.uv_fs_fstat(null, &req, try self.descriptor(), null);
     88         defer c.uv_fs_req_cleanup(&req);
     89         try check(status, rc);
     90         if (req.statbuf.st_size > limit) return error.InvalidBound;
     91         return req.statbuf.st_size;
     92     }
     93     pub fn resize(self: *File, status: *Status, length: u64) Error!void {
     94         status.* = .{};
     95         if (length > limit) return error.InvalidBound;
     96         const fd = try self.descriptor();
     97         if (self.mode != .writer and self.mode != .update) return error.UnsupportedMode;
     98         var req: c.uv_fs_t = undefined;
     99         const rc = c.uv_fs_ftruncate(null, &req, fd, @intCast(length), null);
    100         c.uv_fs_req_cleanup(&req);
    101         try check(status, rc); // Explicit-offset cursor survives resize, even past EOF.
    102     }
    103     pub fn sync(self: *const File, status: *Status) Error!void {
    104         status.* = .{};
    105         var req: c.uv_fs_t = undefined;
    106         const rc = c.uv_fs_fsync(null, &req, try self.descriptor(), null);
    107         c.uv_fs_req_cleanup(&req);
    108         try check(status, rc);
    109     }
    110 };
    111 fn check(status: *Status, rc: c_int) Error!void {
    112     if (rc < 0) {
    113         status.code = rc;
    114         return error.HostFailure;
    115     }
    116 }
    117 fn regular(mode: u64) bool {
    118     return mode & c.S_IFMT == c.S_IFREG;
    119 }
    120 pub fn open(status: *Status, path: [:0]const u8, mode: Mode) Error!File {
    121     return openAfterCheck(status, path, mode, null);
    122 }
    123 const CheckHook = struct { context: *anyopaque, call: *const fn (*anyopaque) void };
    124 // Private deterministic namespace-race seam; never a source callback.
    125 fn openAfterCheck(status: *Status, path: [:0]const u8, mode: Mode, hook: ?CheckHook) Error!File {
    126     status.* = .{};
    127     if (path.len == 0 or std.mem.indexOfScalar(u8, path, 0) != null or !std.unicode.utf8ValidateSlice(path)) return error.InvalidArgument;
    128     // Preflight avoids opening known devices. It is NOT a security check: the
    129     // namespace can change. The descriptor check below is authoritative, and
    130     // deliberately precedes truncation. Opening a raced device may have effects.
    131     var req: c.uv_fs_t = undefined;
    132     const stat_rc = c.uv_fs_stat(null, &req, path.ptr, null);
    133     const is_regular = stat_rc >= 0 and regular(req.statbuf.st_mode);
    134     c.uv_fs_req_cleanup(&req);
    135     if (stat_rc >= 0 and !is_regular) return error.UnsupportedFileType;
    136     if (stat_rc < 0 and (stat_rc != c.UV_ENOENT or mode == .reader)) try check(status, stat_rc);
    137     if (hook) |h| h.call(h.context);
    138     const flags: c_int = c.O_CLOEXEC | c.O_NONBLOCK | c.O_NOCTTY | switch (mode) {
    139         .reader => c.O_RDONLY,
    140         .writer => c.O_WRONLY | c.O_CREAT, // NO O_TRUNC until fstat accepts the FD.
    141         .appender => c.O_WRONLY | c.O_CREAT | c.O_APPEND,
    142         .update => c.O_RDWR | c.O_CREAT,
    143     };
    144     const fd = c.uv_fs_open(null, &req, path.ptr, flags, 0o666, null);
    145     c.uv_fs_req_cleanup(&req);
    146     try check(status, fd);
    147     var file: File = .{ .fd = fd, .mode = mode };
    148     errdefer {
    149         var cleanup: Status = .{};
    150         file.close(&cleanup) catch {};
    151         status.cleanup_code = cleanup.code;
    152     }
    153     const rc = c.uv_fs_fstat(null, &req, fd, null);
    154     const accepted = rc >= 0 and regular(req.statbuf.st_mode);
    155     c.uv_fs_req_cleanup(&req);
    156     try check(status, rc);
    157     if (!accepted) return error.UnsupportedFileType;
    158     if (mode == .writer) try file.resize(status, 0);
    159     return file;
    160 }
    161 
    162 const testing = std.testing;
    163 fn testPath(tmp: std.Io.Dir, name: []const u8) ![:0]u8 {
    164     const directory = try tmp.realPathFileAlloc(testing.io, ".", testing.allocator);
    165     defer testing.allocator.free(directory);
    166     return std.fmt.allocPrintSentinel(testing.allocator, "{s}/{s}", .{ directory, name }, 0);
    167 }
    168 test "regular file modes, bounded transfers and cursor-preserving resize/sync" {
    169     var tmp = testing.tmpDir(.{});
    170     defer tmp.cleanup();
    171     const path = try testPath(tmp.dir, "é😀");
    172     defer testing.allocator.free(path);
    173     var status: Status = .{};
    174     var file = try open(&status, path, .update);
    175     defer file.close(&status) catch {};
    176     try testing.expectEqual(@as(usize, 3), try file.write(&status, "abc"));
    177     try file.sync(&status);
    178     try testing.expectEqual(@as(u64, 3), try file.position());
    179     try file.seek(1);
    180     var bytes: [4097]u8 = undefined;
    181     try testing.expectEqual(@as(usize, 2), try file.read(&status, bytes[0..3]));
    182     try testing.expectEqualStrings("bc", bytes[0..2]);
    183     try testing.expectEqual(@as(usize, 0), try file.read(&status, bytes[0..3]));
    184     try file.seek(10);
    185     try file.resize(&status, 1);
    186     try testing.expectEqual(@as(u64, 10), try file.position());
    187     try testing.expectEqual(@as(u64, 1), try file.size(&status));
    188     try testing.expectError(error.InvalidBound, file.read(&status, &bytes));
    189     try testing.expectError(error.InvalidBound, file.read(&status, bytes[0..0]));
    190     try testing.expectError(error.InvalidBound, file.write(&status, &bytes));
    191     try testing.expectError(error.InvalidBound, file.seek(@as(u64, limit) + 1));
    192     try testing.expectError(error.InvalidBound, file.resize(&status, @as(u64, limit) + 1));
    193     try file.close(&status);
    194     try testing.expectError(error.Closed, file.close(&status));
    195     try testing.expectError(error.Closed, file.read(&status, bytes[0..1]));
    196     var writer = try open(&status, path, .writer);
    197     defer writer.close(&status) catch {};
    198     try testing.expectEqual(@as(u64, 0), try writer.size(&status));
    199     try testing.expectError(error.WrongAccess, writer.read(&status, bytes[0..1]));
    200     _ = try writer.write(&status, "x");
    201     var appender = try open(&status, path, .appender);
    202     defer appender.close(&status) catch {};
    203     _ = try appender.write(&status, "y");
    204     try testing.expectError(error.UnsupportedMode, appender.seek(0));
    205     try testing.expectError(error.UnsupportedMode, appender.position());
    206     try testing.expectError(error.UnsupportedMode, appender.resize(&status, 0));
    207     try testing.expectError(error.WrongAccess, appender.read(&status, bytes[0..1]));
    208     var reader = try open(&status, path, .reader);
    209     defer reader.close(&status) catch {};
    210     try testing.expectError(error.InvalidBound, reader.write(&status, ""));
    211     try testing.expectError(error.UnsupportedMode, reader.resize(&status, 0));
    212     try testing.expectEqual(@as(usize, 2), try reader.read(&status, bytes[0..3]));
    213     try testing.expectEqualStrings("xy", bytes[0..2]);
    214     try reader.seek(limit);
    215     try testing.expectEqual(@as(usize, 0), try reader.read(&status, bytes[0..1]));
    216 }
    217 test "reject nonregular paths, invalid text and missing readers; symlinks are not confinement" {
    218     var tmp = testing.tmpDir(.{});
    219     defer tmp.cleanup();
    220     const fifo = try testPath(tmp.dir, "fifo");
    221     defer testing.allocator.free(fifo);
    222     try testing.expectEqual(@as(c_int, 0), c.mkfifo(fifo.ptr, 0o600));
    223     const directory = try testPath(tmp.dir, ".");
    224     defer testing.allocator.free(directory);
    225     const missing = try testPath(tmp.dir, "missing");
    226     defer testing.allocator.free(missing);
    227     var status: Status = .{};
    228     for (std.enums.values(Mode)) |mode| {
    229         try testing.expectError(error.UnsupportedFileType, open(&status, fifo, mode));
    230         try testing.expectError(error.UnsupportedFileType, open(&status, directory, mode));
    231         try testing.expectError(error.UnsupportedFileType, open(&status, "/dev/null", mode));
    232     }
    233     try testing.expectError(error.HostFailure, open(&status, missing, .reader));
    234     try testing.expectEqual(@as(c_int, c.UV_ENOENT), status.code);
    235     try testing.expectError(error.InvalidArgument, open(&status, "", .writer));
    236     try testing.expectError(error.InvalidArgument, open(&status, "x\x00y", .writer));
    237     try testing.expectError(error.InvalidArgument, open(&status, "\xff", .writer));
    238     var file = try open(&status, missing, .writer);
    239     defer file.close(&status) catch {};
    240     _ = try file.write(&status, "keep");
    241     const link = try testPath(tmp.dir, "link");
    242     defer testing.allocator.free(link);
    243     var req: c.uv_fs_t = undefined;
    244     const rc = c.uv_fs_symlink(null, &req, missing.ptr, link.ptr, 0, null);
    245     c.uv_fs_req_cleanup(&req);
    246     try testing.expectEqual(@as(c_int, 0), rc);
    247     var reader = try open(&status, link, .reader);
    248     defer reader.close(&status) catch {};
    249     var bytes: [4]u8 = undefined;
    250     try testing.expectEqual(@as(usize, 4), try reader.read(&status, &bytes));
    251     try testing.expectEqualStrings("keep", &bytes);
    252     try testing.expect(c.fcntl(reader.fd.?, c.F_GETFD) & c.FD_CLOEXEC != 0);
    253 }
    254 
    255 // Concrete ownership bridge. Null is reserved for standard placeholders; it is
    256 // NOT a standard-stream implementation. Real dispatch must check backend kind
    257 // before leasing. No allocator operation follows successful host acquisition.
    258 pub const Registry = @import("io_v2_resources.zig").Registry(?*File);
    259 pub fn acquire(registry: *Registry, owner: u64, status: *Status, path: [:0]const u8, mode: Mode) !@import("io_v2_resources.zig").Token {
    260     return acquireWrapped(registry, owner, status, path, mode, struct {
    261         fn wrap(file: *File) ?*File {
    262             return file;
    263         }
    264     }.wrap);
    265 }
    266 pub fn acquireWrapped(registry: anytype, owner: u64, status: *Status, path: [:0]const u8, mode: Mode, comptime wrap: anytype) !@import("io_v2_resources.zig").Token {
    267     status.* = .{};
    268     const admission = try registry.beginAcquire(owner);
    269     errdefer registry.abortAcquire(admission) catch unreachable;
    270     const backend = try registry.allocator.create(File);
    271     errdefer registry.allocator.destroy(backend);
    272     backend.* = try open(status, path, mode);
    273     errdefer {
    274         var cleanup: Status = .{};
    275         backend.close(&cleanup) catch {};
    276         status.cleanup_code = cleanup.code;
    277     }
    278     return registry.publish(admission, wrap(backend), .{});
    279 }
    280 // Call only for an acquired close claim, after all leases have settled. Registry
    281 // entry stays present through physical close, even on error. Memory and aliases
    282 // are retired exactly once; a close error is reported, never retried implicitly.
    283 pub fn release(registry: *Registry, token: @import("io_v2_resources.zig").Token, backend: *File, status: *Status) Error!void {
    284     defer registry.finishClose(token) catch unreachable;
    285     defer registry.allocator.destroy(backend);
    286     try backend.close(status);
    287 }
    288 
    289 fn acquisitionFailureProbe(allocator: std.mem.Allocator, path: [:0]const u8) !void {
    290     var registry = try Registry.init(allocator, 91, .{ null, null, null });
    291     defer registry.deinit();
    292     var status: Status = .{};
    293     const token = acquire(&registry, 0, &status, path, .reader) catch |err| {
    294         try testing.expectEqual(@as(usize, 0), registry.admissions.count());
    295         try testing.expectEqual(@as(usize, 3), registry.resources.count());
    296         return err;
    297     };
    298     const claim = (try registry.claimClose(0, token, false)).?;
    299     try release(&registry, token, claim.backend.?, &status);
    300     try testing.expectEqual(@as(usize, 3), registry.resources.count());
    301 }
    302 test "real descriptor admission, lease, handoff, close and allocator failure" {
    303     const R = @import("io_v2_resources.zig");
    304     var tmp = testing.tmpDir(.{});
    305     defer tmp.cleanup();
    306     try tmp.dir.writeFile(testing.io, .{ .sub_path = "file", .data = "abc" });
    307     const path = try testPath(tmp.dir, "file");
    308     defer testing.allocator.free(path);
    309     try testing.checkAllAllocationFailures(testing.allocator, acquisitionFailureProbe, .{path});
    310     var registry = try Registry.init(testing.allocator, 92, .{ null, null, null });
    311     defer registry.deinit();
    312     var status: Status = .{};
    313     const token = try acquire(&registry, 0, &status, path, .update);
    314     const lease = try registry.beginLease(0, token, .exclusive, false);
    315     const fd = lease.backend.?.fd.?;
    316     try testing.expectError(error.Busy, registry.reserveChild(0, &.{token}));
    317     try registry.endLease(lease.token);
    318     const transfer = try registry.reserveChild(0, &.{token});
    319     try testing.expectError(error.StaleHandle, registry.lookup(0, token));
    320     try testing.expectError(error.OutOfScope, registry.lookup(0, transfer.tokens[0]));
    321     const child_lease = try registry.beginLease(transfer.scope, transfer.tokens[0], .exclusive, false);
    322     var bytes: [3]u8 = undefined;
    323     _ = try child_lease.backend.?.read(&status, &bytes);
    324     try testing.expectEqualStrings("abc", &bytes);
    325     registry.beginCloseScope(transfer.scope);
    326     try testing.expect((try registry.claimClose(transfer.scope, transfer.tokens[0], true)) == null);
    327     try registry.endLease(child_lease.token);
    328     const claim = (try registry.claimClose(transfer.scope, transfer.tokens[0], true)).?;
    329     try release(&registry, transfer.tokens[0], claim.backend.?, &status);
    330     try testing.expectEqual(@as(c_int, -1), c.fcntl(fd, c.F_GETFD));
    331     try registry.finishScope(transfer.scope);
    332     // Publication exhaustion can occur AFTER successful open. The descriptor
    333     // must be closed and acquisition aborted, not silently left ownerless.
    334     registry.next = std.math.maxInt(u64) - 1;
    335     try testing.expectError(R.Fault.IdExhausted, acquire(&registry, 0, &status, path, .reader));
    336     try testing.expectEqual(@as(usize, 0), registry.admissions.count());
    337     try testing.expectEqual(@as(usize, 3), registry.resources.count());
    338 }
    339 
    340 pub fn errorCategory(code: c_int) []const u8 {
    341     return switch (code) {
    342         c.UV_ENOENT => "not-found",
    343         c.UV_EACCES, c.UV_EPERM => "os-permission-denied",
    344         c.UV_EEXIST => "already-exists",
    345         else => "io-error",
    346     };
    347 }
    348 pub fn errorText(code: c_int) []const u8 {
    349     return std.mem.span(c.uv_strerror(code));
    350 }
    351 
    352 test "descriptor kind check rejects a namespace substitution after preflight without truncation" {
    353     const Swap = struct {
    354         from: [:0]const u8,
    355         to: [:0]const u8,
    356         fn run(context: *anyopaque) void {
    357             const self: *@This() = @ptrCast(@alignCast(context));
    358             var req: c.uv_fs_t = undefined;
    359             const rc = c.uv_fs_rename(null, &req, self.from.ptr, self.to.ptr, null);
    360             c.uv_fs_req_cleanup(&req);
    361             std.debug.assert(rc == 0);
    362         }
    363     };
    364     for ([_]Mode{ .reader, .writer, .appender, .update }) |mode| {
    365         var tmp = testing.tmpDir(.{});
    366         defer tmp.cleanup();
    367         try tmp.dir.writeFile(testing.io, .{ .sub_path = "file", .data = "keep" });
    368         const path = try testPath(tmp.dir, "file");
    369         defer testing.allocator.free(path);
    370         const fifo = try testPath(tmp.dir, "fifo");
    371         defer testing.allocator.free(fifo);
    372         const saved = try testPath(tmp.dir, "saved");
    373         defer testing.allocator.free(saved);
    374         try testing.expectEqual(@as(c_int, 0), c.mkfifo(fifo.ptr, 0o600));
    375         var req: c.uv_fs_t = undefined;
    376         const linked = c.uv_fs_link(null, &req, path.ptr, saved.ptr, null);
    377         c.uv_fs_req_cleanup(&req);
    378         try testing.expectEqual(@as(c_int, 0), linked);
    379         // Keep a reader for writer modes, but no writer: opening the reader case
    380         // must use NONBLOCK rather than wait for a nonexistent FIFO peer.
    381         const anchor = c.uv_fs_open(null, &req, fifo.ptr, c.O_RDONLY | c.O_NONBLOCK | c.O_CLOEXEC, 0, null);
    382         c.uv_fs_req_cleanup(&req);
    383         try testing.expect(anchor >= 0);
    384         defer {
    385             _ = c.uv_fs_close(null, &req, anchor, null);
    386             c.uv_fs_req_cleanup(&req);
    387         }
    388         var swap: Swap = .{ .from = fifo, .to = path };
    389         var status: Status = .{};
    390         try testing.expectError(error.UnsupportedFileType, openAfterCheck(&status, path, mode, .{ .context = &swap, .call = Swap.run }));
    391         var original = try open(&status, saved, .reader);
    392         defer original.close(&status) catch {};
    393         var bytes: [4]u8 = undefined;
    394         try testing.expectEqual(@as(usize, 4), try original.read(&status, &bytes));
    395         try testing.expectEqualStrings("keep", &bytes);
    396     }
    397 }