diff --git a/crates/lensdb/src/lib.rs b/crates/lensdb/src/lib.rs index cf6b2ec145..e2910125c0 100644 --- a/crates/lensdb/src/lib.rs +++ b/crates/lensdb/src/lib.rs @@ -30,6 +30,9 @@ pub use model::{Distortion, Tca, Vignetting}; const MAX_FILE_BYTES: u64 = 16 << 20; /// Most files read from one directory. const MAX_FILES: usize = 2000; +/// Most bytes of lens names that profiles may copy per parsed document (each calibration owns a copy): a real +/// database copies about a megabyte. +const MAX_PROFILE_COPY_BYTES: usize = 32 << 20; /// Largest radial error (half-diagonal units) accepted when converting a distortion to the DNG polynomial. pub const MAX_WARP_ERROR: f64 = 0.001; /// Largest relative error accepted when converting vignetting to the DNG polynomial. @@ -144,7 +147,8 @@ impl LensDb { let root = xml::parse(xml)?; let Some(top) = root.kid("lensdatabase") else { return Err(Error::Xml("no element".into())) }; let cameras = top.kids("camera").filter_map(camera).collect(); - let lenses = top.kids("lens").flat_map(lens).collect(); + let mut budget = MAX_PROFILE_COPY_BYTES; + let lenses = top.kids("lens").flat_map(|n| lens(n, &mut budget)).collect(); Ok(LensDb { cameras, lenses }) } @@ -272,11 +276,22 @@ fn exif_focal_range(exif: &[String]) -> Option<(f64, f64)> { } fn read_capped(path: &Path) -> Result { - let len = std::fs::metadata(path).map_err(|e| Error::Io(e.to_string()))?.len(); - if len > MAX_FILE_BYTES { + use std::io::Read; + let io = |e: std::io::Error| Error::Io(e.to_string()); + // the opened file is checked, not the pathname: a link or FIFO swapped in after a pathname check is refused too + let file = std::fs::File::open(path).map_err(io)?; + let meta = file.metadata().map_err(io)?; + if !meta.is_file() { + return Err(Error::Io(format!("{}: not a regular file", path.display()))); + } + if meta.len() > MAX_FILE_BYTES { + return Err(Error::TooLarge); + } + let mut bytes = Vec::new(); + file.take(MAX_FILE_BYTES + 1).read_to_end(&mut bytes).map_err(io)?; + if bytes.len() as u64 > MAX_FILE_BYTES { return Err(Error::TooLarge); } - let bytes = std::fs::read(path).map_err(|e| Error::Io(e.to_string()))?; Ok(String::from_utf8_lossy(&bytes).into_owned()) } @@ -369,7 +384,7 @@ fn positive(v: Option) -> Option { } /// One `` element → one profile per `` (newer files give each its own crop factor and aspect). -fn lens(n: &xml::Node) -> Vec { +fn lens(n: &xml::Node, copy_budget: &mut usize) -> Vec { let Some(maker) = n.kid_text("maker") else { return Vec::new() }; let models: Vec = n.kids("model").map(|m| m.text.trim().to_string()).filter(|m| !m.is_empty()).collect(); if models.is_empty() { @@ -379,7 +394,12 @@ fn lens(n: &xml::Node) -> Vec { let aspect = aspect_of(n.kid_text("aspect-ratio")).unwrap_or(1.5); let rectilinear = n.kid_text("type").is_none_or(|t| t.eq_ignore_ascii_case("rectilinear")); let mounts = n.kids("mount").map(|m| m.text.trim().to_string()).filter(|m| !m.is_empty()).collect::>(); + // every calibration owns a copy of the lens's names: calibrations past the document's shared copy budget are left out + let meta_bytes = maker.len() + models.iter().chain(&mounts).map(|m| m.len() + 1).sum::() + 1; + let take = n.kids("calibration").count().min(*copy_budget / meta_bytes); + *copy_budget -= take * meta_bytes; n.kids("calibration") + .take(take) .map(|cal| { let mut p = Profile { maker: maker.to_string(), diff --git a/crates/lensdb/src/tests.rs b/crates/lensdb/src/tests.rs index 71c9a4a0eb..033111de7f 100644 --- a/crates/lensdb/src/tests.rs +++ b/crates/lensdb/src/tests.rs @@ -232,7 +232,8 @@ fn fisheyes_get_no_rectilinear_warp_and_bad_shots_none() { #[test] fn load_dir_reads_xml_files_and_reports_bad_ones() { let dir = std::env::temp_dir().join(format!("lensdb-test-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); + // exclusive: an existing (possibly planted) entry fails the test instead of being written through + std::fs::create_dir(&dir).unwrap(); std::fs::write(dir.join("a.xml"), DB).unwrap(); std::fs::write(dir.join("b.xml"), ""; + let xml = format!("M{name}{}", cal.repeat(2000)); + let db = LensDb::parse(&xml).unwrap(); + assert!(!db.lenses.is_empty()); + assert!(db.lenses.len() * 400_000 <= MAX_PROFILE_COPY_BYTES); +} + +#[cfg(unix)] +#[test] +fn load_dir_refuses_a_non_regular_xml_entry() { + let dir = std::env::temp_dir().join(format!("lensdb-test-dev-{}", std::process::id())); + std::fs::create_dir(&dir).unwrap(); + std::os::unix::fs::symlink("/dev/zero", dir.join("z.xml")).unwrap(); + let (db, errs) = LensDb::load_dir(&dir).unwrap(); + std::fs::remove_dir_all(&dir).unwrap(); + assert!(db.is_empty()); + assert_eq!(errs.len(), 1, "{errs:?}"); +}