From 4696dd2f4297006e1eaebd86cd7b68fbde54f2c5 Mon Sep 17 00:00:00 2001 From: Combiler01 Date: Sun, 25 Oct 2026 15:07:47 +0530 Subject: [PATCH] fix(engine): enforce font discovery total budget before file reads and limit after sort --- crates/engine/src/text_fonts.rs | 66 ++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/text_fonts.rs b/crates/engine/src/text_fonts.rs index 5bcc48d535..e99c766bbb 100644 --- a/crates/engine/src/text_fonts.rs +++ b/crates/engine/src/text_fonts.rs @@ -35,15 +35,19 @@ pub fn installed_directories() -> Vec { /// 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 { + 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 { 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 { 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 { 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 { } } 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, ®_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); + } }