lib/xkb/src/compose/load.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const compose = @import("root.zig");
3
4 pub const Error = compose.ParseError || compose.Storage.AcquireError ||
5 compose.ScratchStorage.AcquireError || error{ComposeFileNotFound};
6
7 pub const Options = struct {
8 locale: []const u8,
9 home: ?[]const u8 = null,
10 xcompose_file: ?[]const u8 = null,
11 xdg_config_home: ?[]const u8 = null,
12 system_directory: []const u8 = compose.default_system_directory,
13 io: std.Io = std.Options.debug_io,
14 diagnostic_context: ?*anyopaque = null,
15 diagnostic: ?compose.DiagnosticFn = null,
16
17 pub fn pathOptions(self: Options) compose.PathOptions {
18 return .{
19 .locale = self.locale,
20 .home = self.home,
21 .xcompose_file = self.xcompose_file,
22 .xdg_config_home = self.xdg_config_home,
23 .system_directory = self.system_directory,
24 .io = self.io,
25 };
26 }
27
28 pub fn parserOptions(self: Options) compose.ParserOptions {
29 return .{
30 .paths = self.pathOptions(),
31 .diagnostic_context = self.diagnostic_context,
32 .diagnostic = self.diagnostic,
33 };
34 }
35 };
36
37 pub fn compile(
38 storage: *compose.Storage,
39 scratch_storage: *compose.ScratchStorage,
40 input: []const u8,
41 options: Options,
42 ) Error!compose.Table {
43 try scratch_storage.acquire();
44 defer scratch_storage.reset();
45 return compileSource(storage, scratch_storage, input, "(buffer)", options);
46 }
47
48 pub fn load(
49 storage: *compose.Storage,
50 scratch_storage: *compose.ScratchStorage,
51 options: Options,
52 ) Error!compose.Table {
53 try scratch_storage.acquire();
54 defer scratch_storage.reset();
55 if (options.xcompose_file) |file_path| {
56 if (file_path.len != 0) {
57 if (try loadCandidate(storage, scratch_storage, file_path, options)) |table| {
58 return table;
59 }
60 }
61 }
62
63 if (try compose.xdgPath(
64 scratch_storage.filePath(0),
65 options.pathOptions(),
66 )) |file_path| {
67 if (try loadCandidate(storage, scratch_storage, file_path, options)) |table| {
68 return table;
69 }
70 }
71 if (try compose.homePath(
72 scratch_storage.filePath(0),
73 options.pathOptions(),
74 )) |file_path| {
75 if (try loadCandidate(storage, scratch_storage, file_path, options)) |table| {
76 return table;
77 }
78 }
79
80 const locale_path = try compose.localePath(
81 scratch_storage,
82 scratch_storage.filePath(0),
83 options.pathOptions(),
84 );
85 return (try loadCandidate(storage, scratch_storage, locale_path, options)) orelse
86 error.ComposeFileNotFound;
87 }
88
89 fn loadCandidate(
90 storage: *compose.Storage,
91 scratch_storage: *compose.ScratchStorage,
92 file_path: []const u8,
93 options: Options,
94 ) Error!?compose.Table {
95 var file = std.Io.Dir.cwd().openFile(options.io, file_path, .{}) catch return null;
96 defer file.close(options.io);
97 const stat = file.stat(options.io) catch return null;
98 if (stat.kind != .file) return null;
99 try storage.acquire();
100 errdefer storage.reset();
101 try compose.parseOpenedFile(
102 storage,
103 scratch_storage,
104 file,
105 stat.size,
106 file_path,
107 options.parserOptions(),
108 0,
109 );
110 return storage.publish();
111 }
112
113 fn compileSource(
114 storage: *compose.Storage,
115 scratch_storage: *compose.ScratchStorage,
116 input: []const u8,
117 source: []const u8,
118 options: Options,
119 ) Error!compose.Table {
120 try storage.acquire();
121 errdefer storage.reset();
122 try compose.parse(
123 storage,
124 scratch_storage,
125 input,
126 source,
127 options.parserOptions(),
128 0,
129 );
130 return storage.publish();
131 }
132
133 test "loader observes XCompose precedence and recursively parses includes" {
134 const allocator = std.testing.allocator;
135 var temporary = std.testing.tmpDir(.{});
136 defer temporary.cleanup();
137 const root = try temporary.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);
138 defer allocator.free(root);
139 const included_path = try std.fs.path.join(allocator, &.{ root, "included" });
140 defer allocator.free(included_path);
141 const selected_path = try std.fs.path.join(allocator, &.{ root, "selected" });
142 defer allocator.free(selected_path);
143
144 try temporary.dir.writeFile(std.Options.debug_io, .{
145 .sub_path = "included",
146 .data = "<Multi_key> <a> : \"included\"\n",
147 });
148 const selected = try std.fmt.allocPrint(
149 allocator,
150 "include \"{s}\"\n<Multi_key> <b> : \"selected\"\n",
151 .{included_path},
152 );
153 defer allocator.free(selected);
154 try temporary.dir.writeFile(std.Options.debug_io, .{
155 .sub_path = "selected",
156 .data = selected,
157 });
158
159 var storage = try compose.Storage.init(allocator, .{
160 .sequence_symbols = 8,
161 .text_bytes = 32,
162 });
163 defer storage.deinit(allocator);
164 var scratch_storage = try compose.ScratchStorage.init(allocator, compose.default_scratch_limits);
165 defer scratch_storage.deinit(allocator);
166 storage.activate();
167 scratch_storage.activate();
168 const table = try load(&storage, &scratch_storage, .{
169 .locale = "C",
170 .home = root,
171 .xcompose_file = selected_path,
172 .system_directory = root,
173 });
174 defer storage.reset();
175 var iterator_value = table.iterator();
176 try std.testing.expectEqualStrings("included", iterator_value.next().?.text.?);
177 try std.testing.expectEqualStrings("selected", iterator_value.next().?.text.?);
178 try std.testing.expect(iterator_value.next() == null);
179 }
180
181 test "loader skips a non-regular XCOMPOSEFILE candidate" {
182 const allocator = std.testing.allocator;
183 var temporary = std.testing.tmpDir(.{});
184 defer temporary.cleanup();
185 const root = try temporary.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);
186 defer allocator.free(root);
187 try temporary.dir.createDirPath(std.Options.debug_io, ".config");
188 try temporary.dir.writeFile(std.Options.debug_io, .{
189 .sub_path = ".config/XCompose",
190 .data = "<A> : \"fallback\" A\n",
191 });
192
193 var storage = try compose.Storage.init(allocator, .{
194 .sequence_symbols = 4,
195 .text_bytes = 16,
196 });
197 defer storage.deinit(allocator);
198 var scratch_storage = try compose.ScratchStorage.init(allocator, compose.default_scratch_limits);
199 defer scratch_storage.deinit(allocator);
200 storage.activate();
201 scratch_storage.activate();
202 const table = try load(&storage, &scratch_storage, .{
203 .locale = "blabla",
204 .home = root,
205 .xcompose_file = root,
206 .system_directory = root,
207 });
208 defer storage.reset();
209 var iterator_value = table.iterator();
210 try std.testing.expectEqualStrings("fallback", iterator_value.next().?.text.?);
211 try std.testing.expect(iterator_value.next() == null);
212 }
213
214 test "Compose compiler accepts exact capacities and rejects max plus one" {
215 comptime {
216 @stardustClaim(
217 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "xkb_compose_boundaries"),
218 null,
219 null,
220 null,
221 null,
222 null,
223 null,
224 );
225 }
226
227 const allocator = std.testing.allocator;
228 var storage = try compose.Storage.init(allocator, .{
229 .sequence_symbols = 2,
230 .text_bytes = 2,
231 });
232 defer storage.deinit(allocator);
233 var scratch_storage = try compose.ScratchStorage.init(allocator, compose.default_scratch_limits);
234 defer scratch_storage.deinit(allocator);
235 storage.activate();
236 scratch_storage.activate();
237
238 const exact = try compile(&storage, &scratch_storage, "<A> <B> : \"ok\"\n", .{ .locale = "C" });
239 var exact_iterator = exact.iterator();
240 try std.testing.expectEqualStrings("ok", exact_iterator.next().?.text.?);
241 storage.reset();
242 try std.testing.expectError(
243 error.SequenceSymbolCapacityExceeded,
244 compile(&storage, &scratch_storage, "<A> <B> <C> : \"ok\"\n", .{ .locale = "C" }),
245 );
246 try std.testing.expectError(
247 error.TextByteCapacityExceeded,
248 compile(&storage, &scratch_storage, "<A> : \"long\"\n", .{ .locale = "C" }),
249 );
250 }
251
252 test "Compose storage rejects concurrent use and resets after exhaustion" {
253 comptime {
254 @stardustClaim(
255 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "xkb_compose_reuse"),
256 null,
257 null,
258 null,
259 null,
260 null,
261 null,
262 );
263 }
264
265 const allocator = std.testing.allocator;
266 var storage = try compose.Storage.init(allocator, .{
267 .sequence_symbols = 1,
268 .text_bytes = 1,
269 });
270 defer storage.deinit(allocator);
271 var scratch_storage = try compose.ScratchStorage.init(allocator, compose.default_scratch_limits);
272 defer scratch_storage.deinit(allocator);
273 storage.activate();
274 scratch_storage.activate();
275 _ = try compile(&storage, &scratch_storage, "<A> : \"x\"\n", .{ .locale = "C" });
276 try std.testing.expectError(
277 error.ComposeStorageInUse,
278 compile(&storage, &scratch_storage, "<A> : A\n", .{ .locale = "C" }),
279 );
280 storage.reset();
281 try std.testing.expectError(
282 error.SequenceSymbolCapacityExceeded,
283 compile(&storage, &scratch_storage, "<A> <B> : B\n", .{ .locale = "C" }),
284 );
285 const reused = try compile(&storage, &scratch_storage, "<B> : B\n", .{ .locale = "C" });
286 defer storage.reset();
287 var reused_iterator = reused.iterator();
288 try std.testing.expect(reused_iterator.next() != null);
289 }
290
291 test "Activated Compose storage performs no backing allocation" {
292 comptime {
293 @stardustClaim(
294 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "xkb_compose_sealed"),
295 null,
296 null,
297 null,
298 null,
299 null,
300 null,
301 );
302 }
303
304 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
305 var storage = try compose.Storage.init(counting.allocator(), .{
306 .sequence_symbols = 4,
307 .text_bytes = 8,
308 });
309 defer storage.deinit(counting.allocator());
310 var scratch_storage = try compose.ScratchStorage.init(
311 counting.allocator(),
312 compose.default_scratch_limits,
313 );
314 defer scratch_storage.deinit(counting.allocator());
315 storage.activate();
316 scratch_storage.activate();
317 const allocations = counting.alloc_index;
318 const resizes = counting.resize_index;
319 _ = try compile(&storage, &scratch_storage, "<A> <B> : \"ok\"\n", .{
320 .locale = "C",
321 });
322 defer storage.reset();
323 try std.testing.expectEqual(allocations, counting.alloc_index);
324 try std.testing.expectEqual(resizes, counting.resize_index);
325 }
326
327 test "Compose scratch accepts exact line and path bounds and rejects max plus one" {
328 comptime {
329 @stardustClaim(
330 @import("alloc_phase").capacity.witness(@import("./workspace/root.zig").Storage, "xkb_compose_scratch_boundaries"),
331 null,
332 null,
333 null,
334 null,
335 null,
336 null,
337 );
338 }
339
340 const allocator = std.testing.allocator;
341 var temporary = std.testing.tmpDir(.{});
342 defer temporary.cleanup();
343 const root = try temporary.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);
344 defer allocator.free(root);
345 const included_path = try std.fs.path.join(allocator, &.{ root, "included" });
346 defer allocator.free(included_path);
347 const over_line_path = try std.fs.path.join(allocator, &.{ root, "over-line" });
348 defer allocator.free(over_line_path);
349 try temporary.dir.writeFile(std.Options.debug_io, .{
350 .sub_path = "included",
351 .data = "<A> : A\n",
352 });
353 try temporary.dir.writeFile(std.Options.debug_io, .{
354 .sub_path = "over-line",
355 .data = "<A> : A \n",
356 });
357 const exact_source = try std.fmt.allocPrint(allocator, "include \"{s}\"", .{included_path});
358 defer allocator.free(exact_source);
359 const over_source = try std.fmt.allocPrint(allocator, "include \"{s}x\"", .{included_path});
360 defer allocator.free(over_source);
361
362 var storage = try compose.Storage.init(allocator, .{
363 .sequence_symbols = 1,
364 .text_bytes = 0,
365 });
366 defer storage.deinit(allocator);
367 var scratch_storage = try compose.ScratchStorage.init(allocator, .{
368 .line_bytes = over_source.len,
369 .path_bytes = included_path.len,
370 });
371 defer scratch_storage.deinit(allocator);
372 storage.activate();
373 scratch_storage.activate();
374
375 _ = try compile(&storage, &scratch_storage, exact_source, .{ .locale = "C" });
376 storage.reset();
377 try std.testing.expectError(
378 error.PathByteCapacityExceeded,
379 compile(&storage, &scratch_storage, over_source, .{ .locale = "C" }),
380 );
381
382 var line_scratch = try compose.ScratchStorage.init(allocator, .{
383 .line_bytes = "<A> : A".len,
384 .path_bytes = 1,
385 });
386 defer line_scratch.deinit(allocator);
387 line_scratch.activate();
388 _ = try compile(&storage, &line_scratch, "<A> : A", .{ .locale = "C" });
389 storage.reset();
390 try std.testing.expectError(
391 error.LineByteCapacityExceeded,
392 compile(&storage, &line_scratch, "<A> : A ", .{ .locale = "C" }),
393 );
394 _ = try load(&storage, &line_scratch, .{
395 .locale = "C",
396 .xcompose_file = included_path,
397 });
398 storage.reset();
399 try std.testing.expectError(
400 error.LineByteCapacityExceeded,
401 load(&storage, &line_scratch, .{
402 .locale = "C",
403 .xcompose_file = over_line_path,
404 }),
405 );
406 }
407
408 test "Compose scratch rejects concurrent use and resets after exhaustion" {
409 comptime {
410 @stardustClaim(
411 @import("alloc_phase").capacity.witness(@import("./workspace/root.zig").Storage, "xkb_compose_scratch_reuse"),
412 null,
413 null,
414 null,
415 null,
416 null,
417 null,
418 );
419 }
420
421 const allocator = std.testing.allocator;
422 var storage = try compose.Storage.init(allocator, .{
423 .sequence_symbols = 1,
424 .text_bytes = 0,
425 });
426 defer storage.deinit(allocator);
427 var scratch_storage = try compose.ScratchStorage.init(allocator, .{
428 .line_bytes = "<A> : A".len,
429 .path_bytes = 1,
430 });
431 defer scratch_storage.deinit(allocator);
432 storage.activate();
433 scratch_storage.activate();
434 try scratch_storage.acquire();
435 try std.testing.expectError(
436 error.ComposeScratchInUse,
437 compile(&storage, &scratch_storage, "<A> : A", .{ .locale = "C" }),
438 );
439 scratch_storage.reset();
440 try std.testing.expectError(
441 error.LineByteCapacityExceeded,
442 compile(&storage, &scratch_storage, "<A> : A ", .{ .locale = "C" }),
443 );
444 _ = try compile(&storage, &scratch_storage, "<A> : A", .{ .locale = "C" });
445 defer storage.reset();
446 }
447
448 test "Compose streaming scratch spans the maximum include depth" {
449 comptime {
450 @stardustClaim(
451 @import("alloc_phase").capacity.witness(@import("./workspace/root.zig").Storage, "xkb_compose_scratch_depth"),
452 null,
453 null,
454 null,
455 null,
456 null,
457 null,
458 );
459 }
460
461 const allocator = std.testing.allocator;
462 var temporary = std.testing.tmpDir(.{});
463 defer temporary.cleanup();
464 const root = try temporary.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);
465 defer allocator.free(root);
466 var paths: [compose.max_include_depth + 2][]u8 = undefined;
467 var path_count: usize = 0;
468 defer for (paths[0..path_count]) |file_path| allocator.free(file_path);
469 for (&paths, 0..) |*file_path, index| {
470 var name_buffer: [16]u8 = undefined;
471 const name = try std.fmt.bufPrint(&name_buffer, "source-{d}", .{index});
472 file_path.* = try std.fs.path.join(allocator, &.{ root, name });
473 path_count += 1;
474 }
475 try temporary.dir.writeFile(std.Options.debug_io, .{
476 .sub_path = "source-5",
477 .data = "<A> : A\n",
478 });
479 var depth: usize = compose.max_include_depth;
480 while (depth > 0) {
481 depth -= 1;
482 const source = try std.fmt.allocPrint(allocator, "include \"{s}\"\n", .{paths[depth + 1]});
483 defer allocator.free(source);
484 var name_buffer: [16]u8 = undefined;
485 const name = try std.fmt.bufPrint(&name_buffer, "source-{d}", .{depth});
486 try temporary.dir.writeFile(std.Options.debug_io, .{
487 .sub_path = name,
488 .data = source,
489 });
490 }
491
492 var storage = try compose.Storage.init(allocator, .{
493 .sequence_symbols = 1,
494 .text_bytes = 0,
495 });
496 defer storage.deinit(allocator);
497 var scratch_storage = try compose.ScratchStorage.init(allocator, compose.default_scratch_limits);
498 defer scratch_storage.deinit(allocator);
499 storage.activate();
500 scratch_storage.activate();
501 _ = try load(&storage, &scratch_storage, .{
502 .locale = "C",
503 .xcompose_file = paths[0],
504 });
505 storage.reset();
506
507 try temporary.dir.writeFile(std.Options.debug_io, .{
508 .sub_path = "source-6",
509 .data = "<A> : A\n",
510 });
511 const excessive = try std.fmt.allocPrint(allocator, "include \"{s}\"\n", .{paths[6]});
512 defer allocator.free(excessive);
513 try temporary.dir.writeFile(std.Options.debug_io, .{
514 .sub_path = "source-5",
515 .data = excessive,
516 });
517 try std.testing.expectError(
518 error.IncludeDepthExceeded,
519 load(&storage, &scratch_storage, .{
520 .locale = "C",
521 .xcompose_file = paths[0],
522 }),
523 );
524 }
525
526 test "Activated Compose compiler and loader perform no backing allocation" {
527 comptime {
528 @stardustClaim(
529 @import("alloc_phase").capacity.witness(@import("./workspace/root.zig").Storage, "xkb_compose_scratch_sealed"),
530 null,
531 null,
532 null,
533 null,
534 null,
535 null,
536 );
537 }
538
539 const allocator = std.testing.allocator;
540 var temporary = std.testing.tmpDir(.{});
541 defer temporary.cleanup();
542 const root = try temporary.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);
543 defer allocator.free(root);
544 try temporary.dir.writeFile(std.Options.debug_io, .{
545 .sub_path = "locale.alias",
546 .data = "friendly en_US.UTF-8\n",
547 });
548 try temporary.dir.writeFile(std.Options.debug_io, .{
549 .sub_path = "compose.dir",
550 .data = "XCompose en_US.UTF-8\n",
551 });
552 try temporary.dir.writeFile(std.Options.debug_io, .{
553 .sub_path = "XCompose",
554 .data = "<A> : A\n",
555 });
556
557 var counting = std.testing.FailingAllocator.init(allocator, .{});
558 var storage = try compose.Storage.init(counting.allocator(), .{
559 .sequence_symbols = 1,
560 .text_bytes = 0,
561 });
562 defer storage.deinit(counting.allocator());
563 var scratch_storage = try compose.ScratchStorage.init(
564 counting.allocator(),
565 compose.default_scratch_limits,
566 );
567 defer scratch_storage.deinit(counting.allocator());
568 storage.activate();
569 scratch_storage.activate();
570 const allocations = counting.alloc_index;
571 const resizes = counting.resize_index;
572 _ = try load(&storage, &scratch_storage, .{
573 .locale = "friendly",
574 .system_directory = root,
575 });
576 defer storage.reset();
577 try std.testing.expectEqual(allocations, counting.alloc_index);
578 try std.testing.expectEqual(resizes, counting.resize_index);
579 }