From fdbf85d4c8e75f9b157cd9ac208410bc7cfc7164 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 10 Nov 2026 22:24:39 -0800 Subject: [PATCH] Stores: saving a preset, profile or plug-in reads at most one byte past it from the file it replaces Before writing a PDF preset, ICC profile, plug-in, Color Settings, disabled list or grants file, the store compared the new bytes with the file already there by reading the whole file into memory, so a large file left in the folder was read in full. The comparison now reads at most one byte more than the new content. --- crates/engine/src/store.rs | 9 ++++++- crates/engine/src/tests/store.rs | 43 +++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/store.rs b/crates/engine/src/store.rs index f6fe8ff5b..01ebc4652 100644 --- a/crates/engine/src/store.rs +++ b/crates/engine/src/store.rs @@ -135,11 +135,18 @@ fn too_large(max: u64) -> String { format!("larger than {max} bytes") } +/// Whether `old` holds exactly `bytes`. It is read no further than one byte past their length, so a +/// large file is not read into memory. +pub(crate) fn holds(old: impl Read, bytes: &[u8]) -> bool { + let mut read = vec![]; + old.take(bytes.len() as u64 + 1).read_to_end(&mut read).is_ok() && read == bytes +} + /// Write `bytes` to `dir/file`, making the folder; nothing is written when the file has them /// already. A failure is logged: remembering is best effort and never fails the command. fn put(dir: &Path, file: &str, bytes: &[u8]) -> bool { let path = dir.join(file); - if std::fs::read(&path).is_ok_and(|old| old == bytes) { + if std::fs::File::open(&path).is_ok_and(|old| holds(old, bytes)) { return true; } let written = std::fs::create_dir_all(dir).and_then(|()| write_atomic(&path, bytes)); diff --git a/crates/engine/src/tests/store.rs b/crates/engine/src/tests/store.rs index 37eafe6e4..b6ac7b020 100644 --- a/crates/engine/src/tests/store.rs +++ b/crates/engine/src/tests/store.rs @@ -5,7 +5,7 @@ use serde_json::{Value, json}; use vectorcraft_color::cms; use crate::Session; -use crate::store::{MAX_PRESET_FILE, file_stem}; +use crate::store::{MAX_PRESET_FILE, file_stem, holds}; fn fresh_dir(tag: &str) -> std::path::PathBuf { let dir = vectorcraft_testkit::temp_dir(&format!("store-{tag}")); @@ -128,6 +128,47 @@ fn a_folder_that_does_not_exist_yet_takes_the_presets_the_preferences_held() { assert_eq!(saved_names(&mut b), ["From Preferences"]); } +/// Serves `0` bytes without end and counts how many were read. +struct Endless(u64); + +impl std::io::Read for Endless { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if self.0 > 1 << 24 { + return Err(std::io::Error::other("read too far")); + } + buf.fill(0); + self.0 += buf.len() as u64; + Ok(buf.len()) + } +} + +#[test] +fn a_write_reads_the_file_it_replaces_only_one_byte_past_the_new_length() { + let mut old = Endless(0); + assert!(!holds(&mut old, &[0; 10])); + assert_eq!(old.0, 11); + assert!(holds(&[1, 2, 3][..], &[1, 2, 3])); + assert!(!holds(&[1, 2, 3, 4][..], &[1, 2, 3])); + assert!(!holds(&[1, 2][..], &[1, 2, 3])); + assert!(holds(&[][..], &[])); +} + +#[test] +fn a_saved_preset_replaces_an_oversized_file_in_its_place() { + let dir = fresh_dir("oversized-put").join("PDF Presets"); + let mut a = Session::new(); + a.attach_pdf_preset_store(dir.clone()); + a.execute("pdf.preset.save", &json!({"name": "Web"})).unwrap(); + let file = dir.join(format!("{}.vcpdfpresets", file_stem("Web"))); + let saved = std::fs::read(&file).unwrap(); + let mut oversized = saved.clone(); + oversized.resize(usize::try_from(MAX_PRESET_FILE).unwrap() * 2, b' '); + std::fs::write(&file, &oversized).unwrap(); + + a.execute("pdf.preset.save", &json!({"name": "Web"})).unwrap(); + assert_eq!(std::fs::read(&file).unwrap(), saved); +} + #[test] fn damaged_unsafe_and_oversized_preset_files_are_skipped() { let dir = fresh_dir("damaged").join("PDF Presets");