lib/content/src/capability.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const model = @import("model.zig");
  3 
  4 pub const Publication = enum(u8) {
  5     created,
  6     exists_same,
  7 };
  8 
  9 const ProviderPublicationError = error{
 10     ContentMutated,
 11     PublicationConflict,
 12     PublicationFailed,
 13     StoreCapacityExceeded,
 14 };
 15 
 16 pub const PublicationError = model.ReferenceError || ProviderPublicationError;
 17 
 18 const ProviderReadError = error{
 19     ContentMissing,
 20     ReadFailed,
 21 };
 22 
 23 pub const ReadError = model.ReferenceError || ProviderReadError || error{
 24     CallerBufferTooSmall,
 25     ContentTruncated,
 26 };
 27 
 28 const ReaderVTable = struct {
 29     read: *const fn (
 30         *anyopaque,
 31         model.Identifier,
 32         []u8,
 33     ) ProviderReadError!u64,
 34 };
 35 
 36 /// Read capability over one provider, for a holder that reopens bytes it
 37 /// already has a reference for. The capability carries three things: the
 38 /// capacity the store was built with, a function table whose single entry reads
 39 /// by identifier, and an opaque context pointer borrowed from the provider that
 40 /// produced it, so the provider outlives the Reader.
 41 pub const Reader = struct {
 42     context: *anyopaque,
 43     capacity: model.Capacity,
 44     vtable: *const VTable,
 45 
 46     pub const VTable: type = ReaderVTable;
 47 
 48     /// Reopens the bytes of one reference into buffer storage the caller owns
 49     /// and returns the slice that was checked. The function rejects a reference
 50     /// whose extent is above the capacity's per-object byte limit, the *object
 51     /// ceiling*, with `ContentExtentExceeded` before it touches the provider,
 52     /// and it reports `CallerBufferTooSmall` when the buffer is shorter than
 53     /// the reference extent. The provider receives a slice cut to exactly that
 54     /// extent. The call reports `ContentTruncated` when the provider returns
 55     /// fewer bytes than the extent and `ContentExtentMismatch` when it returns
 56     /// more, and then rehashes what came back and reports
 57     /// `ContentIdentifierMismatch` when the digest disagrees, so a provider
 58     /// that returns the wrong bytes is caught here.
 59     pub fn read(
 60         self: Reader,
 61         reference: model.Reference,
 62         output: []u8,
 63     ) ReadError![]const u8 {
 64         try self.capacity.validateReference(reference);
 65         const expected = reference.extent.toUsize();
 66         if (output.len < expected) return error.CallerBufferTooSmall;
 67         const actual = try self.vtable.read(
 68             self.context,
 69             reference.identifier,
 70             output[0..expected],
 71         );
 72         if (actual < reference.extent.bytes) return error.ContentTruncated;
 73         if (actual > reference.extent.bytes) return error.ContentExtentMismatch;
 74         const verified = output[0..expected];
 75         if (!reference.identifier.matches(verified)) {
 76             return error.ContentIdentifierMismatch;
 77         }
 78         return verified;
 79     }
 80 };
 81 
 82 const SinkVTable = struct {
 83     put: *const fn (
 84         *anyopaque,
 85         model.Reference,
 86         []const u8,
 87     ) ProviderPublicationError!Publication,
 88 };
 89 
 90 /// Write capability over one provider, for a holder that may add bytes to a
 91 /// store and nothing else. The capability carries three things: an opaque
 92 /// context pointer for the provider, the capacity the store was built with, and
 93 /// a function table whose single entry publishes bytes. Publishing is its whole
 94 /// surface, so holding a Sink grants no way to read, look up, list, or delete.
 95 pub const Sink = struct {
 96     context: *anyopaque,
 97     capacity: model.Capacity,
 98     vtable: *const VTable,
 99 
100     pub const VTable: type = SinkVTable;
101 
102     /// Makes bytes retrievable under the reference a caller derived from them
103     /// by checking the bytes against the reference and then asking the provider
104     /// to publish them. The call rejects a reference whose extent is above the
105     /// object ceiling first, with `ContentExtentExceeded`, and verifies the
106     /// bytes against the reference next, so a length or digest disagreement
107     /// stops the call before the provider sees anything. The call returns the
108     /// provider's publication answer, `created` for new bytes and `exists_same`
109     /// for bytes already held under this reference.
110     pub fn put(
111         self: Sink,
112         reference: model.Reference,
113         bytes: []const u8,
114     ) PublicationError!Publication {
115         try self.capacity.validateReference(reference);
116         try reference.verify(bytes);
117         return self.vtable.put(self.context, reference, bytes);
118     }
119 };
120 
121 /// Holds one Reader and one Sink together for a caller trusted with both
122 /// authorities. Both halves stay reachable as fields, so a caller can still
123 /// hand on one of them alone.
124 pub const Store = struct {
125     reader: Reader,
126     sink: Sink,
127 
128     /// Pairs a Reader with a Sink so a provider can hand out both of its
129     /// capabilities as one value. The function compares the two capacities
130     /// field by field and returns `CapabilityCapacityMismatch` unless they are
131     /// equal, which keeps a reader and a writer from disagreeing about the
132     /// bounds they enforce.
133     pub fn init(reader: Reader, sink: Sink) error{CapabilityCapacityMismatch}!Store {
134         if (!std.meta.eql(reader.capacity, sink.capacity)) {
135             return error.CapabilityCapacityMismatch;
136         }
137         return .{ .reader = reader, .sink = sink };
138     }
139 };
140 
141 test "sink exposes no read lookup enumeration deletion or backend authority" {
142     try std.testing.expect(!@hasDecl(Sink, "read"));
143     try std.testing.expect(!@hasDecl(Sink, "lookup"));
144     try std.testing.expect(!@hasDecl(Sink, "exists"));
145     try std.testing.expect(!@hasDecl(Sink, "enumerate"));
146     try std.testing.expect(!@hasDecl(Sink, "delete"));
147     try std.testing.expect(!@hasField(Sink, "path"));
148     try std.testing.expect(!@hasField(Sink, "backend"));
149 }
150 
151 test "reader exposes no enumeration deletion or ambient location" {
152     try std.testing.expect(!@hasDecl(Reader, "exists"));
153     try std.testing.expect(!@hasDecl(Reader, "enumerate"));
154     try std.testing.expect(!@hasDecl(Reader, "delete"));
155     try std.testing.expect(!@hasField(Reader, "path"));
156     try std.testing.expect(!@hasField(Reader, "backend"));
157 }