build: stop turning read failures into silently wrong metadata
Two read_file(...).unwrap_or_default() calls masqueraded IO errors as empty data: an unreadable debian/files became 'binary build with no binary artifacts found; cannot distribute', and an unreadable dpkg status file produced an empty Installed-Build-Depends. Tolerate a missing debian/files (first build in a fresh tree) but propagate real read errors, and hard-error on an unreadable status file like the source-build path does. installed_build_depends_from_content also returned a bare newline for zero entries, defeating render_buildinfo's empty-guard and emitting a malformed 'Installed-Build-Depends:' field with a blank continuation; it now returns an empty string so the field is omitted.
This commit is contained in:
+78
-5
@@ -75,9 +75,17 @@ pub fn generate_binary_metadata(
|
|||||||
let control_content = ctx.read_file(&package_dir.join("debian/control"))?;
|
let control_content = ctx.read_file(&package_dir.join("debian/control"))?;
|
||||||
let control = ControlInfo::parse_content(&control_content)?;
|
let control = ControlInfo::parse_content(&control_content)?;
|
||||||
|
|
||||||
let files_content = ctx
|
// A missing `debian/files` is tolerated (first binary build in a fresh
|
||||||
.read_file(&package_dir.join("debian/files"))
|
// tree has nothing registered yet; that surfaces below as the "no binary
|
||||||
.unwrap_or_default();
|
// 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)?;
|
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
|
// 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
|
let status_content = ctx
|
||||||
.read_file(Path::new("/var/lib/dpkg/status"))
|
.read_file(status_path)
|
||||||
.unwrap_or_default();
|
.map_err(|e| format!("cannot read status file '{}': {}", status_path.display(), e))?;
|
||||||
let bd_fields = [
|
let bd_fields = [
|
||||||
control.source.get("Build-Depends").unwrap_or(""),
|
control.source.get("Build-Depends").unwrap_or(""),
|
||||||
control.source.get("Build-Depends-Arch").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("previous version"), "{err}");
|
||||||
assert!(err.contains("unbalanced parenthesis"), "{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 <a@b.c> Mon, 01 Jan 2024 00:00:00 +0000
|
||||||
|
";
|
||||||
|
let control = "\
|
||||||
|
Source: hello
|
||||||
|
Section: devel
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: A B <a@b.c>
|
||||||
|
|
||||||
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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);
|
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.
|
// Leading `\n`: pre-wrapped multiline field, dpkg-style.
|
||||||
let mut out = String::from("\n");
|
let mut out = String::from("\n");
|
||||||
out.push_str(
|
out.push_str(
|
||||||
@@ -378,6 +385,68 @@ Architecture: amd64
|
|||||||
assert!(!ibd.contains("not-installed"));
|
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]
|
#[test]
|
||||||
fn wrap_binary_field() {
|
fn wrap_binary_field() {
|
||||||
assert_eq!(wrap_long("abc"), "abc");
|
assert_eq!(wrap_long("abc"), "abc");
|
||||||
|
|||||||
Reference in New Issue
Block a user