fix(engine): enforce font discovery total budget before file reads and limit after sort

This commit is contained in:
Combiler01
2026-10-25 15:07:47 +05:30
parent 338211cdbf
commit 4696dd2f42
+60 -6
View File
@@ -35,15 +35,19 @@ pub fn installed_directories() -> Vec<PathBuf> {
/// Bounded, symlink-free scan. Only standalone, openly licensed, embeddable faces are listed.
/// The caller controls roots (automation resolves confinement before calling this).
pub fn discover(roots: &[PathBuf]) -> Vec<FontFile> {
discover_budgeted(roots, 64u64 << 20)
}
/// Bounded, symlink-free scan with an explicit total-bytes budget.
pub fn discover_budgeted(roots: &[PathBuf], mut budget: u64) -> Vec<FontFile> {
use std::io::Read;
let mut pending: Vec<_> = roots.iter().take(16).map(|r| (r.clone(), 0usize)).collect();
let mut files = Vec::new();
let mut entries = 0usize;
let mut budget = 64u64 << 20;
let mut seen = std::collections::HashSet::new();
let mut seen_face_content = std::collections::HashSet::new();
while let Some((path, depth)) = pending.pop() {
if entries >= 4096 || files.len() >= 500 || budget == 0 {
if entries >= 4096 || budget == 0 {
break;
}
entries += 1;
@@ -59,6 +63,10 @@ pub fn discover(roots: &[PathBuf]) -> Vec<FontFile> {
pending.extend(sorted_children.into_iter().rev().map(|p| (p, depth + 1)));
}
} else if meta.is_file() && meta.len() <= (16 << 20) && font_extension(&path) {
if meta.len() > budget {
continue;
}
budget = budget.saturating_sub(meta.len());
let mut bytes = Vec::new();
let read = std::fs::File::open(&path).and_then(|f| f.take((16 << 20) + 1).read_to_end(&mut bytes));
if read.is_err() {
@@ -67,10 +75,6 @@ pub fn discover(roots: &[PathBuf]) -> Vec<FontFile> {
if pdfcraft_fonts::embedding::validate_document_input(&bytes).is_err() {
continue;
}
if meta.len() > budget {
continue;
}
budget = budget.saturating_sub(meta.len());
if let Ok((family, face)) = pdfcraft_fonts::embedding::names(&bytes) {
let key = font_content_key(&bytes);
if seen_face_content.insert((face.clone(), key)) {
@@ -80,6 +84,7 @@ pub fn discover(roots: &[PathBuf]) -> Vec<FontFile> {
}
}
files.sort_by(|a, b| a.face.cmp(&b.face).then(a.path.cmp(&b.path)));
files.truncate(500);
files
}
@@ -170,4 +175,53 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
}
#[test]
fn test_discover_budgeted_enforces_budget_before_read_and_deterministic_order() {
let root = std::env::temp_dir().join(format!("pdfcraft-font-budget-test-{}", std::process::id()));
let _ = std::fs::create_dir_all(&root);
let inter_reg = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/fonts/Inter-Regular.ttf");
let inter_med = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/fonts/Inter-Medium.ttf");
assert!(inter_reg.exists() && inter_med.exists());
let reg_len = std::fs::metadata(&inter_reg).unwrap().len();
// Write an invalid candidate font first (sorted alphabetically before Inter-Regular)
let invalid_path = root.join("00_candidate_invalid.ttf");
let invalid_size = 400_000usize;
let _ = std::fs::write(&invalid_path, vec![0u8; invalid_size]);
let reg_path = root.join("01_Inter-Regular.ttf");
let _ = std::fs::copy(&inter_reg, &reg_path);
// Budget is 500_000 bytes.
// Candidate invalid file takes 400_000 bytes.
// Remaining budget should be 100_000 bytes, which is less than Inter-Regular's ~411 KB.
// Therefore, Inter-Regular must NOT be read or included.
let budget = 500_000u64;
let run1 = discover_budgeted(std::slice::from_ref(&root), budget);
let run2 = discover_budgeted(std::slice::from_ref(&root), budget);
assert_eq!(run1.len(), 0, "candidate invalid file must consume budget before read so later file cannot fit");
assert_eq!(run1.len(), run2.len());
// Now test with enough budget for one valid font:
// Budget = invalid_size + reg_len + 10: invalid consumes 400_000, then Inter-Regular (411 KB) fits,
// but Inter-Medium (417 KB) exceeds the remaining budget.
let med_path = root.join("02_Inter-Medium.ttf");
let _ = std::fs::copy(&inter_med, &med_path);
let single_budget = (invalid_size as u64) + reg_len + 10;
let run3 = discover_budgeted(std::slice::from_ref(&root), single_budget);
let run4 = discover_budgeted(std::slice::from_ref(&root), single_budget);
let _ = std::fs::remove_dir_all(&root);
assert_eq!(run3.len(), 1, "only files that fit within budget are read and returned");
assert_eq!(run3[0].face, "Inter-Regular");
assert_eq!(run3.len(), run4.len());
assert_eq!(run3[0].face, run4[0].face);
assert_eq!(run3[0].path, run4[0].path);
}
}