Skip to content

Commit bd3aec5

Browse files
authored
feat(builtins): support -d @- and -d @file in curl builtin (#929)
Add support for `-d @-` (read request body from stdin) and `-d @/path/to/file` (read from VFS file) in the curl builtin, matching real curl behavior. File-not-found returns exit code 26. Also regenerate cargo-vet exemptions for updated transitive dependencies.\n\nCloses #928
1 parent d43a3b0 commit bd3aec5

File tree

3 files changed

+138
-48
lines changed

3 files changed

+138
-48
lines changed

crates/bashkit/src/builtins/curl.rs

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
1414
use async_trait::async_trait;
1515

16-
#[cfg(feature = "http_client")]
1716
use super::resolve_path;
1817
use super::{Builtin, Context};
1918
use crate::error::Result;
@@ -171,6 +170,33 @@ impl Builtin for Curl {
171170
i += 1;
172171
}
173172

173+
// Resolve -d @- (stdin) and -d @file (VFS file) before sending
174+
if let Some(ref d) = data
175+
&& let Some(path) = d.strip_prefix('@')
176+
{
177+
if path == "-" {
178+
// Read from stdin
179+
data = Some(ctx.stdin.unwrap_or("").to_string());
180+
} else {
181+
// Read from VFS file
182+
let resolved = resolve_path(ctx.cwd, path);
183+
match ctx.fs.read_file(&resolved).await {
184+
Ok(content) => {
185+
data = Some(String::from_utf8_lossy(&content).into_owned());
186+
}
187+
Err(_) => {
188+
return Ok(ExecResult::err(
189+
format!(
190+
"curl: Failed reading data file {}: No such file or directory\n",
191+
path
192+
),
193+
26,
194+
));
195+
}
196+
}
197+
}
198+
}
199+
174200
// Validate URL
175201
let url = match url {
176202
Some(u) => u,
@@ -1015,7 +1041,7 @@ mod tests {
10151041
use std::path::PathBuf;
10161042
use std::sync::Arc;
10171043

1018-
use crate::fs::InMemoryFs;
1044+
use crate::fs::{FileSystem, InMemoryFs};
10191045

10201046
async fn run_curl(args: &[&str]) -> ExecResult {
10211047
let fs = Arc::new(InMemoryFs::new());
@@ -1094,6 +1120,90 @@ mod tests {
10941120
assert!(result.stderr.contains("network access not configured"));
10951121
}
10961122

1123+
async fn run_curl_with_stdin_and_fs(
1124+
args: &[&str],
1125+
stdin: Option<&str>,
1126+
files: &[(&str, &[u8])],
1127+
) -> ExecResult {
1128+
let fs = Arc::new(InMemoryFs::new());
1129+
for (path, content) in files {
1130+
fs.write_file(std::path::Path::new(path), content)
1131+
.await
1132+
.unwrap();
1133+
}
1134+
let mut variables = HashMap::new();
1135+
let env = HashMap::new();
1136+
let mut cwd = PathBuf::from("/");
1137+
1138+
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
1139+
let ctx = Context {
1140+
args: &args,
1141+
env: &env,
1142+
variables: &mut variables,
1143+
cwd: &mut cwd,
1144+
fs,
1145+
stdin,
1146+
#[cfg(feature = "http_client")]
1147+
http_client: None,
1148+
#[cfg(feature = "git")]
1149+
git_client: None,
1150+
shell: None,
1151+
};
1152+
1153+
Curl.execute(ctx).await.unwrap()
1154+
}
1155+
1156+
#[tokio::test]
1157+
async fn test_curl_data_at_stdin() {
1158+
// -d @- should read from stdin (network not configured, but data resolution
1159+
// happens before the network check)
1160+
let result =
1161+
run_curl_with_stdin_and_fs(&["-d", "@-", "https://example.com"], Some("hello"), &[])
1162+
.await;
1163+
// Without network, we get the "network access not configured" error,
1164+
// but the important thing is that @- was resolved (not sent literally)
1165+
assert!(result.stderr.contains("network access not configured"));
1166+
}
1167+
1168+
#[tokio::test]
1169+
async fn test_curl_data_at_file() {
1170+
let result = run_curl_with_stdin_and_fs(
1171+
&["-d", "@/data.json", "https://example.com"],
1172+
None,
1173+
&[("/data.json", b"{\"key\":\"value\"}")],
1174+
)
1175+
.await;
1176+
assert!(result.stderr.contains("network access not configured"));
1177+
}
1178+
1179+
#[tokio::test]
1180+
async fn test_curl_data_at_file_not_found() {
1181+
let result =
1182+
run_curl_with_stdin_and_fs(&["-d", "@/missing.json", "https://example.com"], None, &[])
1183+
.await;
1184+
assert_ne!(result.exit_code, 0);
1185+
assert_eq!(result.exit_code, 26);
1186+
assert!(result.stderr.contains("Failed reading data file"));
1187+
}
1188+
1189+
#[tokio::test]
1190+
async fn test_curl_data_at_stdin_none() {
1191+
// -d @- with no stdin should resolve to empty string
1192+
let result =
1193+
run_curl_with_stdin_and_fs(&["-d", "@-", "https://example.com"], None, &[]).await;
1194+
// Should proceed past data resolution (get network error, not a data error)
1195+
assert!(result.stderr.contains("network access not configured"));
1196+
}
1197+
1198+
#[tokio::test]
1199+
async fn test_curl_data_literal_no_at() {
1200+
// Regular -d without @ prefix should pass through unchanged
1201+
let result =
1202+
run_curl_with_stdin_and_fs(&["-d", "plain-data", "https://example.com"], None, &[])
1203+
.await;
1204+
assert!(result.stderr.contains("network access not configured"));
1205+
}
1206+
10971207
#[cfg(feature = "http_client")]
10981208
mod network_tests {
10991209
use super::*;

specs/005-builtins.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ persist in shell variables as usual.
161161

162162
#### Network
163163
- `curl` - HTTP client (requires http_client feature + allowlist)
164-
- Options: `-s/--silent`, `-o FILE`, `-X METHOD`, `-d DATA`, `-H HEADER`, `-I/--head`, `-f/--fail`, `-L/--location`, `-w FORMAT`, `--compressed`, `-u/--user`, `-A/--user-agent`, `-e/--referer`, `-v/--verbose`, `-m/--max-time`, `--connect-timeout`
164+
- Options: `-s/--silent`, `-o FILE`, `-X METHOD`, `-d DATA` (supports `@-` for stdin, `@file` for VFS file), `-H HEADER`, `-I/--head`, `-f/--fail`, `-L/--location`, `-w FORMAT`, `--compressed`, `-u/--user`, `-A/--user-agent`, `-e/--referer`, `-v/--verbose`, `-m/--max-time`, `--connect-timeout`
165165
- Security: URL allowlist enforced, 10MB response limit, timeouts clamped to [1s, 10min], zip bomb protection via size-limited decompression
166166
- `wget` - Download files (requires http_client feature + allowlist)
167167
- Options: `-q/--quiet`, `-O FILE`, `--spider`, `--header`, `-U/--user-agent`, `--post-data`, `-t/--tries`, `-T/--timeout`, `--connect-timeout`

supply-chain/config.toml

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -430,10 +430,6 @@ criteria = "safe-to-deploy"
430430
version = "0.3.32"
431431
criteria = "safe-to-deploy"
432432

433-
[[exemptions.generic-array]]
434-
version = "0.14.7"
435-
criteria = "safe-to-deploy"
436-
437433
[[exemptions.get-size-derive2]]
438434
version = "0.7.4"
439435
criteria = "safe-to-deploy"
@@ -531,31 +527,31 @@ version = "0.1.2"
531527
criteria = "safe-to-deploy"
532528

533529
[[exemptions.icu_collections]]
534-
version = "2.1.1"
530+
version = "2.2.0"
535531
criteria = "safe-to-deploy"
536532

537533
[[exemptions.icu_locale_core]]
538-
version = "2.1.1"
534+
version = "2.2.0"
539535
criteria = "safe-to-deploy"
540536

541537
[[exemptions.icu_normalizer]]
542-
version = "2.1.1"
538+
version = "2.2.0"
543539
criteria = "safe-to-deploy"
544540

545541
[[exemptions.icu_normalizer_data]]
546-
version = "2.1.1"
542+
version = "2.2.0"
547543
criteria = "safe-to-deploy"
548544

549545
[[exemptions.icu_properties]]
550-
version = "2.1.2"
546+
version = "2.2.0"
551547
criteria = "safe-to-deploy"
552548

553549
[[exemptions.icu_properties_data]]
554-
version = "2.1.2"
550+
version = "2.2.0"
555551
criteria = "safe-to-deploy"
556552

557553
[[exemptions.icu_provider]]
558-
version = "2.1.1"
554+
version = "2.2.0"
559555
criteria = "safe-to-deploy"
560556

561557
[[exemptions.id-arena]]
@@ -590,10 +586,6 @@ criteria = "safe-to-deploy"
590586
version = "2.12.0"
591587
criteria = "safe-to-deploy"
592588

593-
[[exemptions.iri-string]]
594-
version = "0.7.11"
595-
criteria = "safe-to-deploy"
596-
597589
[[exemptions.is-macro]]
598590
version = "0.3.7"
599591
criteria = "safe-to-deploy"
@@ -663,15 +655,15 @@ version = "0.1.34"
663655
criteria = "safe-to-deploy"
664656

665657
[[exemptions.js-sys]]
666-
version = "0.3.93"
658+
version = "0.3.94"
667659
criteria = "safe-to-deploy"
668660

669661
[[exemptions.leb128fmt]]
670662
version = "0.1.0"
671663
criteria = "safe-to-deploy"
672664

673665
[[exemptions.libc]]
674-
version = "0.2.183"
666+
version = "0.2.184"
675667
criteria = "safe-to-deploy"
676668

677669
[[exemptions.libloading]]
@@ -687,7 +679,7 @@ version = "0.12.1"
687679
criteria = "safe-to-run"
688680

689681
[[exemptions.litemap]]
690-
version = "0.8.1"
682+
version = "0.8.2"
691683
criteria = "safe-to-deploy"
692684

693685
[[exemptions.lock_api]]
@@ -842,10 +834,6 @@ criteria = "safe-to-deploy"
842834
version = "0.2.17"
843835
criteria = "safe-to-deploy"
844836

845-
[[exemptions.pin-utils]]
846-
version = "0.1.0"
847-
criteria = "safe-to-deploy"
848-
849837
[[exemptions.plotters]]
850838
version = "0.3.7"
851839
criteria = "safe-to-run"
@@ -871,7 +859,7 @@ version = "1.1.3"
871859
criteria = "safe-to-deploy"
872860

873861
[[exemptions.potential_utf]]
874-
version = "0.1.4"
862+
version = "0.1.5"
875863
criteria = "safe-to-deploy"
876864

877865
[[exemptions.ppv-lite86]]
@@ -1038,10 +1026,6 @@ criteria = "safe-to-deploy"
10381026
version = "0.1.6"
10391027
criteria = "safe-to-deploy"
10401028

1041-
[[exemptions.regex-lite]]
1042-
version = "0.1.9"
1043-
criteria = "safe-to-deploy"
1044-
10451029
[[exemptions.regex-syntax]]
10461030
version = "0.8.10"
10471031
criteria = "safe-to-deploy"
@@ -1054,10 +1038,6 @@ criteria = "safe-to-deploy"
10541038
version = "0.17.14"
10551039
criteria = "safe-to-deploy"
10561040

1057-
[[exemptions.rustc-hash]]
1058-
version = "2.1.1"
1059-
criteria = "safe-to-deploy"
1060-
10611041
[[exemptions.rustc-hash]]
10621042
version = "2.1.2"
10631043
criteria = "safe-to-deploy"
@@ -1307,7 +1287,7 @@ version = "2.0.18"
13071287
criteria = "safe-to-deploy"
13081288

13091289
[[exemptions.tinystr]]
1310-
version = "0.8.2"
1290+
version = "0.8.3"
13111291
criteria = "safe-to-deploy"
13121292

13131293
[[exemptions.tinytemplate]]
@@ -1467,23 +1447,23 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
14671447
criteria = "safe-to-run"
14681448

14691449
[[exemptions.wasm-bindgen]]
1470-
version = "0.2.116"
1450+
version = "0.2.117"
14711451
criteria = "safe-to-deploy"
14721452

14731453
[[exemptions.wasm-bindgen-futures]]
1474-
version = "0.4.66"
1454+
version = "0.4.67"
14751455
criteria = "safe-to-deploy"
14761456

14771457
[[exemptions.wasm-bindgen-macro]]
1478-
version = "0.2.116"
1458+
version = "0.2.117"
14791459
criteria = "safe-to-deploy"
14801460

14811461
[[exemptions.wasm-bindgen-macro-support]]
1482-
version = "0.2.116"
1462+
version = "0.2.117"
14831463
criteria = "safe-to-deploy"
14841464

14851465
[[exemptions.wasm-bindgen-shared]]
1486-
version = "0.2.116"
1466+
version = "0.2.117"
14871467
criteria = "safe-to-deploy"
14881468

14891469
[[exemptions.wasm-encoder]]
@@ -1503,7 +1483,7 @@ version = "0.244.0"
15031483
criteria = "safe-to-deploy"
15041484

15051485
[[exemptions.web-sys]]
1506-
version = "0.3.93"
1486+
version = "0.3.94"
15071487
criteria = "safe-to-deploy"
15081488

15091489
[[exemptions.web-time]]
@@ -1711,11 +1691,11 @@ version = "1.0.1"
17111691
criteria = "safe-to-run"
17121692

17131693
[[exemptions.yoke]]
1714-
version = "0.8.1"
1694+
version = "0.8.2"
17151695
criteria = "safe-to-deploy"
17161696

17171697
[[exemptions.yoke-derive]]
1718-
version = "0.8.1"
1698+
version = "0.8.2"
17191699
criteria = "safe-to-deploy"
17201700

17211701
[[exemptions.zerocopy]]
@@ -1727,27 +1707,27 @@ version = "0.8.48"
17271707
criteria = "safe-to-deploy"
17281708

17291709
[[exemptions.zerofrom]]
1730-
version = "0.1.6"
1710+
version = "0.1.7"
17311711
criteria = "safe-to-deploy"
17321712

17331713
[[exemptions.zerofrom-derive]]
1734-
version = "0.1.6"
1714+
version = "0.1.7"
17351715
criteria = "safe-to-deploy"
17361716

17371717
[[exemptions.zeroize]]
17381718
version = "1.8.2"
17391719
criteria = "safe-to-deploy"
17401720

17411721
[[exemptions.zerotrie]]
1742-
version = "0.2.3"
1722+
version = "0.2.4"
17431723
criteria = "safe-to-deploy"
17441724

17451725
[[exemptions.zerovec]]
1746-
version = "0.11.5"
1726+
version = "0.11.6"
17471727
criteria = "safe-to-deploy"
17481728

17491729
[[exemptions.zerovec-derive]]
1750-
version = "0.11.2"
1730+
version = "0.11.3"
17511731
criteria = "safe-to-deploy"
17521732

17531733
[[exemptions.zmij]]

0 commit comments

Comments
 (0)