mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 14:38:13 -06:00
fix(engine): enforce font discovery total budget before file reads and limit after sort
This commit is contained in:
@@ -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, ®_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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user