diff --git a/crates/engine/src/cmd/fileio/importer.rs b/crates/engine/src/cmd/fileio/importer.rs index 9fc667c4a..00dfc8d97 100644 --- a/crates/engine/src/cmd/fileio/importer.rs +++ b/crates/engine/src/cmd/fileio/importer.rs @@ -58,22 +58,36 @@ pub(super) fn pick(name: &str, bytes: &[u8], p: &Value) -> Result Result> { + /// How much of the file [`detect`] is given: more than its content checks read. + const SNIFF_BYTES: usize = 64 << 10; + let ext = extension(path); let explicit = named(p)?.is_some(); if !explicit && (super::readable_format(&ext).is_some() || import::for_extension(&ext).is_empty()) { return super::read_file(path); } - let head = super::read_file_head(path, import::MAX_INPUT_BYTES + 1)?; - if head.len() <= import::MAX_INPUT_BYTES { + let head = super::read_file_head(path, SNIFF_BYTES)?; + if head.len() < SNIFF_BYTES { return Ok(head); } if !explicit && detect(path, &head).is_some() { return super::read_file(path); } - Err(err(format!("can't open `{}`: {}", file_name(path), import::too_large()))) + let too_large = || err(format!("can't open `{}`: {}", file_name(path), import::too_large())); + let size = std::fs::metadata(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?.len(); + if size > import::MAX_INPUT_BYTES as u64 { + return Err(too_large()); + } + // The file can grow after the size is read. + let file = super::read_file_head(path, import::MAX_INPUT_BYTES + 1)?; + if file.len() > import::MAX_INPUT_BYTES { + return Err(too_large()); + } + Ok(file) } #[cfg(target_arch = "wasm32")] diff --git a/crates/engine/src/cmd/fileio/tests.rs b/crates/engine/src/cmd/fileio/tests.rs index fca92d73f..9755cab52 100644 --- a/crates/engine/src/cmd/fileio/tests.rs +++ b/crates/engine/src/cmd/fileio/tests.rs @@ -878,3 +878,26 @@ fn a_photo_places_the_way_its_exif_orientation_says() { let red = |p: &image::Rgb| p[0] > 150 && p[1] < 80; assert!(red(px.get_pixel(4, 3)) && !red(px.get_pixel(4, 12)), "the left half is on top"); } + +/// With `importer` named, a file up to the importer's limit is read whole and a larger one is an +/// error, whatever its content. +#[test] +fn read_path_for_a_named_importer_stops_at_the_limit() { + let limit = vectorcraft_plugins::import::MAX_INPUT_BYTES as u64; + let dir = tmp_dir("read-path-limit"); + let path = dir.join("drawing.tuv"); + let name = path.to_string_lossy().into_owned(); + let named = json!({"importer": "plugin.test.notinstalled"}); + let sized = |len: u64| { + std::fs::write(&path, b"\x89PNG\r\n\x1a\n").unwrap(); + std::fs::OpenOptions::new().write(true).open(&path).unwrap().set_len(len).unwrap(); + }; + for len in [8, 64 << 10, 70_000, limit] { + sized(len); + assert_eq!(importer::read_path(&name, &named).unwrap().len() as u64, len); + } + sized(limit + 1); + let e = importer::read_path(&name, &named).unwrap_err(); + assert!(e.to_string().contains("larger than") && e.to_string().contains("drawing.tuv"), "{e}"); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/crates/engine/tests/plugin_import.rs b/crates/engine/tests/plugin_import.rs index 7f62de2b7..2abe7f320 100644 --- a/crates/engine/tests/plugin_import.rs +++ b/crates/engine/tests/plugin_import.rs @@ -316,3 +316,23 @@ fn a_file_past_the_input_limit_is_refused() { assert!(s.documents().is_empty()); let _ = std::fs::remove_dir_all(&dir); } + +/// A file past the importer's input limit whose start shows a built-in format opens as that format, +/// whatever its extension; named, the importer's limit applies to it. +#[test] +fn a_built_in_file_past_the_input_limit_still_opens() { + let dir = tmp_dir("largebuiltin"); + let mut s = Session::new(); + echo(&mut s, "test.largepng", r#"["tsp"]"#); + let path = dir.join("big.tsp"); + let mut png = std::io::Cursor::new(vec![]); + image::RgbaImage::new(2, 2).write_to(&mut png, image::ImageFormat::Png).unwrap(); + std::fs::write(&path, png.get_ref()).unwrap(); + std::fs::OpenOptions::new().write(true).open(&path).unwrap().set_len(vectorcraft_plugins::import::MAX_INPUT_BYTES as u64 + 1).unwrap(); + let e = s.execute("document.open", &json!({"path": path_of(&path), "importer": "plugin.test.largepng"})).unwrap_err(); + assert!(e.to_string().contains("larger than"), "{e}"); + assert!(s.documents().is_empty()); + let r = s.execute("document.open", &json!({"path": path_of(&path)})).unwrap(); + assert_eq!(r["format"], "png"); + let _ = std::fs::remove_dir_all(&dir); +}