Files
ghostty-rs/crates/ghostty-font/src/fontconfig.rs
T
ddidderr 79a7fd4f76 fix(config): match Zig's numeric grammar, NUL handling and diagnostics
Resolves the 6 differential gap groups in docs/rewrite/notes/review-
rv9-codex.md (details in final-Z11.md):
- ghostty_os::zig_fmt ports std.fmt.parseInt/parseUnsigned/parseFloat
  (underscores, 0x/0o/0b prefixes, hex floats, inf/nan) with the Zig std
  tests and replaces every int/float parse in config, keybind
  parameters, ssh-cache and desktop detection.
- Interior NUL no longer panics in font names and ends C strings like
  Zig's backing bytes (fontconfig, C config getters, title actions,
  clipboard callback, relative paths, config-file, shaders, background
  image, GTK CSS).
- GTK new-window arguments are trimmed of ASCII whitespace only.
- Command tokenization uses a port of std.process.ArgIteratorGeneral
  (incl. CR/LF separators) with its std tests.
- UTF-8 validation happens where Zig's field parsers do it, so error
  kinds match (metric modifiers, quick-terminal size, link, ...).

A scripted differential run of 202 fields x ~220 hostile/valid values
against /usr/bin/ghostty (+validate-config and +show-config: exit code,
stdout, stderr) now has zero mismatches besides build-specific defaults
(theme resources path, auto-update-channel, gtk-opengl-debug); it also
surfaced and fixed background-blur failure state, NaN formatting and
NaN/-0 finalize clamps. Regression tests: ghostty-config/tests/
zig_parity.rs.

Test Plan:
- cargo test --workspace --no-fail-fast (3456 passed, exit 0)
- just check-macos (both), just check-windows, just check-wasm
2026-10-12 12:35:37 +02:00

487 lines
15 KiB
Rust

//! Minimal safe wrappers over the fontconfig C API (the subset used by
//! font discovery). Equivalent of `pkg/fontconfig` in the Zig tree.
//!
//! All raw FFI is confined to this module.
#![allow(unsafe_code)]
use std::{
ffi::{CStr, CString, c_int},
ptr::{self, NonNull},
sync::Arc,
};
use fontconfig_sys as sys;
use fontconfig_sys::constants as fc;
/// Errors from fontconfig calls (mirrors `fontconfig.Error`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("fontconfig: no match")]
NoMatch,
#[error("fontconfig: type mismatch")]
TypeMismatch,
#[error("fontconfig: no id")]
NoId,
#[error("fontconfig: out of memory")]
OutOfMemory,
#[error("fontconfig failed")]
FontconfigFailed,
}
fn result_to_error(r: u32) -> Result<(), Error> {
match r {
sys::FcResultMatch => Ok(()),
sys::FcResultNoMatch => Err(Error::NoMatch),
sys::FcResultTypeMismatch => Err(Error::TypeMismatch),
sys::FcResultNoId => Err(Error::NoId),
_ => Err(Error::OutOfMemory),
}
}
/// Font weight constants (`FC_WEIGHT_*`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Weight {
Bold = fc::FC_WEIGHT_BOLD,
}
/// Font slant constants (`FC_SLANT_*`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Slant {
Italic = fc::FC_SLANT_ITALIC,
}
/// Font spacing constants (`FC_*` spacing).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Spacing {
Mono = fc::FC_MONO,
}
/// Pattern property names.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Property {
Family,
Style,
FullName,
File,
Index,
Charset,
Lang,
Size,
Weight,
Slant,
Spacing,
}
impl Property {
fn cstr(self) -> &'static CStr {
match self {
Property::Family => fc::FC_FAMILY,
Property::Style => fc::FC_STYLE,
Property::FullName => fc::FC_FULLNAME,
Property::File => fc::FC_FILE,
Property::Index => fc::FC_INDEX,
Property::Charset => fc::FC_CHARSET,
Property::Lang => fc::FC_LANG,
Property::Size => fc::FC_SIZE,
Property::Weight => fc::FC_WEIGHT,
Property::Slant => fc::FC_SLANT,
Property::Spacing => fc::FC_SPACING,
}
}
}
/// Initialize fontconfig (safe to call multiple times and concurrently).
pub fn init() -> bool {
// SAFETY: FcInit has no preconditions.
unsafe { sys::FcInit() != 0 }
}
/// An owned `FcCharSet`.
pub struct CharSet(NonNull<sys::FcCharSet>);
impl CharSet {
pub fn create() -> CharSet {
// SAFETY: no preconditions; null (OOM) is handled.
let p = unsafe { sys::FcCharSetCreate() };
CharSet(NonNull::new(p).expect("FcCharSetCreate out of memory"))
}
pub fn add_char(&mut self, cp: u32) -> bool {
// SAFETY: self.0 is a valid, uniquely owned charset.
unsafe { sys::FcCharSetAddChar(self.0.as_ptr(), cp) != 0 }
}
pub fn has_char(&self, cp: u32) -> bool {
// SAFETY: self.0 is a valid charset.
unsafe { sys::FcCharSetHasChar(self.0.as_ptr(), cp) != 0 }
}
}
impl Drop for CharSet {
fn drop(&mut self) {
// SAFETY: we own the charset and destroy it once.
unsafe { sys::FcCharSetDestroy(self.0.as_ptr()) }
}
}
/// An owned `FcLangSet`.
pub struct LangSet(NonNull<sys::FcLangSet>);
impl LangSet {
pub fn create() -> LangSet {
// SAFETY: no preconditions.
let p = unsafe { sys::FcLangSetCreate() };
LangSet(NonNull::new(p).expect("FcLangSetCreate out of memory"))
}
pub fn add_lang(&mut self, lang: &str) -> bool {
let Ok(c) = CString::new(lang) else {
return false;
};
// SAFETY: valid langset and NUL-terminated string.
unsafe { sys::FcLangSetAdd(self.0.as_ptr(), c.as_ptr().cast()) != 0 }
}
pub fn has_lang(&self, lang: &str) -> bool {
has_lang_raw(self.0.as_ptr(), lang)
}
}
impl Drop for LangSet {
fn drop(&mut self) {
// SAFETY: we own the langset and destroy it once.
unsafe { sys::FcLangSetDestroy(self.0.as_ptr()) }
}
}
fn has_lang_raw(ls: *const sys::FcLangSet, lang: &str) -> bool {
let Ok(c) = CString::new(lang) else {
return false;
};
// SAFETY: `ls` is a valid langset (callers guarantee), string is NUL-terminated.
unsafe { sys::FcLangSetHasLang(ls, c.as_ptr().cast()) == sys::FcLangEqual }
}
/// An owned `FcPattern`.
pub struct Pattern(NonNull<sys::FcPattern>);
// SAFETY: patterns are never mutated after construction in this crate
// (only read through &self), and fontconfig patterns are plain data.
unsafe impl Send for Pattern {}
// SAFETY: see above; all &self methods are read-only.
unsafe impl Sync for Pattern {}
impl Pattern {
pub fn create() -> Pattern {
// SAFETY: no preconditions.
let p = unsafe { sys::FcPatternCreate() };
Pattern(NonNull::new(p).expect("FcPatternCreate out of memory"))
}
fn as_ptr(&self) -> *mut sys::FcPattern {
self.0.as_ptr()
}
pub fn add_string(&mut self, prop: Property, v: &str) -> bool {
self.add_string_bytes(prop, v.as_bytes())
}
/// [`Pattern::add_string`] for arbitrary bytes (file names). Like Zig's
/// sentinel-terminated C string handoff, the value ends at the first NUL.
pub fn add_string_bytes(&mut self, prop: Property, v: &[u8]) -> bool {
let v = v.split(|&b| b == 0).next().unwrap_or(v);
let Ok(c) = CString::new(v) else {
return false;
};
// SAFETY: valid pattern; property and value are NUL-terminated (value is copied).
unsafe {
sys::FcPatternAddString(self.as_ptr(), prop.cstr().as_ptr(), c.as_ptr().cast()) != 0
}
}
pub fn add_integer(&mut self, prop: Property, v: i32) -> bool {
// SAFETY: valid pattern, NUL-terminated property.
unsafe { sys::FcPatternAddInteger(self.as_ptr(), prop.cstr().as_ptr(), v) != 0 }
}
pub fn add_charset(&mut self, prop: Property, cs: &CharSet) -> bool {
// SAFETY: valid pattern and charset (fontconfig takes its own reference).
unsafe { sys::FcPatternAddCharSet(self.as_ptr(), prop.cstr().as_ptr(), cs.0.as_ptr()) != 0 }
}
/// Get the string value at index `id` as the raw OS string. Use this for
/// file names: they are arbitrary bytes, not necessarily UTF-8.
pub fn get_os_string(&self, prop: Property, id: u32) -> Result<std::ffi::OsString, Error> {
let bytes = self.get_string_bytes(prop, id)?;
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
Ok(std::ffi::OsString::from_vec(bytes))
}
#[cfg(not(unix))]
{
Ok(std::ffi::OsString::from(
String::from_utf8_lossy(&bytes).into_owned(),
))
}
}
/// Get the string value at index `id` as text. Only for properties that
/// fontconfig defines as UTF-8 (family/style/full names); invalid bytes
/// are replaced. File names use [`Pattern::get_os_string`].
pub fn get_string(&self, prop: Property, id: u32) -> Result<String, Error> {
Ok(String::from_utf8_lossy(&self.get_string_bytes(prop, id)?).into_owned())
}
fn get_string_bytes(&self, prop: Property, id: u32) -> Result<Vec<u8>, Error> {
let mut out: *mut sys::FcChar8 = ptr::null_mut();
// SAFETY: valid pattern; out is a valid out pointer.
let r = unsafe {
sys::FcPatternGetString(
self.as_ptr(),
prop.cstr().as_ptr(),
id as c_int,
&raw mut out,
)
};
result_to_error(r)?;
if out.is_null() {
return Err(Error::NoMatch);
}
// SAFETY: fontconfig returned a NUL-terminated string owned by the pattern.
let s = unsafe { CStr::from_ptr(out.cast()) };
Ok(s.to_bytes().to_vec())
}
pub fn get_integer(&self, prop: Property, id: u32) -> Result<i32, Error> {
let mut out: c_int = 0;
// SAFETY: valid pattern; out is a valid out pointer.
let r = unsafe {
sys::FcPatternGetInteger(
self.as_ptr(),
prop.cstr().as_ptr(),
id as c_int,
&raw mut out,
)
};
result_to_error(r)?;
Ok(out)
}
/// Returns whether `cp` is in the charset property (index 0).
pub fn charset_has_char(&self, cp: u32) -> Result<bool, Error> {
let mut out: *mut sys::FcCharSet = ptr::null_mut();
// SAFETY: valid pattern; out is a valid out pointer.
let r = unsafe {
sys::FcPatternGetCharSet(
self.as_ptr(),
Property::Charset.cstr().as_ptr(),
0,
&raw mut out,
)
};
result_to_error(r)?;
if out.is_null() {
return Err(Error::NoMatch);
}
// SAFETY: the charset is owned by the pattern, which outlives this call.
Ok(unsafe { sys::FcCharSetHasChar(out, cp) != 0 })
}
/// Returns whether the langset property (index 0) equals `lang`.
pub fn langset_has_lang(&self, lang: &str) -> Result<bool, Error> {
let mut out: *mut sys::FcLangSet = ptr::null_mut();
// SAFETY: valid pattern; out is a valid out pointer.
let r = unsafe {
sys::FcPatternGetLangSet(
self.as_ptr(),
Property::Lang.cstr().as_ptr(),
0,
&raw mut out,
)
};
result_to_error(r)?;
if out.is_null() {
return Err(Error::NoMatch);
}
Ok(has_lang_raw(out, lang))
}
pub fn default_substitute(&mut self) {
// SAFETY: valid pattern.
unsafe { sys::FcDefaultSubstitute(self.as_ptr()) }
}
}
impl Drop for Pattern {
fn drop(&mut self) {
// SAFETY: we own the pattern reference and release it once.
unsafe { sys::FcPatternDestroy(self.as_ptr()) }
}
}
/// An owned `FcFontSet` as returned by `FcFontSort`.
pub struct FontSet(NonNull<sys::FcFontSet>);
impl FontSet {
pub fn len(&self) -> usize {
// SAFETY: valid font set.
let n = unsafe { (*self.0.as_ptr()).nfont };
usize::try_from(n).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Raw pattern pointer at `i`, borrowed from the set.
fn font(&self, i: usize) -> *mut sys::FcPattern {
assert!(i < self.len());
// SAFETY: bounds checked; `fonts` has `nfont` entries.
unsafe { *(*self.0.as_ptr()).fonts.add(i) }
}
}
impl Drop for FontSet {
fn drop(&mut self) {
// SAFETY: we own the set and destroy it once.
unsafe { sys::FcFontSetDestroy(self.0.as_ptr()) }
}
}
// SAFETY: the set is only read after construction.
unsafe impl Send for FontSet {}
// SAFETY: see above.
unsafe impl Sync for FontSet {}
/// An owned `FcConfig`.
pub struct Config(NonNull<sys::FcConfig>);
// SAFETY: fontconfig configs are internally reference counted and
// thread safe for the read operations we perform.
unsafe impl Send for Config {}
// SAFETY: see above.
unsafe impl Sync for Config {}
impl Config {
/// `FcInitLoadConfigAndFonts`.
pub fn init_load_config_and_fonts() -> Option<Config> {
// SAFETY: no preconditions.
NonNull::new(unsafe { sys::FcInitLoadConfigAndFonts() }).map(Config)
}
/// `FcConfigSubstituteWithPat(config, pat, NULL, FcMatchPattern)`.
pub fn substitute_with_pat_pattern(&self, pat: &mut Pattern) -> bool {
// SAFETY: valid config and pattern; null pattern-for-match is allowed.
unsafe {
sys::FcConfigSubstituteWithPat(
self.0.as_ptr(),
pat.as_ptr(),
ptr::null_mut(),
sys::FcMatchPattern,
) != 0
}
}
/// `FcFontSort`. Returns the sorted set when the result is a match.
pub fn font_sort(&self, pat: &mut Pattern, trim: bool) -> Result<FontSet, Error> {
let mut result: sys::FcResult = 0;
// SAFETY: valid config/pattern; null charset out-pointer is allowed.
let fs = unsafe {
sys::FcFontSort(
self.0.as_ptr(),
pat.as_ptr(),
c_int::from(trim),
ptr::null_mut(),
&raw mut result,
)
};
let fs = NonNull::new(fs).map(FontSet);
if result != sys::FcResultMatch {
return Err(Error::FontconfigFailed);
}
fs.ok_or(Error::FontconfigFailed)
}
/// `FcFontRenderPrepare(config, pat, set[i])`.
pub fn font_render_prepare(
&self,
pat: &mut Pattern,
set: &FontSet,
i: usize,
) -> Result<Pattern, Error> {
// SAFETY: valid config/pattern/font pattern; result is a new owned pattern.
let p = unsafe { sys::FcFontRenderPrepare(self.0.as_ptr(), pat.as_ptr(), set.font(i)) };
NonNull::new(p).map(Pattern).ok_or(Error::FontconfigFailed)
}
}
impl Drop for Config {
fn drop(&mut self) {
// SAFETY: we own the config reference and release it once.
unsafe { sys::FcConfigDestroy(self.0.as_ptr()) }
}
}
/// Shared handle to a config.
pub type SharedConfig = Arc<Config>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn langset_create() {
let fs = LangSet::create();
assert!(!fs.has_lang("und-zsye"));
}
#[test]
fn langset_has_lang_exact_match() {
let mut fs = LangSet::create();
assert!(fs.add_lang("en-US"));
assert!(fs.has_lang("en-US"));
let mut emoji = LangSet::create();
assert!(emoji.add_lang("und-zsye"));
assert!(emoji.has_lang("und-zsye"));
assert!(!emoji.has_lang("en-US"));
}
#[cfg(unix)]
#[test]
fn file_name_keeps_non_utf8_bytes() {
use std::os::unix::ffi::OsStrExt;
let mut p = Pattern::create();
assert!(p.add_string_bytes(Property::File, b"/fonts/a\xffb.ttf"));
let f = p.get_os_string(Property::File, 0).unwrap();
assert_eq!(f.as_bytes(), b"/fonts/a\xffb.ttf");
// The text accessor is for UTF-8 names only and is lossy.
assert!(
p.get_string(Property::File, 0)
.unwrap()
.contains('\u{fffd}')
);
}
#[test]
fn charset_add_has() {
let mut cs = CharSet::create();
assert!(!cs.has_char('A' as u32));
assert!(cs.add_char('A' as u32));
assert!(cs.has_char('A' as u32));
}
#[test]
fn pattern_roundtrip() {
let mut p = Pattern::create();
assert!(p.add_string(Property::Family, "Foo"));
assert!(p.add_integer(Property::Index, 3));
assert_eq!("Foo", p.get_string(Property::Family, 0).unwrap());
assert_eq!(3, p.get_integer(Property::Index, 0).unwrap());
assert_eq!(Err(Error::NoMatch), p.get_string(Property::File, 0));
}
}