lib/xkb/src/compose/path.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const scratch = @import("workspace/root.zig");
  3 
  4 pub const default_system_directory = "/usr/share/X11/locale";
  5 pub const fallback_locale = "en_US.UTF-8";
  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 = default_system_directory,
 13     io: std.Io = std.Options.debug_io,
 14 };
 15 
 16 pub fn xdg(output: []u8, options: Options) !?[]const u8 {
 17     if (options.xdg_config_home) |directory| {
 18         if (std.fs.path.isAbsolute(directory)) {
 19             return try join(output, &.{ directory, "XCompose" });
 20         }
 21     }
 22     const home_directory = options.home orelse return null;
 23     return try join(output, &.{ home_directory, ".config", "XCompose" });
 24 }
 25 
 26 pub fn home(output: []u8, options: Options) !?[]const u8 {
 27     const directory = options.home orelse return null;
 28     return try join(output, &.{ directory, ".XCompose" });
 29 }
 30 
 31 pub fn locale(storage: *scratch.Storage, output: []u8, options: Options) ![]const u8 {
 32     return try copy(output, try localeValue(storage, options));
 33 }
 34 
 35 pub fn expandInclude(
 36     storage: *scratch.Storage,
 37     output: []u8,
 38     options: Options,
 39     input: []const u8,
 40 ) ![]const u8 {
 41     var builder = Builder.init(output);
 42     var index: usize = 0;
 43     while (index < input.len) {
 44         if (input[index] != '%') {
 45             try builder.appendByte(input[index]);
 46             index += 1;
 47             continue;
 48         }
 49         if (index + 1 >= input.len) return error.InvalidIncludeExpansion;
 50         switch (input[index + 1]) {
 51             '%' => try builder.appendByte('%'),
 52             'H' => try builder.append(options.home orelse return error.MissingHome),
 53             'S' => try builder.append(options.system_directory),
 54             'L' => try builder.append(try localeValue(storage, options)),
 55             else => return error.InvalidIncludeExpansion,
 56         }
 57         index += 2;
 58     }
 59     return builder.value();
 60 }
 61 
 62 fn localeValue(storage: *scratch.Storage, options: Options) ![]const u8 {
 63     const resolved_locale = try resolveLocale(storage, options);
 64     const requested = if (std.mem.eql(u8, resolved_locale, "C"))
 65         fallback_locale
 66     else
 67         resolved_locale;
 68 
 69     const registry_path = try join(
 70         storage.auxiliaryPath(0),
 71         &.{ options.system_directory, "compose.dir" },
 72     );
 73     const relative = (try resolveRegistry(
 74         storage,
 75         options.io,
 76         registry_path,
 77         .right_to_left,
 78         requested,
 79     )) orelse return error.LocaleUnavailable;
 80     const output = storage.auxiliaryPath(1);
 81     if (std.fs.path.isAbsolute(relative)) return try copy(output, relative);
 82     return try join(output, &.{ options.system_directory, relative });
 83 }
 84 
 85 fn resolveLocale(storage: *scratch.Storage, options: Options) ![]const u8 {
 86     const alias_path = try join(
 87         storage.auxiliaryPath(0),
 88         &.{ options.system_directory, "locale.alias" },
 89     );
 90     const resolved = try resolveRegistry(
 91         storage,
 92         options.io,
 93         alias_path,
 94         .left_to_right,
 95         options.locale,
 96     );
 97     return try copy(storage.auxiliaryPath(1), resolved orelse options.locale);
 98 }
 99 
100 const Direction = enum {
101     left_to_right,
102     right_to_left,
103 };
104 
105 fn resolveRegistry(
106     storage: *scratch.Storage,
107     io: std.Io,
108     file_path: []const u8,
109     direction: Direction,
110     name: []const u8,
111 ) !?[]const u8 {
112     var file = std.Io.Dir.cwd().openFile(io, file_path, .{}) catch |err| switch (err) {
113         error.FileNotFound => return null,
114         else => return err,
115     };
116     defer file.close(io);
117     const stat = try file.stat(io);
118     if (stat.kind != .file) return null;
119     if (stat.size >= scratch.max_registry_bytes) return error.StreamTooLong;
120     var reader = file.reader(io, storage.registryLine());
121     while (true) {
122         const raw_line = reader.interface.takeDelimiter('\n') catch |err| switch (err) {
123             error.ReadFailed => return reader.err.?,
124             error.StreamTooLong => return error.LineByteCapacityExceeded,
125         } orelse return null;
126         if (raw_line.len > storage.lineLimit()) return error.LineByteCapacityExceeded;
127         if (resolveLine(raw_line, direction, name)) |resolved| return resolved;
128     }
129 }
130 
131 fn resolveLine(raw_line: []const u8, direction: Direction, name: []const u8) ?[]const u8 {
132     var line = std.mem.trim(u8, raw_line, " \t\x0b\x0c\r\n");
133     if (line.len == 0 or line[0] == '#') return null;
134 
135     const left_end = std.mem.indexOfAny(u8, line, " \t\x0b\x0c\r:") orelse line.len;
136     if (left_end == 0) return null;
137     const left = line[0..left_end];
138     line = line[left_end..];
139     line = std.mem.trimStart(u8, line, " \t\x0b\x0c\r");
140     if (line.len > 0 and line[0] == ':') line = line[1..];
141     line = std.mem.trimStart(u8, line, " \t\x0b\x0c\r");
142     const right_end = std.mem.indexOfAny(u8, line, " \t\x0b\x0c\r") orelse line.len;
143     if (right_end == 0) return null;
144     const right = line[0..right_end];
145 
146     return switch (direction) {
147         .left_to_right => if (std.mem.eql(u8, left, name)) right else null,
148         .right_to_left => if (std.mem.eql(u8, right, name)) left else null,
149     };
150 }
151 
152 fn join(output: []u8, parts: []const []const u8) ![]const u8 {
153     var builder = Builder.init(output);
154     const first_index = for (parts, 0..) |part, index| {
155         if (part.len != 0) break index;
156     } else return builder.value();
157     try builder.append(parts[first_index]);
158     var previous = parts[first_index];
159     for (parts[first_index + 1 ..]) |part| {
160         if (part.len == 0) continue;
161         const previous_separator = std.fs.path.isSep(previous[previous.len - 1]);
162         const current_separator = std.fs.path.isSep(part[0]);
163         if (!previous_separator and !current_separator) {
164             try builder.appendByte(std.fs.path.sep);
165         }
166         try builder.append(if (previous_separator and current_separator) part[1..] else part);
167         previous = part;
168     }
169     return builder.value();
170 }
171 
172 fn copy(output: []u8, input: []const u8) ![]const u8 {
173     var builder = Builder.init(output);
174     try builder.append(input);
175     return builder.value();
176 }
177 
178 const Builder = struct {
179     bytes: []u8,
180     length: usize = 0,
181 
182     fn init(bytes: []u8) Builder {
183         return .{ .bytes = bytes };
184     }
185 
186     fn appendByte(self: *Builder, byte: u8) !void {
187         if (self.length == self.bytes.len) return error.PathByteCapacityExceeded;
188         self.bytes[self.length] = byte;
189         self.length += 1;
190     }
191 
192     fn append(self: *Builder, input: []const u8) !void {
193         if (input.len > self.bytes.len - self.length) {
194             return error.PathByteCapacityExceeded;
195         }
196         @memcpy(self.bytes[self.length..][0..input.len], input);
197         self.length += input.len;
198     }
199 
200     fn value(self: *const Builder) []const u8 {
201         return self.bytes[0..self.length];
202     }
203 };
204 
205 test "locale aliases and Compose registry resolve through bounded scratch" {
206     const allocator = std.testing.allocator;
207     var temporary = std.testing.tmpDir(.{});
208     defer temporary.cleanup();
209     const root = try temporary.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);
210     defer allocator.free(root);
211 
212     try temporary.dir.writeFile(std.Options.debug_io, .{
213         .sub_path = "locale.alias",
214         .data = "friendly en_US.UTF-8\n",
215     });
216     try temporary.dir.writeFile(std.Options.debug_io, .{
217         .sub_path = "compose.dir",
218         .data = "custom/Compose en_US.UTF-8\n",
219     });
220 
221     var storage = try scratch.Storage.init(allocator, scratch.default_limits);
222     defer storage.deinit(allocator);
223     storage.activate();
224     try storage.acquire();
225     defer storage.reset();
226     const resolved = try locale(&storage, storage.filePath(0), .{
227         .locale = "friendly",
228         .system_directory = root,
229     });
230     const expected = try std.fs.path.join(allocator, &.{ root, "custom/Compose" });
231     defer allocator.free(expected);
232     try std.testing.expectEqualStrings(expected, resolved);
233 }
234 
235 test "include expansion uses bounded home locale system and percent substitutions" {
236     const allocator = std.testing.allocator;
237     var temporary = std.testing.tmpDir(.{});
238     defer temporary.cleanup();
239     const root = try temporary.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);
240     defer allocator.free(root);
241 
242     try temporary.dir.writeFile(std.Options.debug_io, .{
243         .sub_path = "compose.dir",
244         .data = "Compose en_US.UTF-8\n",
245     });
246     var storage = try scratch.Storage.init(allocator, scratch.default_limits);
247     defer storage.deinit(allocator);
248     storage.activate();
249     try storage.acquire();
250     defer storage.reset();
251     const expanded = try expandInclude(
252         &storage,
253         storage.filePath(1),
254         .{ .locale = "C", .home = "/home/example", .system_directory = root },
255         "%H|%S|%L|%%",
256     );
257 
258     const locale_path = try std.fs.path.join(allocator, &.{ root, "Compose" });
259     defer allocator.free(locale_path);
260     const expected = try std.fmt.allocPrint(
261         allocator,
262         "/home/example|{s}|{s}|%",
263         .{ root, locale_path },
264     );
265     defer allocator.free(expected);
266     try std.testing.expectEqualStrings(expected, expanded);
267 }
268 
269 test "unregistered locale does not silently select the fallback" {
270     const allocator = std.testing.allocator;
271     var temporary = std.testing.tmpDir(.{});
272     defer temporary.cleanup();
273     const root = try temporary.dir.realPathFileAlloc(std.Options.debug_io, ".", allocator);
274     defer allocator.free(root);
275 
276     try temporary.dir.writeFile(std.Options.debug_io, .{
277         .sub_path = "compose.dir",
278         .data = "Compose en_US.UTF-8\n",
279     });
280     var storage = try scratch.Storage.init(allocator, scratch.default_limits);
281     defer storage.deinit(allocator);
282     storage.activate();
283     try storage.acquire();
284     defer storage.reset();
285     try std.testing.expectError(error.LocaleUnavailable, locale(
286         &storage,
287         storage.filePath(0),
288         .{ .locale = "blabla", .system_directory = root },
289     ));
290 }