From a5fcf4b9161bebbf5a866de6604d7da06533f8c6 Mon Sep 17 00:00:00 2001 From: SFG545 Date: Mon, 23 Mar 2026 17:14:18 -0500 Subject: [PATCH] feat: add support for SHA1 checksum verification and update related documentation --- Cargo.lock | 1 + Cargo.toml | 1 + src/install/hooks.rs | 40 ++++++++++++++++++++++++++++++++++++-- src/package/interactive.rs | 4 ++-- src/package/spec.rs | 2 +- src/source/fetcher.rs | 10 +++++++++- src/source/mod.rs | 26 +++++++++++++++++++++++-- 7 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37dcca6..c780e53 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,6 +444,7 @@ dependencies = [ "semver", "serde", "serde_ignored", + "sha1", "sha2", "signal-hook 0.4.3", "suppaftp", diff --git a/Cargo.toml b/Cargo.toml index 81bb6a6..d0b2971 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ serde_ignored = "0.1.14" lz4_flex = "0.13.0" lzma-rust2 = "0.16.2" signal-hook = "0.4.3" +sha1 = "0.10.6" [dev-dependencies] tempfile = "=3.27.0" diff --git a/src/install/hooks.rs b/src/install/hooks.rs index 32aec95..79c796c 100644 --- a/src/install/hooks.rs +++ b/src/install/hooks.rs @@ -25,7 +25,7 @@ use crate::fakeroot; use anyhow::{Context, Result, bail}; use std::fs; -use std::io::Write; +use std::io::{ErrorKind, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus, Stdio}; @@ -549,7 +549,11 @@ fn run_command_with_optional_stdin( command.stdin(Stdio::piped()); let mut child = command.spawn()?; if let Some(mut stdin) = child.stdin.take() { - stdin.write_all(payload.as_bytes())?; + match stdin.write_all(payload.as_bytes()) { + Ok(()) => {} + Err(err) if err.kind() == ErrorKind::BrokenPipe => {} + Err(err) => return Err(err.into()), + } } Ok(child.wait()?) } else { @@ -674,6 +678,38 @@ needs_paths = true assert_eq!(out, "install:pre:foo"); } + #[test] + fn run_transaction_hooks_ignores_broken_pipe_for_needs_paths_hooks() { + let tmp = tempfile::tempdir().unwrap(); + write_hook( + tmp.path(), + "noop.toml", + r#" +[hook] +name = "noop" + +[when] +phase = "pre" +operation = ["install"] +packages = ["foo"] + +[exec] +command = "true" +needs_paths = true +"#, + ); + + let affected = vec!["usr/bin/foo".to_string()]; + let ctx = HookExecutionContext { + phase: HookPhase::Pre, + operation: HookOperation::Install, + package: "foo", + affected_paths: &affected, + }; + let ran = run_transaction_hooks(tmp.path(), &ctx).unwrap(); + assert_eq!(ran, 1); + } + #[test] fn run_transaction_hooks_respects_negation_and_filters_out() { let tmp = tempfile::tempdir().unwrap(); diff --git a/src/package/interactive.rs b/src/package/interactive.rs index 515ec52..a7bff95 100644 --- a/src/package/interactive.rs +++ b/src/package/interactive.rs @@ -217,7 +217,7 @@ fn prompt_manual_sources() -> Result> { if single_entry { let checksum = prompt_optional_text( "Manual source checksum (optional):", - "sha256:/sha512:/md5:/b2:/b2sum:, raw SHA256 hex, or 'skip' (empty omits field)", + "sha256:/sha512:/sha1:/md5:/b2:/b2sum:, raw SHA256 hex, or 'skip' (empty omits field)", )?; if !checksum.is_empty() { manual.sha256 = Some(checksum); @@ -411,7 +411,7 @@ pub fn create_interactive() -> Result { let source_sha256 = Text::new("Source checksum:") .with_help_message( - "Accepts sha256:, sha512:, md5:, b2:, b2sum:, or raw SHA256 hex (use 'skip' to bypass)", + "Accepts sha256:, sha512:, sha1:, md5:, b2:, b2sum:, or raw SHA256 hex (use 'skip' to bypass)", ) .with_default(computed_sha_default.as_str()) .prompt()?; diff --git a/src/package/spec.rs b/src/package/spec.rs index 656da88..4e70e2e 100755 --- a/src/package/spec.rs +++ b/src/package/spec.rs @@ -3930,7 +3930,7 @@ impl Alternatives { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct Source { pub url: String, - /// Checksum for the source (e.g. `sha256:...`, `sha512:...`, `md5:...`, `b2:...`, `b2sum:...`, or raw SHA256 hex). + /// Checksum for the source (e.g. `sha256:...`, `sha512:...`, `sha1:...`, `md5:...`, `b2:...`, `b2sum:...`, or raw SHA256 hex). /// Defaults to `skip` when omitted. #[serde(default = "default_source_sha256")] pub sha256: String, diff --git a/src/source/fetcher.rs b/src/source/fetcher.rs index 6ff9b15..aa8ecad 100755 --- a/src/source/fetcher.rs +++ b/src/source/fetcher.rs @@ -289,6 +289,7 @@ pub fn fetch_archive(spec: &PackageSpec, source: &Source, cache_dir: &Path) -> R /// Supported formats: /// - `sha256:` (or just `` — default) /// - `sha512:` +/// - `sha1:` /// - `md5:` /// - `b2:` / `b2sum:` /// - `skip` to bypass verification @@ -732,7 +733,8 @@ mod tests { } #[test] - fn verify_checksum_accepts_md5_sha512_b2sum_and_default_sha256() { + fn verify_checksum_accepts_sha1_md5_sha512_b2sum_and_default_sha256() { + use sha1::Sha1; use sha2::Digest; use sha2::Sha256; use sha2::Sha512; @@ -752,6 +754,11 @@ mod tests { h.update(b"abc"); format!("{:x}", h.finalize()) }; + let sha1_hex = { + let mut h = Sha1::new(); + h.update(b"abc"); + format!("{:x}", h.finalize()) + }; let md5_hex = format!("{:x}", md5::compute(b"abc")); let b2_hex = b2sum_rust::Blake2bSum::new(64) @@ -763,6 +770,7 @@ mod tests { // explicit prefixes assert!(verify_checksum(tmp.path(), &format!("sha256:{}", sha256_hex)).unwrap()); assert!(verify_checksum(tmp.path(), &format!("sha512:{}", sha512_hex)).unwrap()); + assert!(verify_checksum(tmp.path(), &format!("sha1:{}", sha1_hex)).unwrap()); assert!(verify_checksum(tmp.path(), &format!("md5:{}", md5_hex)).unwrap()); assert!(verify_checksum(tmp.path(), &format!("b2:{}", b2_hex)).unwrap()); assert!(verify_checksum(tmp.path(), &format!("b2sum:{}", b2_hex)).unwrap()); diff --git a/src/source/mod.rs b/src/source/mod.rs index ebba80f..d311bcc 100755 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -279,8 +279,8 @@ fn copy_manual_source_file( /// Verify a file against an `expected` checksum string. /// -/// Formats accepted: `sha256:HEX`, `sha512:HEX`, `md5:HEX`, `b2:HEX`, -/// `b2sum:HEX`, or plain `HEX` (assumed sha256). +/// Formats accepted: `sha256:HEX`, `sha512:HEX`, `sha1:HEX`, `md5:HEX`, +/// `b2:HEX`, `b2sum:HEX`, or plain `HEX` (assumed sha256). fn verify_file_hash(path: &Path, expected: &str) -> Result { use anyhow::bail; use std::io::Read; @@ -335,6 +335,21 @@ fn verify_file_hash(path: &Path, expected: &str) -> Result { let actual = format!("{:x}", hasher.finalize()); Ok(actual == hex) } + "sha1" => { + use sha1::Sha1; + let mut f = fs::File::open(path)?; + let mut hasher = Sha1::new(); + let mut buf = [0u8; 8192]; + loop { + let n = f.read(&mut buf)?; + if n == 0 { + break; + } + hasher.update(&buf[..n]); + } + let actual = format!("{:x}", hasher.finalize()); + Ok(actual == hex) + } "md5" => { let mut ctx = md5::Context::new(); let mut f = fs::File::open(path)?; @@ -875,6 +890,7 @@ mod tests { #[test] fn verify_file_hash_accepts_multiple_algorithms() { + use sha1::Sha1; use sha2::{Digest, Sha256, Sha512}; let tmp = tempfile::NamedTempFile::new().unwrap(); @@ -890,6 +906,11 @@ mod tests { h.update(b"abc"); format!("{:x}", h.finalize()) }; + let sha1_hex = { + let mut h = Sha1::new(); + h.update(b"abc"); + format!("{:x}", h.finalize()) + }; let md5_hex = format!("{:x}", md5::compute(b"abc")); let b2_hex = b2sum_rust::Blake2bSum::new(64) .read(tmp.path()) @@ -898,6 +919,7 @@ mod tests { assert!(verify_file_hash(tmp.path(), &sha256_hex).unwrap()); assert!(verify_file_hash(tmp.path(), &format!("sha256:{}", sha256_hex)).unwrap()); assert!(verify_file_hash(tmp.path(), &format!("sha512:{}", sha512_hex)).unwrap()); + assert!(verify_file_hash(tmp.path(), &format!("sha1:{}", sha1_hex)).unwrap()); assert!(verify_file_hash(tmp.path(), &format!("md5:{}", md5_hex)).unwrap()); assert!(verify_file_hash(tmp.path(), &format!("b2:{}", b2_hex)).unwrap()); assert!(verify_file_hash(tmp.path(), &format!("b2sum:{}", b2_hex)).unwrap());