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 }