diff --git a/src/build/binary.rs b/src/build/binary.rs
index 72828a9..c490d8b 100644
--- a/src/build/binary.rs
+++ b/src/build/binary.rs
@@ -75,9 +75,17 @@ pub fn generate_binary_metadata(
let control_content = ctx.read_file(&package_dir.join("debian/control"))?;
let control = ControlInfo::parse_content(&control_content)?;
- let files_content = ctx
- .read_file(&package_dir.join("debian/files"))
- .unwrap_or_default();
+ // A missing `debian/files` is tolerated (first binary build in a fresh
+ // tree has nothing registered yet; that surfaces below as the "no binary
+ // artifacts" error), like `FilesList::load`. Any other read failure must
+ // not be silently mistaken for an empty registry.
+ let files_path = package_dir.join("debian/files");
+ let files_content = if ctx.exists(&files_path)? {
+ ctx.read_file(&files_path)
+ .map_err(|e| format!("cannot read '{}': {}", files_path.display(), e))?
+ } else {
+ String::new()
+ };
let mut files_list = FilesList::parse(&files_content)?;
// ------------------------------------------------------------------
@@ -208,9 +216,13 @@ pub fn generate_binary_metadata(
// ------------------------------------------------------------------
// Installed-Build-Depends closure over the context status database
// ------------------------------------------------------------------
+ // Like the source-build path, a status database that cannot be read is
+ // a hard error: silently treating it as empty would drop (or gut) the
+ // `Installed-Build-Depends` field of the produced metadata.
+ let status_path = Path::new("/var/lib/dpkg/status");
let status_content = ctx
- .read_file(Path::new("/var/lib/dpkg/status"))
- .unwrap_or_default();
+ .read_file(status_path)
+ .map_err(|e| format!("cannot read status file '{}': {}", status_path.display(), e))?;
let bd_fields = [
control.source.get("Build-Depends").unwrap_or(""),
control.source.get("Build-Depends-Arch").unwrap_or(""),
@@ -650,4 +662,65 @@ Description: test package
assert!(err.contains("previous version"), "{err}");
assert!(err.contains("unbalanced parenthesis"), "{err}");
}
+
+ /// An unreadable `debian/files` (e.g. permissions) must fail the
+ /// metadata generation with an error naming the read failure, instead of
+ /// being silently treated as an empty registry and reported as "no
+ /// binary artifacts found". A *missing* file stays tolerated (first
+ /// build in a fresh tree); the distinction matters.
+ #[test]
+ fn unreadable_debian_files_errors_instead_of_empty_registry() {
+ if crate::utils::root::is_root().unwrap_or(false) {
+ // Root can read files regardless of permissions.
+ return;
+ }
+ let changelog = "\
+hello (1.0-1) unstable; urgency=medium
+
+ * Regular build.
+
+ -- A B Mon, 01 Jan 2024 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");
+ let files_path = tree.join("debian/files");
+ std::fs::write(&files_path, "hello_1.0-1_all.deb devel optional\n").expect("write files");
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ std::fs::set_permissions(&files_path, std::fs::Permissions::from_mode(0o000))
+ .expect("chmod files");
+ }
+
+ 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("unreadable debian/files must fail with a read error");
+ let err = err.to_string();
+ assert!(err.contains("cannot read"), "{err}");
+ assert!(err.contains("debian/files"), "{err}");
+ #[cfg(unix)]
+ assert!(err.contains("Permission denied"), "{err}");
+ }
}
diff --git a/src/build/buildinfo.rs b/src/build/buildinfo.rs
index a53e8b4..52aca96 100644
--- a/src/build/buildinfo.rs
+++ b/src/build/buildinfo.rs
@@ -207,6 +207,13 @@ pub fn installed_build_depends_from_content(
entries.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
entries.dedup_by(|a, b| a.0 == b.0);
+ // With no reachable entries, return an empty value so `render_buildinfo`
+ // omits the field entirely; a leading `\n` alone would render a
+ // malformed `Installed-Build-Depends:` with only a blank continuation.
+ if entries.is_empty() {
+ return Ok(String::new());
+ }
+
// Leading `\n`: pre-wrapped multiline field, dpkg-style.
let mut out = String::from("\n");
out.push_str(
@@ -378,6 +385,68 @@ Architecture: amd64
assert!(!ibd.contains("not-installed"));
}
+ /// With no installed entries reachable (empty status database), the
+ /// computed value must be EMPTY so `render_buildinfo` omits the
+ /// `Installed-Build-Depends` field entirely, instead of emitting a
+ /// malformed field with only a blank continuation line.
+ #[test]
+ fn installed_build_depends_without_entries_is_empty_and_omitted() {
+ let ibd = installed_build_depends_from_content("", &["libc6"]).unwrap();
+ assert_eq!(
+ ibd, "",
+ "zero entries must yield an empty value, not \"\\n\""
+ );
+
+ let input = BuildInfoInput {
+ source: "hello".to_string(),
+ binaries: vec!["hello".to_string()],
+ architecture: "amd64".to_string(),
+ version: "1.0".to_string(),
+ binary_only_changes: None,
+ build_origin: "debian".to_string(),
+ build_architecture: "amd64".to_string(),
+ build_date: "Sat, 22 Aug 2026 23:08:42 +0200".to_string(),
+ checksums: FileChecksums::new(),
+ installed_build_depends: ibd,
+ environment: String::new(),
+ };
+ let p = render_buildinfo(&input);
+ assert!(
+ p.get("Installed-Build-Depends").is_none(),
+ "empty value must omit the field entirely"
+ );
+ }
+
+ /// With installed entries, the value keeps the dpkg-style leading `\n`
+ /// (pre-wrapped multiline field) and the field is rendered.
+ #[test]
+ fn installed_build_depends_with_entries_renders_field() {
+ let status = "\
+Package: gcc
+Status: install ok installed
+Version: 13.2
+Architecture: amd64
+";
+ let ibd = installed_build_depends_from_content(status, &["gcc"]).unwrap();
+ assert_eq!(ibd, "\ngcc (= 13.2)");
+
+ let input = BuildInfoInput {
+ source: "hello".to_string(),
+ binaries: vec!["hello".to_string()],
+ architecture: "amd64".to_string(),
+ version: "1.0".to_string(),
+ binary_only_changes: None,
+ build_origin: "debian".to_string(),
+ build_architecture: "amd64".to_string(),
+ build_date: "Sat, 22 Aug 2026 23:08:42 +0200".to_string(),
+ checksums: FileChecksums::new(),
+ installed_build_depends: ibd,
+ environment: String::new(),
+ };
+ let p = render_buildinfo(&input);
+ assert_eq!(p.get("Installed-Build-Depends"), Some("\ngcc (= 13.2)"));
+ }
+
#[test]
fn wrap_binary_field() {
assert_eq!(wrap_long("abc"), "abc");