Skip to content

Commit 84b1ec0

Browse files
committed
feat(builtins): add md5sum, sha1sum, sha256sum checksum builtins
Implement checksum computation builtins using RustCrypto crates (md-5, sha1, sha2). Support stdin and file arguments with standard GNU coreutils output format (hash filename). Includes unit tests for all three algorithms and spec tests covering stdin, file, multi-file, and missing file error cases. Closes #330
1 parent 91ee983 commit 84b1ec0

File tree

6 files changed

+251
-0
lines changed

6 files changed

+251
-0
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ flate2 = "1"
5858
# Base64 encoding
5959
base64 = "0.22"
6060

61+
# Checksums (md5sum, sha256sum, etc.)
62+
md-5 = "0.10"
63+
sha1 = "0.10"
64+
sha2 = "0.10"
65+
6166
# CLI
6267
clap = { version = "4", features = ["derive"] }
6368

crates/bashkit/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ flate2 = { workspace = true }
5757
# Base64 encoding (for base64 builtin and HTTP basic auth)
5858
base64 = { workspace = true }
5959

60+
# Checksums (for md5sum, sha1sum, sha256sum builtins)
61+
md-5 = { workspace = true }
62+
sha1 = { workspace = true }
63+
sha2 = { workspace = true }
64+
6065
# Logging/tracing (optional)
6166
tracing = { workspace = true, optional = true }
6267

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//! Checksum builtins - md5sum, sha1sum, sha256sum
2+
3+
use async_trait::async_trait;
4+
use md5::Md5;
5+
use sha1::Sha1;
6+
use sha2::{Digest, Sha256};
7+
8+
use super::{Builtin, Context};
9+
use crate::error::Result;
10+
use crate::interpreter::ExecResult;
11+
12+
/// md5sum builtin - compute MD5 message digest
13+
pub struct Md5sum;
14+
15+
/// sha1sum builtin - compute SHA-1 message digest
16+
pub struct Sha1sum;
17+
18+
/// sha256sum builtin - compute SHA-256 message digest
19+
pub struct Sha256sum;
20+
21+
#[async_trait]
22+
impl Builtin for Md5sum {
23+
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
24+
checksum_execute::<Md5>(&ctx, "md5sum").await
25+
}
26+
}
27+
28+
#[async_trait]
29+
impl Builtin for Sha1sum {
30+
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
31+
checksum_execute::<Sha1>(&ctx, "sha1sum").await
32+
}
33+
}
34+
35+
#[async_trait]
36+
impl Builtin for Sha256sum {
37+
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
38+
checksum_execute::<Sha256>(&ctx, "sha256sum").await
39+
}
40+
}
41+
42+
async fn checksum_execute<D: Digest>(ctx: &Context<'_>, cmd: &str) -> Result<ExecResult> {
43+
let files: Vec<&String> = ctx.args.iter().filter(|a| !a.starts_with('-')).collect();
44+
45+
let mut output = String::new();
46+
47+
if files.is_empty() {
48+
// Read from stdin
49+
let input = ctx.stdin.unwrap_or("");
50+
let hash = hex_digest::<D>(input.as_bytes());
51+
output.push_str(&hash);
52+
output.push_str(" -\n");
53+
} else {
54+
for file in &files {
55+
let path = if file.starts_with('/') {
56+
std::path::PathBuf::from(file)
57+
} else {
58+
ctx.cwd.join(file)
59+
};
60+
61+
match ctx.fs.read_file(&path).await {
62+
Ok(content) => {
63+
let hash = hex_digest::<D>(&content);
64+
output.push_str(&hash);
65+
output.push_str(" ");
66+
output.push_str(file);
67+
output.push('\n');
68+
}
69+
Err(e) => {
70+
return Ok(ExecResult::err(format!("{}: {}: {}\n", cmd, file, e), 1));
71+
}
72+
}
73+
}
74+
}
75+
76+
Ok(ExecResult::ok(output))
77+
}
78+
79+
fn hex_digest<D: Digest>(data: &[u8]) -> String {
80+
let result = D::digest(data);
81+
result.iter().map(|b| format!("{:02x}", b)).collect()
82+
}
83+
84+
#[cfg(test)]
85+
#[allow(clippy::unwrap_used)]
86+
mod tests {
87+
use super::*;
88+
use std::collections::HashMap;
89+
use std::path::PathBuf;
90+
use std::sync::Arc;
91+
92+
use crate::fs::InMemoryFs;
93+
94+
async fn run_checksum<B: Builtin>(
95+
builtin: &B,
96+
args: &[&str],
97+
stdin: Option<&str>,
98+
) -> ExecResult {
99+
let fs = Arc::new(InMemoryFs::new());
100+
let mut variables = HashMap::new();
101+
let env = HashMap::new();
102+
let mut cwd = PathBuf::from("/");
103+
104+
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
105+
let ctx = Context {
106+
args: &args,
107+
env: &env,
108+
variables: &mut variables,
109+
cwd: &mut cwd,
110+
fs,
111+
stdin,
112+
#[cfg(feature = "http_client")]
113+
http_client: None,
114+
#[cfg(feature = "git")]
115+
git_client: None,
116+
};
117+
118+
builtin.execute(ctx).await.unwrap()
119+
}
120+
121+
#[tokio::test]
122+
async fn test_md5sum_stdin() {
123+
let result = run_checksum(&Md5sum, &[], Some("hello\n")).await;
124+
assert_eq!(result.exit_code, 0);
125+
// md5("hello\n") = b1946ac92492d2347c6235b4d2611184
126+
assert!(result
127+
.stdout
128+
.starts_with("b1946ac92492d2347c6235b4d2611184"));
129+
assert!(result.stdout.contains(" -"));
130+
}
131+
132+
#[tokio::test]
133+
async fn test_sha256sum_stdin() {
134+
let result = run_checksum(&Sha256sum, &[], Some("hello\n")).await;
135+
assert_eq!(result.exit_code, 0);
136+
// sha256("hello\n") = 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
137+
assert!(result
138+
.stdout
139+
.starts_with("5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"));
140+
}
141+
142+
#[tokio::test]
143+
async fn test_sha1sum_stdin() {
144+
let result = run_checksum(&Sha1sum, &[], Some("hello\n")).await;
145+
assert_eq!(result.exit_code, 0);
146+
// sha1("hello\n") = f572d396fae9206628714fb2ce00f72e94f2258f
147+
assert!(result
148+
.stdout
149+
.starts_with("f572d396fae9206628714fb2ce00f72e94f2258f"));
150+
}
151+
152+
#[tokio::test]
153+
async fn test_md5sum_empty() {
154+
let result = run_checksum(&Md5sum, &[], Some("")).await;
155+
assert_eq!(result.exit_code, 0);
156+
// md5("") = d41d8cd98f00b204e9800998ecf8427e
157+
assert!(result
158+
.stdout
159+
.starts_with("d41d8cd98f00b204e9800998ecf8427e"));
160+
}
161+
}

crates/bashkit/src/builtins/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod archive;
2626
mod awk;
2727
mod base64;
2828
mod cat;
29+
mod checksum;
2930
mod column;
3031
mod comm;
3132
mod curl;
@@ -78,6 +79,7 @@ pub use archive::{Gunzip, Gzip, Tar};
7879
pub use awk::Awk;
7980
pub use base64::Base64;
8081
pub use cat::Cat;
82+
pub use checksum::{Md5sum, Sha1sum, Sha256sum};
8183
pub use column::Column;
8284
pub use comm::Comm;
8385
pub use curl::{Curl, Wget};

crates/bashkit/src/interpreter/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ impl Interpreter {
259259
builtins.insert("xxd".to_string(), Box::new(builtins::Xxd));
260260
builtins.insert("hexdump".to_string(), Box::new(builtins::Hexdump));
261261
builtins.insert("base64".to_string(), Box::new(builtins::Base64));
262+
builtins.insert("md5sum".to_string(), Box::new(builtins::Md5sum));
263+
builtins.insert("sha1sum".to_string(), Box::new(builtins::Sha1sum));
264+
builtins.insert("sha256sum".to_string(), Box::new(builtins::Sha256sum));
262265
builtins.insert("seq".to_string(), Box::new(builtins::Seq));
263266
builtins.insert("tac".to_string(), Box::new(builtins::Tac));
264267
builtins.insert("rev".to_string(), Box::new(builtins::Rev));
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
### md5sum_stdin
2+
# md5sum from stdin
3+
echo -n "hello" | md5sum
4+
### expect
5+
5d41402abc4b2a76b9719d911017c592 -
6+
### end
7+
8+
### sha1sum_stdin
9+
# sha1sum from stdin
10+
echo -n "hello" | sha1sum
11+
### expect
12+
aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d -
13+
### end
14+
15+
### sha256sum_stdin
16+
# sha256sum from stdin
17+
echo -n "hello" | sha256sum
18+
### expect
19+
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -
20+
### end
21+
22+
### md5sum_empty
23+
# md5sum of empty string
24+
echo -n "" | md5sum
25+
### expect
26+
d41d8cd98f00b204e9800998ecf8427e -
27+
### end
28+
29+
### sha256sum_newline
30+
# sha256sum with trailing newline
31+
echo "hello" | sha256sum
32+
### expect
33+
5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03 -
34+
### end
35+
36+
### md5sum_file
37+
# md5sum of a file
38+
echo -n "test" > /tmp/checkfile.txt
39+
md5sum /tmp/checkfile.txt
40+
### expect
41+
098f6bcd4621d373cade4e832627b4f6 /tmp/checkfile.txt
42+
### end
43+
44+
### sha1sum_file
45+
# sha1sum of a file
46+
echo -n "test" > /tmp/checkfile.txt
47+
sha1sum /tmp/checkfile.txt
48+
### expect
49+
a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 /tmp/checkfile.txt
50+
### end
51+
52+
### sha256sum_file
53+
# sha256sum of a file
54+
echo -n "test" > /tmp/checkfile.txt
55+
sha256sum /tmp/checkfile.txt
56+
### expect
57+
9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 /tmp/checkfile.txt
58+
### end
59+
60+
### md5sum_multiple_files
61+
# md5sum of multiple files
62+
echo -n "aaa" > /tmp/a.txt
63+
echo -n "bbb" > /tmp/b.txt
64+
md5sum /tmp/a.txt /tmp/b.txt
65+
### expect
66+
47bce5c74f589f4867dbd57e9ca9f808 /tmp/a.txt
67+
08f8e0260c64418510cefb2b06eee5cd /tmp/b.txt
68+
### end
69+
70+
### checksum_missing_file
71+
# checksum of non-existent file
72+
md5sum /tmp/nonexistent.txt
73+
### expect_exit_code
74+
1
75+
### end

0 commit comments

Comments
 (0)