build: check stat's exit status and honor DPKG_ORIGINS_DIR

hashes_in_context never checked stat's exit status and parsed its size
with unwrap_or(0), silently recording zero-size artifacts in the
generated .changes/.buildinfo; stat failures and unparsable sizes are
now errors naming the file. current_vendor hardcoded
/etc/dpkg/origins/default while dpkg honors DPKG_ORIGINS_DIR (already
in the file's own ENV_ALLOWED list); the origins default is now
resolved against it with the usual fallback.
This commit is contained in:
2026-09-17 18:56:32 +02:00
parent 22e43741f3
commit fa39851783
2 changed files with 56 additions and 7 deletions
+16 -3
View File
@@ -332,21 +332,34 @@ fn hashes_in_context(
.map(|n| (n.clone(), ArtifactHashes::default())) .map(|n| (n.clone(), ArtifactHashes::default()))
.collect(); .collect();
// Sizes. // Sizes. A failed `stat` must fail the metadata generation: an unchecked
// exit status would leave the default size 0 in the produced
// `.changes`/`.buildinfo` checksum entries.
let output = ctx let output = ctx
.command("stat") .command("stat")
.current_dir(dir) .current_dir(dir)
.arg("-c") .arg("-c")
.arg("%s %n") .arg("%s %n")
.args(names) .args(names)
.output()?; .output()
.map_err(|e| format!("failed to run 'stat' inside the build context: {e}"))?;
if !output.status.success() {
return Err(format!(
"'stat' failed inside the build context: {}",
String::from_utf8_lossy(&output.stderr).trim()
)
.into());
}
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() { for line in stdout.lines() {
let Some((size, name)) = line.trim().split_once(' ') else { let Some((size, name)) = line.trim().split_once(' ') else {
continue; continue;
}; };
let size = size
.parse::<u64>()
.map_err(|_| format!("'stat' reported an invalid size '{size}' for '{name}'"))?;
if let Some(slot) = out.get_mut(name) { if let Some(slot) = out.get_mut(name) {
slot.size = size.parse().unwrap_or(0); slot.size = size;
} }
} }
+40 -4
View File
@@ -3,7 +3,7 @@
//! sanitized environment recorded in `.buildinfo` files. //! sanitized environment recorded in `.buildinfo` files.
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path; use std::path::{Path, PathBuf};
/// Number of parallel jobs to advertise in `DEB_BUILD_OPTIONS`. /// Number of parallel jobs to advertise in `DEB_BUILD_OPTIONS`.
pub fn num_parallel() -> usize { pub fn num_parallel() -> usize {
@@ -57,15 +57,31 @@ pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, Str
crate::debian::arch::arch_env(host_arch) crate::debian::arch::arch_env(host_arch)
} }
/// Read the current vendor name from `/etc/dpkg/origins/default` /// Read the current vendor name from the active dpkg origins `default` file
/// (its `Vendor:` or `Origin:` field), defaulting to `"debian"`. /// (`$DPKG_ORIGINS_DIR/default`, falling back to `/etc/dpkg/origins/default`;
/// its `Vendor:` or `Origin:` field), defaulting to `"debian"`.
pub fn current_vendor() -> String { pub fn current_vendor() -> String {
std::fs::read_to_string(Path::new("/etc/dpkg/origins/default")) let path = resolve_origins_default(
std::env::var("DPKG_ORIGINS_DIR").ok().as_deref(),
"/etc/dpkg/origins",
);
std::fs::read_to_string(path)
.ok() .ok()
.and_then(|content| vendor_from_origins_content(&content)) .and_then(|content| vendor_from_origins_content(&content))
.unwrap_or_else(|| "debian".to_string()) .unwrap_or_else(|| "debian".to_string())
} }
/// Resolve the path of the active dpkg origins file from the
/// `DPKG_ORIGINS_DIR` value (the directory holding the origin files, where
/// `default` selects the active one) and the fallback directory
/// (`/etc/dpkg/origins`). An unset or empty directory value falls back.
fn resolve_origins_default(origins_dir: Option<&str>, fallback_dir: &str) -> PathBuf {
let dir = origins_dir
.filter(|d| !d.is_empty())
.unwrap_or(fallback_dir);
Path::new(dir).join("default")
}
/// Extract the vendor name from the content of a dpkg origins file: its /// Extract the vendor name from the content of a dpkg origins file: its
/// `Vendor:` field, falling back to `Origin:` when absent. `None` when /// `Vendor:` field, falling back to `Origin:` when absent. `None` when
/// neither field carries a non-empty value. /// neither field carries a non-empty value.
@@ -306,6 +322,26 @@ mod tests {
assert_eq!(vendor_from_origins_content("Suite: stable\n"), None); assert_eq!(vendor_from_origins_content("Suite: stable\n"), None);
} }
/// `current_vendor` must honor `DPKG_ORIGINS_DIR` (already on the
/// `.buildinfo` allow-list) when locating the `default` origins file,
/// falling back to `/etc/dpkg/origins/default` when unset or empty.
#[test]
fn origins_default_path_honors_dpkg_origins_dir() {
assert_eq!(
resolve_origins_default(Some("/custom/origins"), "/etc/dpkg/origins"),
PathBuf::from("/custom/origins/default")
);
assert_eq!(
resolve_origins_default(None, "/etc/dpkg/origins"),
PathBuf::from("/etc/dpkg/origins/default")
);
// An empty value behaves as unset, like dpkg's `$dir || $default`.
assert_eq!(
resolve_origins_default(Some(""), "/etc/dpkg/origins"),
PathBuf::from("/etc/dpkg/origins/default")
);
}
#[test] #[test]
fn environment_escaping() { fn environment_escaping() {
// The function reads the process env; just verify formatting helpers // The function reads the process env; just verify formatting helpers