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 }