diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index 60d9a323..ba456f6c 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -734,6 +734,12 @@ fn read_bundle(bytes: &[u8], limits: BundleLimits) -> Result<(String, std::colle if z.len() > limits.entries { return Err(EngineError::Io("bundle: too many files".into())); } + // Entries are stored, so the files we keep can only outgrow the bundle when several entries + // point at the same bytes: a small bundle naming one payload thousands of times would be + // copied out once per name. + if z.has_overlapping_files().map_err(zerr)? { + return Err(EngineError::Io("bundle: files share data".into())); + } let (mut text, mut files, mut total) = (None, std::collections::HashMap::new(), 0u64); for i in 0..z.len() { let mut f = z.by_index(i).map_err(zerr)?; diff --git a/crates/engine/src/io/bundle.rs b/crates/engine/src/io/bundle.rs index cfe6e0af..b906333d 100644 --- a/crates/engine/src/io/bundle.rs +++ b/crates/engine/src/io/bundle.rs @@ -194,6 +194,23 @@ mod bundle_tests { } } + #[test] + fn entries_sharing_one_payload_are_refused() { + use std::io::Write as _; + let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); + let opts = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); + w.start_file("Song.scraft", opts).unwrap(); + w.write_all(Session::default().to_json().unwrap().as_bytes()).unwrap(); + w.start_file("Audio Files/a.wav", opts).unwrap(); + w.write_all(&[7u8; 4096]).unwrap(); + for i in 0..100 { + w.shallow_copy_file("Audio Files/a.wav", &format!("Audio Files/copy{i}.wav")).unwrap(); + } + let bundle = w.finish().unwrap().into_inner(); + let err = read_bundle(&bundle, BUNDLE_LIMITS).unwrap_err(); + assert!(err.to_string().contains("share data"), "{err}"); + } + #[test] fn a_bundle_is_saved_only_if_it_opens_again() { // #433: the writer keeps to the reader's limits.