#![cfg(not(target_arch = "wasm32"))]
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use cosmic_text::fontdb::{Database, Source};
const CACHE_HEADER: &str = "ruviz-font-cache\tv1";
pub(crate) fn system_font_database() -> Database {
if !(cfg!(target_os = "macos") || cfg!(windows)) {
return scanned_database();
}
if std::env::var_os("RUVIZ_FONT_CACHE").is_some_and(|v| v == "0") {
return scanned_database();
}
let Some(cache_path) = cache_file_path() else {
return scanned_database();
};
if let Some(database) = load_from_cache(&cache_path) {
return database;
}
let database = scanned_database();
let _ = write_cache(&cache_path, &database);
database
}
fn scanned_database() -> Database {
let mut database = Database::new();
database.load_system_fonts();
database
}
fn cache_file_path() -> Option<PathBuf> {
if let Some(dir) = std::env::var_os("RUVIZ_FONT_CACHE") {
return Some(PathBuf::from(dir).join("font-cache-v1.tsv"));
}
let base = if cfg!(target_os = "macos") {
PathBuf::from(std::env::var_os("HOME")?).join("Library/Caches")
} else if cfg!(windows) {
PathBuf::from(std::env::var_os("LOCALAPPDATA")?)
} else {
match std::env::var_os("XDG_CACHE_HOME") {
Some(xdg) if !xdg.is_empty() => PathBuf::from(xdg),
_ => PathBuf::from(std::env::var_os("HOME")?).join(".cache"),
}
};
Some(base.join("ruviz").join("font-cache-v1.tsv"))
}
fn mtime_stamp(path: &Path) -> String {
match fs::metadata(path).and_then(|m| m.modified()) {
Ok(time) => match time.duration_since(std::time::UNIX_EPOCH) {
Ok(d) => d.as_nanos().to_string(),
Err(_) => "-".to_string(),
},
Err(_) => "-".to_string(),
}
}
fn file_stamp(path: &Path) -> Option<(String, u64)> {
let meta = fs::metadata(path).ok()?;
let mtime = meta
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos()
.to_string();
Some((mtime, meta.len()))
}
fn platform_font_roots() -> Vec<PathBuf> {
let mut roots = Vec::new();
if cfg!(target_os = "macos") {
roots.push(PathBuf::from("/Library/Fonts"));
roots.push(PathBuf::from("/System/Library/Fonts"));
roots.push(PathBuf::from("/System/Library/AssetsV2"));
roots.push(PathBuf::from("/Network/Library/Fonts"));
if let Some(home) = std::env::var_os("HOME") {
roots.push(PathBuf::from(home).join("Library/Fonts"));
}
} else if cfg!(windows) {
if let Some(system_root) = std::env::var_os("SYSTEMROOT") {
roots.push(PathBuf::from(system_root).join("Fonts"));
} else {
roots.push(PathBuf::from("C:\\Windows\\Fonts"));
}
if let Some(profile) = std::env::var_os("USERPROFILE") {
let profile = PathBuf::from(profile);
roots.push(profile.join("AppData\\Local\\Microsoft\\Windows\\Fonts"));
roots.push(profile.join("AppData\\Roaming\\Microsoft\\Windows\\Fonts"));
}
}
roots
}
fn push_dirs_recursive(dir: &Path, dirs: &mut BTreeSet<PathBuf>) {
if !dirs.insert(dir.to_path_buf()) {
return;
}
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
if entry.file_type().is_ok_and(|kind| kind.is_dir()) {
push_dirs_recursive(&entry.path(), dirs);
}
}
}
}
fn guard_directories(files: &[PathBuf]) -> Vec<PathBuf> {
let mut dirs = BTreeSet::new();
for root in platform_font_roots() {
if cfg!(target_os = "macos") && root.ends_with("AssetsV2") {
if let Ok(entries) = fs::read_dir(&root) {
for entry in entries.flatten() {
if entry
.file_name()
.to_string_lossy()
.starts_with("com_apple_MobileAsset_Font")
{
push_dirs_recursive(&entry.path(), &mut dirs);
}
}
}
dirs.insert(root);
} else {
push_dirs_recursive(&root, &mut dirs);
}
}
for file in files {
if let Some(parent) = file.parent() {
dirs.insert(parent.to_path_buf());
}
}
dirs.into_iter().collect()
}
fn field_is_cacheable(text: &str) -> bool {
!text.contains('\t') && !text.contains('\n') && !text.contains('\r')
}
fn write_cache(cache_path: &Path, database: &Database) -> std::io::Result<()> {
let mut files: Vec<PathBuf> = Vec::new();
let mut seen: BTreeSet<PathBuf> = BTreeSet::new();
for face in database.faces() {
match &face.source {
Source::File(path) => {
if seen.insert(path.clone()) {
files.push(path.clone());
}
}
_ => return Ok(()),
}
}
let mut out = String::new();
out.push_str(CACHE_HEADER);
out.push('\n');
out.push_str(&format!("faces\t{}\n", database.len()));
for dir in guard_directories(&files) {
let Some(text) = dir.to_str() else {
return Ok(());
};
if !field_is_cacheable(text) {
return Ok(());
}
out.push_str(&format!("dir\t{}\t{}\n", mtime_stamp(&dir), text));
}
for file in &files {
let Some(text) = file.to_str() else {
return Ok(());
};
if !field_is_cacheable(text) {
return Ok(());
}
let Some((mtime, size)) = file_stamp(file) else {
return Ok(());
};
out.push_str(&format!("file\t{mtime}\t{size}\t{text}\n"));
}
if let Some(parent) = cache_path.parent() {
fs::create_dir_all(parent)?;
}
let tmp = cache_path.with_extension("tmp");
fs::write(&tmp, out)?;
fs::rename(&tmp, cache_path)
}
fn load_from_cache(cache_path: &Path) -> Option<Database> {
let content = fs::read_to_string(cache_path).ok()?;
let mut lines = content.lines();
if lines.next()? != CACHE_HEADER {
return None;
}
let expected_faces: usize = lines.next()?.strip_prefix("faces\t")?.parse().ok()?;
let mut files: Vec<PathBuf> = Vec::new();
for line in lines {
let mut fields = line.split('\t');
match fields.next()? {
"dir" => {
let recorded = fields.next()?;
let path = Path::new(fields.next()?);
if mtime_stamp(path) != recorded {
return None;
}
}
"file" => {
let recorded_mtime = fields.next()?;
let recorded_size: u64 = fields.next()?.parse().ok()?;
let path = PathBuf::from(fields.next()?);
let (mtime, size) = file_stamp(&path)?;
if mtime != recorded_mtime || size != recorded_size {
return None;
}
files.push(path);
}
_ => return None,
}
}
let mut database = Database::new();
for file in &files {
database.load_font_file(file).ok()?;
}
if database.len() != expected_faces {
return None;
}
Some(database)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_round_trip_reproduces_the_scanned_database() {
let scanned = scanned_database();
let dir =
std::env::temp_dir().join(format!("ruviz-font-cache-test-{}", std::process::id()));
let cache_path = dir.join("font-cache-v1.tsv");
write_cache(&cache_path, &scanned).expect("cache write");
let rebuilt = load_from_cache(&cache_path).expect("cache load");
let fingerprint = |db: &Database| {
db.faces()
.map(|f| {
(
match &f.source {
Source::File(p) => p.clone(),
_ => PathBuf::new(),
},
f.index,
f.post_script_name.clone(),
f.weight,
f.style,
f.stretch,
f.monospaced,
f.families.clone(),
)
})
.collect::<Vec<_>>()
};
assert_eq!(fingerprint(&scanned), fingerprint(&rebuilt));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn stale_file_stamp_invalidates_the_cache() {
let scanned = scanned_database();
let dir =
std::env::temp_dir().join(format!("ruviz-font-stale-test-{}", std::process::id()));
let cache_path = dir.join("font-cache-v1.tsv");
write_cache(&cache_path, &scanned).expect("cache write");
let content = fs::read_to_string(&cache_path).expect("read cache");
let tampered: String = content
.lines()
.map(|line| {
if let Some(rest) = line.strip_prefix("file\t") {
format!("file\t0{rest}\n")
} else {
format!("{line}\n")
}
})
.collect();
fs::write(&cache_path, tampered).expect("tamper cache");
assert!(load_from_cache(&cache_path).is_none());
let _ = fs::remove_dir_all(&dir);
}
}