Something went wrong. Try again.
A Zig library that finds fonts as fontconfig does, without it: the same configuration, OpenType and bitmap fonts, and the same answers.
Something went wrong. Try again.
17 kB · 420 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2002 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! Finding and reading configuration files, from `fccfg.c` and the file//! half of `fcxml.c`: `FONTCONFIG_FILE` and `FONTCONFIG_PATH`, `<include>`//! of files and of `conf.d` directories, the `xdg`, `relative` and//! `default` prefixes, `~`, and the sysroot.
const std = @import("std");const Io = std.Io;const Allocator = std.mem.Allocator;const str = @import("str.zig");const Pattern = @import("Pattern.zig");const rules = @import("rules.zig");const xml = @import("xml.zig");const Config = @import("Config.zig");
/// Configuration files larger than this are refused.const max_config_size = 16 * 1024 * 1024;
pub const Loader = struct { config: *Config, /// Null to read nothing from the file system: every file is then /// missing, which is how a configuration held in memory is loaded. io: ?Io, arena: Allocator,
pub fn message(l: *Loader, severity: xml.Severity, file: ?[]const u8, line: ?usize, comptime fmt: []const u8, args: anytype) void { const text = std.fmt.allocPrint(l.arena, fmt, args) catch return; l.config.messages.append(l.arena, .{ .severity = severity, .file = file, .line = line, .text = text, }) catch {}; }
// ---- environment
fn env(l: *Loader) *const Config.Environment { return &l.config.env; }
/// `FcConfigHome`. pub fn home(l: *Loader) ?[]const u8 { return l.env().home; }
/// `FcConfigXdgCacheHome`. pub fn xdgCacheHome(l: *Loader) ?[]const u8 { if (l.env().xdg_cache_home) |e| if (e.len > 0) return e; return std.mem.concat(l.arena, u8, &.{ l.home() orelse "", "/.cache" }) catch null; }
/// `FcConfigXdgConfigHome`. Unlike the cache home, an empty /// `XDG_CONFIG_HOME` is used as it is. pub fn xdgConfigHome(l: *Loader) ?[]const u8 { if (l.env().xdg_config_home) |e| return e; return std.mem.concat(l.arena, u8, &.{ l.home() orelse "", "/.config" }) catch null; }
/// `FcConfigXdgDataHome`. pub fn xdgDataHome(l: *Loader) ?[]const u8 { if (l.env().xdg_data_home) |e| return e; return std.mem.concat(l.arena, u8, &.{ l.home() orelse "", "/.local/share" }) catch null; }
/// `FcConfigXdgDataDirs`. pub fn xdgDataDirs(l: *Loader) Allocator.Error![]const []const u8 { var out: std.ArrayList([]const u8) = .empty; const e = l.env().xdg_data_dirs orelse ""; if (e.len == 0) { try out.append(l.arena, "/usr/local/share"); try out.append(l.arena, "/usr/share"); return out.items; } var it = std.mem.splitScalar(u8, e, ':'); while (it.next()) |raw| { if (raw.len == 0) continue; var len = raw.len; if (raw[len - 1] == '/') { len -= 1; while (len > 1 and raw[len - 1] == '/') len -= 1; } const s = raw[0..len]; if (!contains(out.items, s)) try out.append(l.arena, s); } return out.items; }
fn contains(list: []const []const u8, s: []const u8) bool { for (list) |x| if (std.mem.eql(u8, x, s)) return true; return false; }
// ---- paths
/// `FcStrCopyFilename`: `~` expanded and the path made absolute and /// canonical, without touching the file system. pub fn copyFilename(l: *Loader, s: []const u8) Allocator.Error!?[]const u8 { if (s.len > 0 and s[0] == '~') { const h = l.home() orelse return null; return try canonFilename(l.arena, try std.mem.concat(l.arena, u8, &.{ h, s[1..] }), l.config.cwd); } return try canonFilename(l.arena, s, l.config.cwd); }
/// `_get_real_paths_from_prefix`: where a `<dir>` with a `prefix` points. /// `xdg` gives the data home first, then every data directory. pub fn pathsFromPrefix(l: *Loader, p: *xml.Parser, path: []const u8, prefix: ?[]const u8, element: []const u8) Allocator.Error!?[]const []const u8 { var parent: ?[]const u8 = null; var extra: []const []const u8 = &.{}; if (prefix) |pre| { if (std.mem.eql(u8, pre, "xdg")) { parent = l.xdgDataHome() orelse return null; extra = try l.xdgDataDirs(); } else if (std.mem.eql(u8, pre, "default") or std.mem.eql(u8, pre, "cwd")) { // Used as it is. } else if (std.mem.eql(u8, pre, "relative")) { const real = try l.realPath(p.filename) orelse return null; parent = dirname(real); } } else if (!std.mem.startsWith(u8, path, "/") and !std.mem.startsWith(u8, path, "~")) { l.message(.warning, p.filename, null, "Use of ambiguous path in <{s}> element. please add prefix=\"cwd\" if current behavior is desired.", .{element}); } var out: std.ArrayList([]const u8) = .empty; try out.append(l.arena, if (parent) |pa| try std.mem.concat(l.arena, u8, &.{ pa, "/", path }) else path); for (extra) |e| { const s = try std.mem.concat(l.arena, u8, &.{ e, "/", path }); // FcStrSet keeps one copy of each string. if (!contains(out.items, s)) try out.append(l.arena, s); } return out.items; }
fn realPath(l: *Loader, path: []const u8) Allocator.Error!?[]const u8 { const io = l.io orelse return null; const real = Io.Dir.cwd().realPathFileAlloc(io, path, l.arena) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return null, }; return try l.copyFilename(real); }
// ---- what the configuration does
/// `FcConfigAddFontDir`: a directory added again moves to the end. pub fn addFontDir(l: *Loader, dir: []const u8, map: ?[]const u8, salt: ?[]const u8) Allocator.Error!void { const path = try l.copyFilename(dir) orelse return; const mapped: ?[]const u8 = if (map) |m| try l.copyFilename(m) orelse return else null; var dirs = &l.config.font_dirs; var i: usize = 0; while (i < dirs.items.len) { if (std.mem.eql(u8, dirs.items[i].path, path)) { _ = dirs.orderedRemove(i); } else i += 1; } try dirs.append(l.arena, .{ .path = path, .map = mapped, .salt = if (salt) |s| try l.arena.dupe(u8, s) else null, }); }
pub fn resetFontDirs(l: *Loader) void { l.config.font_dirs.clearRetainingCapacity(); }
pub fn addCacheDir(l: *Loader, dir: []const u8) Allocator.Error!void { const path = try l.copyFilename(dir) orelse return; if (!contains(l.config.cache_dirs.items, path)) try l.config.cache_dirs.append(l.arena, path); }
/// `FcConfigGlobAdd`: a glob is canonicalized like a file name, unless it /// had no directory in it to begin with. pub fn addGlob(l: *Loader, glob: []const u8, accept: bool) Allocator.Error!void { const real = try l.copyFilename(glob) orelse return; var s = real; if (l.config.cwd) |cwd| { if (std.mem.startsWith(u8, real, cwd) and real.len > cwd.len and real[cwd.len] == '/') s = real[cwd.len + 1 ..]; } const set = if (accept) &l.config.accept_globs else &l.config.reject_globs; if (!contains(set.items, s)) try set.append(l.arena, s); }
pub fn addPattern(l: *Loader, pat: *Pattern, accept: bool) Allocator.Error!void { const set = if (accept) &l.config.accept_patterns else &l.config.reject_patterns; try set.append(l.arena, pat); }
pub fn setRescanInterval(l: *Loader, interval: i32) void { l.config.rescan_interval = interval; }
pub fn addRules(l: *Loader, rs: *const rules.RuleSets) Allocator.Error!void { inline for (.{ .pattern, .font, .scan }) |kind| { try l.config.rules.get(kind).appendSlice(l.arena, rs.getConst(kind)); } }
// ---- reading files
/// `FcConfigFileExists`: `dir` and `file` joined, if the result can be /// read. fn fileExists(l: *Loader, dir: ?[]const u8, file: []const u8) Allocator.Error!?[]const u8 { const io = l.io orelse return null; const d = dir orelse ""; const path = if ((d.len == 0 or d[d.len - 1] != '/') and (file.len == 0 or file[0] != '/')) try std.mem.concat(l.arena, u8, &.{ d, "/", file }) else try std.mem.concat(l.arena, u8, &.{ d, file }); Io.Dir.cwd().access(io, path, .{ .read = true }) catch return null; return path; }
/// `FcConfigGetPath`: `FONTCONFIG_PATH`, then the built-in directory. fn searchPath(l: *Loader) Allocator.Error![]const []const u8 { var out: std.ArrayList([]const u8) = .empty; if (l.env().fontconfig_path) |e| { var rest = e; while (rest.len > 0) { const colon = std.mem.findScalar(u8, rest, ':') orelse rest.len; try out.append(l.arena, rest[0..colon]); rest = if (colon < rest.len) rest[colon + 1 ..] else rest[colon..]; } } try out.append(l.arena, l.config.options.config_path); return out.items; }
/// `FcConfigGetFilename`: where a configuration file named in an /// `<include>` (or `FONTCONFIG_FILE`, or the built-in default, for a /// null name) actually is. fn getFilename(l: *Loader, url_in: ?[]const u8) Allocator.Error!?[]const u8 { var url = url_in orelse ""; if (url.len == 0) url = l.env().fontconfig_file orelse l.config.options.config_file; var sysroot = l.config.sysroot; if (url.len > 0 and url[0] == '/') { if (sysroot) |sr| { if (std.mem.startsWith(u8, url, sr)) sysroot = null; } return l.fileExists(sysroot, url); } if (url.len > 0 and url[0] == '~') { const h = l.home() orelse return null; const base = if (sysroot) |sr| try std.mem.concat(l.arena, u8, &.{ sr, "/", h }) else h; return l.fileExists(base, url[1..]); } for (try l.searchPath()) |dir| { const base = if (sysroot) |sr| try std.mem.concat(l.arena, u8, &.{ sr, "/", dir }) else dir; if (try l.fileExists(base, url)) |f| return f; } return null; }
/// `FcConfigRealFilename`: the file name with one symbolic link /// followed, for telling whether a file has been read already. fn realFilename(l: *Loader, filename: []const u8) Allocator.Error![]const u8 { const io = l.io orelse return filename; var buf: [4096]u8 = undefined; const n = Io.Dir.cwd().readLink(io, filename, &buf) catch return filename; const target = buf[0..n]; if (target.len > 0 and target[0] == '/') return try l.arena.dupe(u8, target); const joined = try std.mem.concat(l.arena, u8, &.{ dirname(filename), "/", target }); return try canonFilename(l.arena, joined, l.config.cwd); }
fn isDir(l: *Loader, path: []const u8) bool { const io = l.io orelse return false; const st = Io.Dir.cwd().statFile(io, path, .{}) catch return false; return st.kind == .directory; }
/// `_FcConfigParse`. Returns false, if `complain` is set, when the file /// cannot be found or read or has an error in it. pub fn parseFile(l: *Loader, name: ?[]const u8, complain: bool) Allocator.Error!bool { const filename = try l.getFilename(name) orelse { if (complain) l.message(.@"error", null, null, "Cannot load config file \"{s}\": File not found", .{name orelse "(default)"}); return !complain; }; const real = try l.realFilename(filename); if (contains(l.config.available_config_files.items, real)) return true; try l.config.config_files.append(l.arena, filename); try l.config.available_config_files.append(l.arena, real);
if (l.isDir(real)) return l.parseDir(name orelse filename, real, complain);
const io = l.io.?; const data = Io.Dir.cwd().readFileAlloc(io, real, l.arena, .limited(max_config_size)) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => { if (complain) l.message(.@"error", null, null, "Cannot load config file \"{s}\": Unable to open {s}", .{ name orelse filename, real }); return !complain; }, }; return l.parseMemory(filename, data, complain); }
/// `FcConfigParseAndLoadDir`: every `[0-9]*.conf` in a directory, in /// byte order of their names. fn parseDir(l: *Loader, name: []const u8, dir: []const u8, complain: bool) Allocator.Error!bool { const io = l.io orelse return !complain; var d = Io.Dir.cwd().openDir(io, dir, .{ .iterate = true }) catch { if (complain) l.message(.@"error", null, null, "Cannot open config dir \"{s}\"", .{name}); return !complain; }; defer d.close(io); if (!contains(l.config.config_dirs.items, dir)) try l.config.config_dirs.append(l.arena, dir);
var files: std.ArrayList([]const u8) = .empty; var it = d.iterate(); while (it.next(io) catch null) |entry| { const n = entry.name; if (n.len > 5 and n[0] >= '0' and n[0] <= '9' and std.mem.endsWith(u8, n, ".conf")) { try files.append(l.arena, try std.mem.concat(l.arena, u8, &.{ dir, "/", n })); } } std.mem.sort([]const u8, files.items, {}, struct { fn lessThan(_: void, a: []const u8, b: []const u8) bool { return std.mem.order(u8, a, b) == .lt; } }.lessThan); var ret = true; for (files.items) |f| { ret = try l.parseFile(f, complain); if (!ret) break; } return ret or !complain; }
/// `FcConfigParseAndLoadFromMemoryInternal`. pub fn parseMemory(l: *Loader, filename: []const u8, data: []const u8, complain: bool) Allocator.Error!bool { var p: xml.Parser = .{ .loader = l, .arena = l.arena, .filename = filename, .src = data, .reader = .init(data), }; const parsed = try p.run(); // A document that is not well-formed loses its rules since the last // <include>, as fontconfig loses them. if (parsed) try l.addRules(&p.ruleset); if ((!parsed or p.@"error") and complain) { l.message(.@"error", null, null, "Cannot load config file from {s}", .{filename}); return false; } return true; }};
pub fn dirname(path: []const u8) []const u8 { const slash = std.mem.findScalarLast(u8, path, '/') orelse return "."; return path[0..slash];}
/// `FcStrCanonFilename`: a relative path made absolute against `cwd`, then/// `//`, `/.` and `/..` squashed, and a trailing `/` dropped.pub fn canonFilename(gpa: Allocator, s: []const u8, cwd: ?[]const u8) Allocator.Error![]const u8 { if (s.len > 0 and s[0] == '/') return canonAbsoluteFilename(gpa, s); const base = cwd orelse return canonAbsoluteFilename(gpa, s); const full = try std.mem.concat(gpa, u8, &.{ base, "/", s }); defer gpa.free(full); return canonAbsoluteFilename(gpa, full);}
/// `FcStrCanonAbsoluteFilename`.fn canonAbsoluteFilename(gpa: Allocator, s: []const u8) Allocator.Error![]const u8 { const file = try gpa.alloc(u8, s.len + 1); var f: usize = 0; var slash: ?usize = null; var i: usize = 0; while (true) : (i += 1) { const c: u8 = if (i < s.len) s[i] else 0; if (c == '/' or c == 0) { if (slash) |sl| { switch (i - sl) { 1 => f -= 1, 2 => if (std.mem.eql(u8, s[sl..i], "/.")) { f -= 2; }, 3 => if (std.mem.eql(u8, s[sl..i], "/..")) { f -= 3; while (f > 0) { f -= 1; if (file[f] == '/') break; } }, else => {}, } } slash = i; } file[f] = c; f += 1; if (c == 0) break; } defer gpa.free(file); return gpa.dupe(u8, file[0 .. f - 1]);}
test "canonical file names" { const gpa = std.testing.allocator; const cases = [_][2][]const u8{ .{ "/usr/share/fonts/", "/usr/share/fonts" }, .{ "/usr//share/./fonts", "/usr/share/fonts" }, .{ "/usr/share/../lib/fonts", "/usr/lib/fonts" }, .{ "/", "" }, }; for (cases) |c| { const got = try canonFilename(gpa, c[0], null); defer gpa.free(got); try std.testing.expectEqualStrings(c[1], got); } const rel = try canonFilename(gpa, "fonts/../x", "/home/u"); defer gpa.free(rel); try std.testing.expectEqualStrings("/home/u/x", rel);}