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");