lib/windowing/src/clipboard.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3
4 pub const WaitEvent = sys.x11.Message;
5
6 const x11_message_bytes = @sizeOf(WaitEvent);
7
8 pub const Limits = struct {
9 received_text_byte_count: usize = 1 << 20,
10 published_text_byte_count: usize = 64 * 1024,
11 transfer_read_byte_count: usize = 4096,
12 retained_wait_event_count: usize = 64,
13 };
14
15 pub const CapacityError = error{
16 ReceivedTextEmpty,
17 ReceivedTextTooLarge,
18 PublishedTextEmpty,
19 TransferReadStorageEmpty,
20 WaitEventStorageEmpty,
21 CapacityOverflow,
22 };
23
24 pub const Capacity = struct {
25 received_text_byte_count: usize,
26 published_text_byte_count: usize,
27 transfer_read_byte_count: usize,
28 retained_wait_event_count: usize,
29 x11_property_long_count: u32,
30 transfer_read_bytes: usize,
31 retained_wait_event_bytes: usize,
32 maximum_backend_working_storage_bytes: usize,
33
34 pub fn derive(limits: Limits) CapacityError!Capacity {
35 if (limits.received_text_byte_count == 0) return error.ReceivedTextEmpty;
36 if (limits.received_text_byte_count > std.math.maxInt(u32)) {
37 return error.ReceivedTextTooLarge;
38 }
39 if (limits.published_text_byte_count == 0) return error.PublishedTextEmpty;
40 if (limits.transfer_read_byte_count == 0) return error.TransferReadStorageEmpty;
41 if (limits.retained_wait_event_count == 0) return error.WaitEventStorageEmpty;
42 const x11_property_long_count: u32 = @intCast(
43 limits.received_text_byte_count / 4 +
44 @intFromBool(limits.received_text_byte_count % 4 != 0),
45 );
46 const retained_wait_event_bytes = std.math.mul(
47 usize,
48 limits.retained_wait_event_count,
49 x11_message_bytes,
50 ) catch return error.CapacityOverflow;
51 const maximum_backend_working_storage_bytes = @max(
52 limits.transfer_read_byte_count,
53 retained_wait_event_bytes,
54 );
55 return .{
56 .received_text_byte_count = limits.received_text_byte_count,
57 .published_text_byte_count = limits.published_text_byte_count,
58 .transfer_read_byte_count = limits.transfer_read_byte_count,
59 .retained_wait_event_count = limits.retained_wait_event_count,
60 .x11_property_long_count = x11_property_long_count,
61 .transfer_read_bytes = limits.transfer_read_byte_count,
62 .retained_wait_event_bytes = retained_wait_event_bytes,
63 .maximum_backend_working_storage_bytes = maximum_backend_working_storage_bytes,
64 };
65 }
66 };
67
68 pub const Status = struct {
69 wait_event_capacity_rejection_count: u64 = 0,
70 publication_capacity_rejection_count: u64 = 0,
71
72 pub fn combine(a: Status, b: Status) Status {
73 return .{
74 .wait_event_capacity_rejection_count = a.wait_event_capacity_rejection_count +|
75 b.wait_event_capacity_rejection_count,
76 .publication_capacity_rejection_count = a.publication_capacity_rejection_count +|
77 b.publication_capacity_rejection_count,
78 };
79 }
80 };
81
82 pub const PublishError = error{
83 CapacityExceeded,
84 Unavailable,
85 Unexpected,
86 };
87
88 pub const Source = struct {
89 allocator: std.mem.Allocator,
90 bytes: []u8,
91 length: usize = 0,
92 generation: u64 = 0,
93 rejection_count: u64 = 0,
94
95 pub fn init(
96 allocator: std.mem.Allocator,
97 capacity: Capacity,
98 ) std.mem.Allocator.Error!Source {
99 std.debug.assert(capacity.published_text_byte_count > 0);
100 return .{
101 .allocator = allocator,
102 .bytes = try allocator.alloc(u8, capacity.published_text_byte_count),
103 };
104 }
105
106 pub fn deinit(self: *Source) void {
107 std.debug.assert(self.length <= self.bytes.len);
108 self.allocator.free(self.bytes);
109 self.* = undefined;
110 }
111
112 pub fn replace(self: *Source, source_bytes: []const u8) PublishError!void {
113 std.debug.assert(self.length <= self.bytes.len);
114 if (source_bytes.len > self.bytes.len) {
115 self.rejection_count +|= 1;
116 return error.CapacityExceeded;
117 }
118 @memcpy(self.bytes[0..source_bytes.len], source_bytes);
119 self.length = source_bytes.len;
120 self.generation +%= 1;
121 }
122
123 pub fn clear(self: *Source) void {
124 std.debug.assert(self.length <= self.bytes.len);
125 self.length = 0;
126 self.generation +%= 1;
127 }
128
129 pub fn text(self: *const Source) []const u8 {
130 std.debug.assert(self.length <= self.bytes.len);
131 return self.bytes[0..self.length];
132 }
133
134 pub fn status(self: *const Source) Status {
135 return .{
136 .publication_capacity_rejection_count = self.rejection_count,
137 };
138 }
139 };
140
141 pub const WaitEvents = struct {
142 allocator: std.mem.Allocator,
143 items: []WaitEvent,
144 retained_count: usize = 0,
145 rejection_count: u64 = 0,
146
147 pub fn init(
148 allocator: std.mem.Allocator,
149 capacity: Capacity,
150 ) std.mem.Allocator.Error!WaitEvents {
151 std.debug.assert(capacity.retained_wait_event_count > 0);
152 std.debug.assert(capacity.retained_wait_event_bytes % @sizeOf(WaitEvent) == 0);
153 std.debug.assert(
154 capacity.retained_wait_event_bytes / @sizeOf(WaitEvent) ==
155 capacity.retained_wait_event_count,
156 );
157 return .{
158 .allocator = allocator,
159 .items = try allocator.alloc(WaitEvent, capacity.retained_wait_event_count),
160 };
161 }
162
163 pub fn deinit(self: *WaitEvents) void {
164 std.debug.assert(self.retained_count <= self.items.len);
165 self.allocator.free(self.items);
166 self.* = undefined;
167 }
168
169 pub fn reset(self: *WaitEvents) void {
170 std.debug.assert(self.retained_count <= self.items.len);
171 self.retained_count = 0;
172 }
173
174 pub fn admitNext(self: *WaitEvents) bool {
175 std.debug.assert(self.retained_count <= self.items.len);
176 if (self.retained_count < self.items.len) return true;
177 self.rejection_count +|= 1;
178 return false;
179 }
180
181 pub fn appendAssumeCapacity(self: *WaitEvents, event: WaitEvent) void {
182 std.debug.assert(self.retained_count < self.items.len);
183 self.items[self.retained_count] = event;
184 self.retained_count += 1;
185 }
186
187 pub fn full(self: *const WaitEvents) bool {
188 std.debug.assert(self.retained_count <= self.items.len);
189 return self.retained_count == self.items.len;
190 }
191
192 pub fn retained(self: *const WaitEvents) []const WaitEvent {
193 std.debug.assert(self.retained_count <= self.items.len);
194 return self.items[0..self.retained_count];
195 }
196
197 pub fn status(self: *const WaitEvents) Status {
198 return .{ .wait_event_capacity_rejection_count = self.rejection_count };
199 }
200 };
201
202 test "clipboard capacity derives exact storage and rejects invalid limits" {
203 const capacity = try Capacity.derive(.{});
204 try std.testing.expectEqual(@as(usize, 1 << 20), capacity.received_text_byte_count);
205 try std.testing.expectEqual(@as(usize, 64 * 1024), capacity.published_text_byte_count);
206 try std.testing.expectEqual(@as(usize, 4096), capacity.transfer_read_byte_count);
207 try std.testing.expectEqual(@as(usize, 64), capacity.retained_wait_event_count);
208 try std.testing.expectEqual(@as(u32, (1 << 20) / 4), capacity.x11_property_long_count);
209 try std.testing.expectEqual(@as(usize, 4096), capacity.transfer_read_bytes);
210 try std.testing.expectEqual(64 * x11_message_bytes, capacity.retained_wait_event_bytes);
211 try std.testing.expectEqual(
212 @max(capacity.transfer_read_bytes, capacity.retained_wait_event_bytes),
213 capacity.maximum_backend_working_storage_bytes,
214 );
215 try std.testing.expectEqual(
216 @as(u32, 2),
217 (try Capacity.derive(.{ .received_text_byte_count = 5 })).x11_property_long_count,
218 );
219 try std.testing.expectError(error.ReceivedTextEmpty, Capacity.derive(.{
220 .received_text_byte_count = 0,
221 }));
222 if (@sizeOf(usize) > @sizeOf(u32)) {
223 try std.testing.expectError(error.ReceivedTextTooLarge, Capacity.derive(.{
224 .received_text_byte_count = @as(usize, std.math.maxInt(u32)) + 1,
225 }));
226 }
227 try std.testing.expectError(error.TransferReadStorageEmpty, Capacity.derive(.{
228 .transfer_read_byte_count = 0,
229 }));
230 try std.testing.expectError(error.PublishedTextEmpty, Capacity.derive(.{
231 .published_text_byte_count = 0,
232 }));
233 try std.testing.expectError(error.WaitEventStorageEmpty, Capacity.derive(.{
234 .retained_wait_event_count = 0,
235 }));
236 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
237 .retained_wait_event_count = std.math.maxInt(usize) / x11_message_bytes + 1,
238 }));
239 }
240
241 test "clipboard source replaces transactionally through exact capacity" {
242 const capacity = try Capacity.derive(.{
243 .received_text_byte_count = 7,
244 .published_text_byte_count = 7,
245 .transfer_read_byte_count = 3,
246 .retained_wait_event_count = 2,
247 });
248 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
249 try std.testing.expectError(error.OutOfMemory, Source.init(failing.allocator(), capacity));
250
251 failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
252 var source = try Source.init(failing.allocator(), capacity);
253 defer source.deinit();
254 try source.replace("before");
255 try std.testing.expectEqualStrings("before", source.text());
256 try std.testing.expectEqual(@as(u64, 1), source.generation);
257 try std.testing.expectError(error.CapacityExceeded, source.replace("12345678"));
258 try std.testing.expectEqualStrings("before", source.text());
259 try std.testing.expectEqual(@as(u64, 1), source.generation);
260 try std.testing.expectEqual(Status{
261 .publication_capacity_rejection_count = 1,
262 }, source.status());
263 try source.replace("after");
264 try std.testing.expectEqualStrings("after", source.text());
265 try std.testing.expectEqual(@as(u64, 2), source.generation);
266 source.clear();
267 try std.testing.expectEqualStrings("", source.text());
268 try std.testing.expectEqual(@as(u64, 3), source.generation);
269 try std.testing.expectEqual(@as(usize, 1), failing.allocations);
270 }
271
272 test "clipboard wait storage rejects max plus one and reuses its allocation" {
273 const capacity = try Capacity.derive(.{
274 .received_text_byte_count = 7,
275 .transfer_read_byte_count = 3,
276 .retained_wait_event_count = 2,
277 });
278 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
279 try std.testing.expectError(error.OutOfMemory, WaitEvents.init(failing.allocator(), capacity));
280
281 failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 1 });
282 var events = try WaitEvents.init(failing.allocator(), capacity);
283 defer events.deinit();
284 try std.testing.expect(events.admitNext());
285 events.appendAssumeCapacity(@splat(1));
286 try std.testing.expect(events.admitNext());
287 events.appendAssumeCapacity(@splat(2));
288 try std.testing.expect(events.full());
289 try std.testing.expect(!events.admitNext());
290 try std.testing.expectEqual(@as(usize, 2), events.retained().len);
291 try std.testing.expectEqual(@as(u8, 1), events.retained()[0][0]);
292 try std.testing.expectEqual(@as(u8, 2), events.retained()[1][0]);
293 try std.testing.expectEqual(Status{
294 .wait_event_capacity_rejection_count = 1,
295 }, events.status());
296 events.rejection_count = std.math.maxInt(u64);
297 try std.testing.expect(!events.admitNext());
298 try std.testing.expectEqual(Status{
299 .wait_event_capacity_rejection_count = std.math.maxInt(u64),
300 }, events.status());
301 events.reset();
302 try std.testing.expect(events.admitNext());
303 events.appendAssumeCapacity(@splat(3));
304 try std.testing.expectEqual(@as(usize, 1), events.retained().len);
305 try std.testing.expectEqual(@as(usize, 1), failing.allocations);
306 }