From fa3985178354a67255d86dbab8d61ddfb8adbef0 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Thu, 17 Sep 2026 18:56:32 +0200 Subject: [PATCH] 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. --- src/build/binary.rs | 19 ++++++++++++++++--- src/build/env.rs | 44 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/build/binary.rs b/src/build/binary.rs index c490d8b..52e5edb 100644 --- a/src/build/binary.rs +++ b/src/build/binary.rs @@ -332,21 +332,34 @@ fn hashes_in_context( .map(|n| (n.clone(), ArtifactHashes::default())) .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 .command("stat") .current_dir(dir) .arg("-c") .arg("%s %n") .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); for line in stdout.lines() { let Some((size, name)) = line.trim().split_once(' ') else { continue; }; + let size = size + .parse::() + .map_err(|_| format!("'stat' reported an invalid size '{size}' for '{name}'"))?; if let Some(slot) = out.get_mut(name) { - slot.size = size.parse().unwrap_or(0); + slot.size = size; } } diff --git a/src/build/env.rs b/src/build/env.rs index faa11c1..3abb9ec 100644 --- a/src/build/env.rs +++ b/src/build/env.rs @@ -3,7 +3,7 @@ //! sanitized environment recorded in `.buildinfo` files. use std::collections::BTreeMap; -use std::path::Path; +use std::path::{Path, PathBuf}; /// Number of parallel jobs to advertise in `DEB_BUILD_OPTIONS`. pub fn num_parallel() -> usize { @@ -57,15 +57,31 @@ pub fn arch_env(host_arch: Option<&str>) -> Result, Str crate::debian::arch::arch_env(host_arch) } -/// Read the current vendor name from `/etc/dpkg/origins/default` -/// (its `Vendor:` or `Origin:` field), defaulting to `"debian"`. +/// Read the current vendor name from the active dpkg origins `default` file +/// (`$DPKG_ORIGINS_DIR/default`, falling back to `/etc/dpkg/origins/default`; +/// its `Vendor:` or `Origin:` field), defaulting to `"debian"`. 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() .and_then(|content| vendor_from_origins_content(&content)) .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 /// `Vendor:` field, falling back to `Origin:` when absent. `None` when /// neither field carries a non-empty value. @@ -306,6 +322,26 @@ mod tests { 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] fn environment_escaping() { // The function reads the process env; just verify formatting helpers