From 06e591c66535a3ea904cf36e383d83a8db7d72c4 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Thu, 17 Sep 2026 18:31:31 +0200 Subject: [PATCH] build: fail binary-only metadata when the previous version is unparseable The binNMU path swallowed parse errors with a let-chain: a changelog that could not yield the previous version silently produced a .changes with plain 'Source: pkg', no Binary-Only-Changes and no redistributed previous .dsc. Propagate the parse error like the source-build path does (a single-entry changelog stays tolerated), and reuse the changelog already read instead of reading the file a second time. --- src/build/binary.rs | 102 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 16 deletions(-) diff --git a/src/build/binary.rs b/src/build/binary.rs index 1867d82..72828a9 100644 --- a/src/build/binary.rs +++ b/src/build/binary.rs @@ -130,22 +130,31 @@ pub fn generate_binary_metadata( let mut source_display = entry.source.clone(); let mut binary_only_changes = None; - if entry.binary_only - && let Ok(prev_entry) = crate::debian::changelog::parse_previous_version_from_str( - &ctx.read_file(&package_dir.join("debian/changelog"))?, - ) - && let Some(prev) = prev_entry - { - source_display = format!("{} ({})", entry.source, prev); - binary_only_changes = Some(format!( - "{}\n\n -- {} <{}> {}", - entry.changes_field, entry.maintainer_name, entry.maintainer_email, entry.date_raw - )); - let prev_version = crate::debian::DebianVersion::parse(&prev)?; - let dsc_name = format!("{}_{}.dsc", entry.source, prev_version.no_epoch()); - let dsc_path = upload_dir.join(&dsc_name); - if ctx.exists(&dsc_path)? { - include_dsc_artifacts(ctx, upload_dir, &dsc_name, &mut checksums)?; + if entry.binary_only { + // A binary-only upload must reference the previous source version; + // a changelog that cannot yield it is a hard error, like in the + // source-build path. Reuse the changelog read above instead of + // reading the file a second time. + let changelog_path = package_dir.join("debian/changelog"); + let prev = crate::debian::changelog::parse_previous_version_from_str(&changelog_content) + .map_err(|e| { + format!( + "cannot parse the previous version from '{}': {e}", + changelog_path.display() + ) + })?; + if let Some(prev) = prev { + source_display = format!("{} ({})", entry.source, prev); + binary_only_changes = Some(format!( + "{}\n\n -- {} <{}> {}", + entry.changes_field, entry.maintainer_name, entry.maintainer_email, entry.date_raw + )); + let prev_version = crate::debian::DebianVersion::parse(&prev)?; + let dsc_name = format!("{}_{}.dsc", entry.source, prev_version.no_epoch()); + let dsc_path = upload_dir.join(&dsc_name); + if ctx.exists(&dsc_path)? { + include_dsc_artifacts(ctx, upload_dir, &dsc_name, &mut checksums)?; + } } } @@ -580,4 +589,65 @@ Files: assert_eq!(tar_entry.sha1, "aaa111"); assert_eq!(tar_entry.sha256, "bbb222"); } + + /// A binary-only (binNMU) build whose changelog cannot yield the + /// previous version (malformed second header, unbalanced parenthesis) + /// must fail the metadata generation with a diagnostic naming the + /// changelog, instead of silently emitting a plain `Source:` `.changes` + /// with no `Binary-Only-Changes` and no redistributed previous `.dsc`. + #[test] + fn binary_only_prev_version_parse_failure_errors_instead_of_wrong_metadata() { + let changelog = "\ +hello (1.0-1+b1) unstable; urgency=medium, binary-only=yes + + * Binary-only rebuild. + + -- A B Mon, 01 Jan 2024 00:00:00 +0000 + +hello (1.0-1 unstable; urgency=medium + + * Previous entry with an unbalanced parenthesis. + + -- A B Sun, 31 Dec 2023 00:00:00 +0000 +"; + let control = "\ +Source: hello +Section: devel +Priority: optional +Maintainer: A B + +Package: hello +Architecture: all +Description: test package +"; + let base = tempfile::tempdir().expect("tempdir"); + let tree = base.path().join("hello-1.0"); + std::fs::create_dir_all(tree.join("debian")).expect("mkdir tree"); + std::fs::write(tree.join("debian/changelog"), changelog).expect("write changelog"); + std::fs::write(tree.join("debian/control"), control).expect("write control"); + std::fs::write( + tree.join("debian/files"), + "hello_1.0-1+b1_all.deb devel optional\n", + ) + .expect("write files"); + std::fs::write(base.path().join("hello_1.0-1+b1_all.deb"), "deb payload") + .expect("write deb"); + + let ctx = Arc::new( + crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"), + ); + let opts = BinaryMetadataOptions { + profiles: Vec::new(), + vendor: "debian".to_string(), + exported_env: BTreeMap::new(), + build_arch: "amd64".to_string(), + host_arch: "amd64".to_string(), + }; + let err = generate_binary_metadata(&ctx, &tree, base.path(), &opts) + .expect_err("binary-only build with an unparseable changelog must fail"); + let err = err.to_string(); + assert!(err.contains("debian/changelog"), "{err}"); + assert!(err.contains("previous version"), "{err}"); + assert!(err.contains("unbalanced parenthesis"), "{err}"); + } }