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 }