diff --git a/Cargo.toml b/Cargo.toml index 05e9da9f..cfd9c0f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,11 @@ flate2 = "1" # Base64 encoding base64 = "0.22" +# Checksums (md5sum, sha256sum, etc.) +md-5 = "0.10" +sha1 = "0.10" +sha2 = "0.10" + # CLI clap = { version = "4", features = ["derive"] } diff --git a/crates/bashkit/Cargo.toml b/crates/bashkit/Cargo.toml index a3d75f02..88ce5e97 100644 --- a/crates/bashkit/Cargo.toml +++ b/crates/bashkit/Cargo.toml @@ -57,6 +57,11 @@ flate2 = { workspace = true } # Base64 encoding (for base64 builtin and HTTP basic auth) base64 = { workspace = true } +# Checksums (for md5sum, sha1sum, sha256sum builtins) +md-5 = { workspace = true } +sha1 = { workspace = true } +sha2 = { workspace = true } + # Logging/tracing (optional) tracing = { workspace = true, optional = true } diff --git a/crates/bashkit/src/builtins/checksum.rs b/crates/bashkit/src/builtins/checksum.rs new file mode 100644 index 00000000..ccb6afd8 --- /dev/null +++ b/crates/bashkit/src/builtins/checksum.rs @@ -0,0 +1,161 @@ +//! Checksum builtins - md5sum, sha1sum, sha256sum + +use async_trait::async_trait; +use md5::Md5; +use sha1::Sha1; +use sha2::{Digest, Sha256}; + +use super::{Builtin, Context}; +use crate::error::Result; +use crate::interpreter::ExecResult; + +/// md5sum builtin - compute MD5 message digest +pub struct Md5sum; + +/// sha1sum builtin - compute SHA-1 message digest +pub struct Sha1sum; + +/// sha256sum builtin - compute SHA-256 message digest +pub struct Sha256sum; + +#[async_trait] +impl Builtin for Md5sum { + async fn execute(&self, ctx: Context<'_>) -> Result { + checksum_execute::(&ctx, "md5sum").await + } +} + +#[async_trait] +impl Builtin for Sha1sum { + async fn execute(&self, ctx: Context<'_>) -> Result { + checksum_execute::(&ctx, "sha1sum").await + } +} + +#[async_trait] +impl Builtin for Sha256sum { + async fn execute(&self, ctx: Context<'_>) -> Result { + checksum_execute::(&ctx, "sha256sum").await + } +} + +async fn checksum_execute(ctx: &Context<'_>, cmd: &str) -> Result { + let files: Vec<&String> = ctx.args.iter().filter(|a| !a.starts_with('-')).collect(); + + let mut output = String::new(); + + if files.is_empty() { + // Read from stdin + let input = ctx.stdin.unwrap_or(""); + let hash = hex_digest::(input.as_bytes()); + output.push_str(&hash); + output.push_str(" -\n"); + } else { + for file in &files { + let path = if file.starts_with('/') { + std::path::PathBuf::from(file) + } else { + ctx.cwd.join(file) + }; + + match ctx.fs.read_file(&path).await { + Ok(content) => { + let hash = hex_digest::(&content); + output.push_str(&hash); + output.push_str(" "); + output.push_str(file); + output.push('\n'); + } + Err(e) => { + return Ok(ExecResult::err(format!("{}: {}: {}\n", cmd, file, e), 1)); + } + } + } + } + + Ok(ExecResult::ok(output)) +} + +fn hex_digest(data: &[u8]) -> String { + let result = D::digest(data); + result.iter().map(|b| format!("{:02x}", b)).collect() +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::path::PathBuf; + use std::sync::Arc; + + use crate::fs::InMemoryFs; + + async fn run_checksum( + builtin: &B, + args: &[&str], + stdin: Option<&str>, + ) -> ExecResult { + let fs = Arc::new(InMemoryFs::new()); + let mut variables = HashMap::new(); + let env = HashMap::new(); + let mut cwd = PathBuf::from("/"); + + let args: Vec = args.iter().map(|s| s.to_string()).collect(); + let ctx = Context { + args: &args, + env: &env, + variables: &mut variables, + cwd: &mut cwd, + fs, + stdin, + #[cfg(feature = "http_client")] + http_client: None, + #[cfg(feature = "git")] + git_client: None, + }; + + builtin.execute(ctx).await.unwrap() + } + + #[tokio::test] + async fn test_md5sum_stdin() { + let result = run_checksum(&Md5sum, &[], Some("hello\n")).await; + assert_eq!(result.exit_code, 0); + // md5("hello\n") = b1946ac92492d2347c6235b4d2611184 + assert!(result + .stdout + .starts_with("b1946ac92492d2347c6235b4d2611184")); + assert!(result.stdout.contains(" -")); + } + + #[tokio::test] + async fn test_sha256sum_stdin() { + let result = run_checksum(&Sha256sum, &[], Some("hello\n")).await; + assert_eq!(result.exit_code, 0); + // sha256("hello\n") = 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 + assert!(result + .stdout + .starts_with("5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03")); + } + + #[tokio::test] + async fn test_sha1sum_stdin() { + let result = run_checksum(&Sha1sum, &[], Some("hello\n")).await; + assert_eq!(result.exit_code, 0); + // sha1("hello\n") = f572d396fae9206628714fb2ce00f72e94f2258f + assert!(result + .stdout + .starts_with("f572d396fae9206628714fb2ce00f72e94f2258f")); + } + + #[tokio::test] + async fn test_md5sum_empty() { + let result = run_checksum(&Md5sum, &[], Some("")).await; + assert_eq!(result.exit_code, 0); + // md5("") = d41d8cd98f00b204e9800998ecf8427e + assert!(result + .stdout + .starts_with("d41d8cd98f00b204e9800998ecf8427e")); + } +} diff --git a/crates/bashkit/src/builtins/mod.rs b/crates/bashkit/src/builtins/mod.rs index 85d18bb4..c7c901e1 100644 --- a/crates/bashkit/src/builtins/mod.rs +++ b/crates/bashkit/src/builtins/mod.rs @@ -26,6 +26,7 @@ mod archive; mod awk; mod base64; mod cat; +mod checksum; mod column; mod comm; mod curl; @@ -78,6 +79,7 @@ pub use archive::{Gunzip, Gzip, Tar}; pub use awk::Awk; pub use base64::Base64; pub use cat::Cat; +pub use checksum::{Md5sum, Sha1sum, Sha256sum}; pub use column::Column; pub use comm::Comm; pub use curl::{Curl, Wget}; diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 1a021404..fa60551a 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -259,6 +259,9 @@ impl Interpreter { builtins.insert("xxd".to_string(), Box::new(builtins::Xxd)); builtins.insert("hexdump".to_string(), Box::new(builtins::Hexdump)); builtins.insert("base64".to_string(), Box::new(builtins::Base64)); + builtins.insert("md5sum".to_string(), Box::new(builtins::Md5sum)); + builtins.insert("sha1sum".to_string(), Box::new(builtins::Sha1sum)); + builtins.insert("sha256sum".to_string(), Box::new(builtins::Sha256sum)); builtins.insert("seq".to_string(), Box::new(builtins::Seq)); builtins.insert("tac".to_string(), Box::new(builtins::Tac)); builtins.insert("rev".to_string(), Box::new(builtins::Rev)); diff --git a/crates/bashkit/tests/spec_cases/bash/checksum.test.sh b/crates/bashkit/tests/spec_cases/bash/checksum.test.sh new file mode 100644 index 00000000..49935895 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/checksum.test.sh @@ -0,0 +1,75 @@ +### md5sum_stdin +# md5sum from stdin +echo -n "hello" | md5sum +### expect +5d41402abc4b2a76b9719d911017c592 - +### end + +### sha1sum_stdin +# sha1sum from stdin +echo -n "hello" | sha1sum +### expect +aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d - +### end + +### sha256sum_stdin +# sha256sum from stdin +echo -n "hello" | sha256sum +### expect +2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 - +### end + +### md5sum_empty +# md5sum of empty string +echo -n "" | md5sum +### expect +d41d8cd98f00b204e9800998ecf8427e - +### end + +### sha256sum_newline +# sha256sum with trailing newline +echo "hello" | sha256sum +### expect +5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 - +### end + +### md5sum_file +# md5sum of a file +echo -n "test" > /tmp/checkfile.txt +md5sum /tmp/checkfile.txt +### expect +098f6bcd4621d373cade4e832627b4f6 /tmp/checkfile.txt +### end + +### sha1sum_file +# sha1sum of a file +echo -n "test" > /tmp/checkfile.txt +sha1sum /tmp/checkfile.txt +### expect +a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 /tmp/checkfile.txt +### end + +### sha256sum_file +# sha256sum of a file +echo -n "test" > /tmp/checkfile.txt +sha256sum /tmp/checkfile.txt +### expect +9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 /tmp/checkfile.txt +### end + +### md5sum_multiple_files +# md5sum of multiple files +echo -n "aaa" > /tmp/a.txt +echo -n "bbb" > /tmp/b.txt +md5sum /tmp/a.txt /tmp/b.txt +### expect +47bce5c74f589f4867dbd57e9ca9f808 /tmp/a.txt +08f8e0260c64418510cefb2b06eee5cd /tmp/b.txt +### end + +### checksum_missing_file +# checksum of non-existent file +md5sum /tmp/nonexistent.txt +### expect_exit_code +1 +### end diff --git a/supply-chain/config.toml b/supply-chain/config.toml index a380a56b..774dc2bb 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -118,6 +118,10 @@ criteria = "safe-to-run" version = "2.11.0" criteria = "safe-to-deploy" +[[exemptions.block-buffer]] +version = "0.10.4" +criteria = "safe-to-deploy" + [[exemptions.bstr]] version = "1.12.1" criteria = "safe-to-deploy" @@ -246,6 +250,10 @@ criteria = "safe-to-deploy" version = "0.8.7" criteria = "safe-to-deploy" +[[exemptions.cpufeatures]] +version = "0.2.17" +criteria = "safe-to-deploy" + [[exemptions.crc32fast]] version = "1.5.0" criteria = "safe-to-deploy" @@ -278,6 +286,10 @@ criteria = "safe-to-run" version = "0.2.4" criteria = "safe-to-run" +[[exemptions.crypto-common]] +version = "0.1.7" +criteria = "safe-to-deploy" + [[exemptions.derive-where]] version = "1.6.0" criteria = "safe-to-deploy" @@ -286,6 +298,10 @@ criteria = "safe-to-deploy" version = "0.1.13" criteria = "safe-to-run" +[[exemptions.digest]] +version = "0.10.7" +criteria = "safe-to-deploy" + [[exemptions.displaydoc]] version = "0.2.5" criteria = "safe-to-deploy" @@ -398,6 +414,10 @@ criteria = "safe-to-deploy" version = "0.3.32" criteria = "safe-to-deploy" +[[exemptions.generic-array]] +version = "0.14.7" +criteria = "safe-to-deploy" + [[exemptions.get-size-derive2]] version = "0.7.4" criteria = "safe-to-deploy" @@ -658,6 +678,10 @@ criteria = "safe-to-deploy" version = "0.3.10" criteria = "safe-to-deploy" +[[exemptions.md-5]] +version = "0.10.6" +criteria = "safe-to-deploy" + [[exemptions.memchr]] version = "2.8.0" criteria = "safe-to-deploy" @@ -1090,6 +1114,14 @@ criteria = "safe-to-run" version = "3.4.0" criteria = "safe-to-run" +[[exemptions.sha1]] +version = "0.10.6" +criteria = "safe-to-deploy" + +[[exemptions.sha2]] +version = "0.10.9" +criteria = "safe-to-deploy" + [[exemptions.shlex]] version = "1.3.0" criteria = "safe-to-deploy"