diff --git a/Cargo.lock b/Cargo.lock index 1c9e2993a..ae7b24f2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2287,6 +2287,7 @@ dependencies = [ "filmcraft-color", "harfrust", "skrifa", + "tempfile", "unicode-bidi", "unicode-linebreak", "unicode-segmentation", diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 00989623d..811164495 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -21,3 +21,6 @@ unicode-bidi = "0.3.18" unicode-linebreak = "0.1.5" # UAX #29 extended grapheme clusters (MIT OR Apache-2.0). unicode-segmentation = "1.13" + +[dev-dependencies] +tempfile = "3.27" diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index a6c3f444f..add11df73 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -114,16 +114,46 @@ fn walk(dir: &std::path::Path, depth: u32, out: &mut Vec) { let Ok(rd) = std::fs::read_dir(dir) else { return }; for e in rd.flatten() { let p = e.path(); - if p.is_dir() { + let Ok(file_type) = e.file_type() else { continue }; + // Font discovery is bounded to the configured folders: never follow a symlinked folder + // into a loop or an unrelated tree. Symlinked font files (NixOS, home-manager, Debian + // alternatives) still count. + let is_file = if file_type.is_symlink() { std::fs::metadata(&p).is_ok_and(|m| m.is_file()) } else { file_type.is_file() }; + if file_type.is_dir() { if depth < 4 { walk(&p, depth + 1, out); } - } else if p.extension().and_then(|x| x.to_str()).is_some_and(|x| matches!(x.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc")) { + } else if is_file && p.extension().and_then(|x| x.to_str()).is_some_and(|x| matches!(x.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc")) { out.push(p); } } } +#[cfg(all(test, unix))] +mod directory_tests { + use super::walk; + + #[test] + fn directory_source_does_not_follow_symlinked_font_trees() { + use std::os::unix::fs::symlink; + + let root = tempfile::tempdir().unwrap(); + let configured = root.path().join("configured"); + let foreign = root.path().join("foreign"); + std::fs::create_dir_all(&configured).unwrap(); + std::fs::create_dir_all(&foreign).unwrap(); + std::fs::write(configured.join("inside.ttf"), b"").unwrap(); + std::fs::write(foreign.join("outside.ttf"), b"").unwrap(); + symlink(&foreign, configured.join("loop")).unwrap(); + symlink(foreign.join("outside.ttf"), configured.join("linked.ttf")).unwrap(); + + let mut found = Vec::new(); + walk(&configured, 0, &mut found); + found.sort(); + assert_eq!(found, vec![configured.join("inside.ttf"), configured.join("linked.ttf")]); + } +} + impl FontSource for DirectorySource { fn faces(&self) -> Vec { let mut files = Vec::new();