Handle malformed remote and edge-case data instead of panicking

- distro_info: malformed CSV rows are skipped with a warning, dates
  that fail to parse become None, and all plain HTTP requests go
  through a shared reqwest client with connect/total timeouts
- package_info: the Sources stanza iterator is iterative (a crafted
  index with many blank stanzas overflowed the stack), stanzas missing
  a Version are skipped, and failed series/pocket probes are summarized
  in the final 'not found' error instead of being silently dropped
- pull: no double unwrap on the remote-derived artifact filename, an
  empty series list is an error, and streaming downloads get a
  per-request timeout
- deb/cross: dpkg-architecture output parsing skips unexpected lines
  and its exit status is checked, as is dpkg --add-architecture
- changelog: version increments parse as u64 with checked arithmetic
  (1.0-20250123123456 used to panic on the u32 parse)
This commit is contained in:
2026-09-16 02:44:45 +02:00
parent 6a5c5a7106
commit f72b35acfa
5 changed files with 489 additions and 142 deletions
+110 -17
View File
@@ -1,24 +1,19 @@
use crate::context::Context;
use log::debug;
use std::collections::HashMap;
use std::error::Error;
use std::sync::Arc;
/// Set environment variables for cross-compilation
pub fn setup_environment(
env: &mut HashMap<String, String>,
arch: &str,
ctx: Arc<Context>,
) -> Result<(), Box<dyn Error>> {
let dpkg_architecture = String::from_utf8(
ctx.command("dpkg-architecture")
.arg("-a")
.arg(arch)
.output()?
.stdout,
)?;
/// Parse 'dpkg-architecture' output (KEY=value lines) into a set of
/// environment variables. Unexpected lines (e.g. warnings on stderr leaking
/// into stdout) are skipped instead of causing a failure.
fn parse_dpkg_architecture_output(output: &str, env: &mut HashMap<String, String>) {
let env_var_regex = regex::Regex::new(r"(?<key>.*)=(?<value>.*)").unwrap();
for l in dpkg_architecture.lines() {
let capture = env_var_regex.captures(l).unwrap();
for l in output.lines() {
let Some(capture) = env_var_regex.captures(l) else {
debug!("Skipping unexpected dpkg-architecture output line: '{l}'");
continue;
};
let key = capture.name("key").unwrap().as_str().to_string();
let value = capture.name("value").unwrap().as_str().to_string();
@@ -28,6 +23,45 @@ pub fn setup_environment(
env.insert("CROSS_COMPILE".to_string(), format!("{value}-"));
}
}
}
/// Set environment variables for cross-compilation
pub fn setup_environment(
env: &mut HashMap<String, String>,
arch: &str,
ctx: Arc<Context>,
) -> Result<(), Box<dyn Error>> {
let output = ctx
.command("dpkg-architecture")
.arg("-a")
.arg(arch)
.output()
.map_err(|e| {
format!(
"Failed to run 'dpkg-architecture -a {arch}': {e}. \
Is 'dpkg-dev' installed?"
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"'dpkg-architecture -a {}' failed with status: {}.{}",
arch,
output.status,
if stderr.trim().is_empty() {
String::new()
} else {
format!("\ndpkg-architecture output:\n{}", stderr.trim())
}
)
.into());
}
let dpkg_architecture = String::from_utf8(output.stdout)
.map_err(|e| format!("Invalid UTF-8 in dpkg-architecture output: {e}"))?;
parse_dpkg_architecture_output(&dpkg_architecture, env);
env.insert("DEB_BUILD_PROFILES".to_string(), "cross".to_string());
Ok(())
@@ -44,10 +78,19 @@ pub fn ensure_repositories(
let local_arch = crate::get_current_arch();
// Add target ('host') architecture
ctx.command("dpkg")
let status = ctx
.command("dpkg")
.arg("--add-architecture")
.arg(arch)
.status()?;
.status()
.map_err(|e| format!("Failed to run 'dpkg --add-architecture {arch}': {e}"))?;
if !status.success() {
return Err(format!(
"'dpkg --add-architecture {}' failed with status: {}",
arch, status
)
.into());
}
// Check if we are on Ubuntu
let os_release = String::from_utf8(ctx.command("cat").arg("/etc/os-release").output()?.stdout)?;
@@ -142,3 +185,53 @@ pub fn ensure_repositories(
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_dpkg_architecture_output() {
let output = "DEB_BUILD_ARCH=amd64\n\
DEB_HOST_ARCH=arm64\n\
DEB_HOST_GNU_TYPE=aarch64-linux-gnu\n";
let mut env = HashMap::new();
parse_dpkg_architecture_output(output, &mut env);
assert_eq!(env.get("DEB_BUILD_ARCH").map(String::as_str), Some("amd64"));
assert_eq!(env.get("DEB_HOST_ARCH").map(String::as_str), Some("arm64"));
assert_eq!(
env.get("DEB_HOST_GNU_TYPE").map(String::as_str),
Some("aarch64-linux-gnu")
);
// Derived variable for the GNU type
assert_eq!(
env.get("CROSS_COMPILE").map(String::as_str),
Some("aarch64-linux-gnu-")
);
}
#[test]
fn test_parse_dpkg_architecture_output_skips_unexpected_lines() {
// Unexpected lines (warnings on stdout, empty lines) must be skipped
// instead of panicking
let output = "dpkg-architecture: warning: something odd happened\n\
\n\
DEB_HOST_GNU_TYPE=arm-linux-gnueabihf\n\
not an environment variable assignment\n";
let mut env = HashMap::new();
parse_dpkg_architecture_output(output, &mut env);
assert_eq!(
env.get("DEB_HOST_GNU_TYPE").map(String::as_str),
Some("arm-linux-gnueabihf")
);
assert_eq!(
env.get("CROSS_COMPILE").map(String::as_str),
Some("arm-linux-gnueabihf-")
);
assert_eq!(env.len(), 2);
}
}