#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "localfs")]
mod common;
use common::kernel_at;
use std::fs;
use tempfile::tempdir;
const BIN: &[u8] = b"\xff\x00\xfe\x80\x01\xc0kaish\xf5";
fn is_loud_binary_error(msg: &str) -> bool {
msg.contains("cannot be used as") || msg.contains("cannot cross the argv/text boundary")
}
async fn assert_loud_binary(script: &str) {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
match kernel.execute(script).await {
Ok(r) => {
assert_ne!(r.code, 0, "binary at a text sink must be a nonzero exit: {script:?}");
assert!(
is_loud_binary_error(&r.err),
"error should name the binary problem, got err={:?} for {script:?}",
r.err
);
assert!(
!r.text_out().contains("[binary") && !r.err.contains("[binary"),
"the placeholder must NOT leak to stdout or stderr: out={:?} err={:?}",
r.text_out(),
r.err
);
}
Err(e) => {
let msg = format!("{:#}", e);
assert!(
is_loud_binary_error(&msg),
"execute error should name the binary problem, got {msg:?} for {script:?}"
);
}
}
}
#[tokio::test]
async fn interpolating_a_binary_capture_is_loud_not_placeholder() {
assert_loud_binary(r#"b=$(cat src.bin); echo "x=$b""#).await;
}
#[tokio::test]
async fn bare_word_binary_into_echo_is_loud() {
assert_loud_binary("b=$(cat src.bin); echo $b").await;
}
#[tokio::test]
async fn binary_arg_into_printf_is_loud() {
assert_loud_binary(r#"b=$(cat src.bin); printf "val=%s\n" "$b""#).await;
}
#[tokio::test]
async fn binary_in_default_expansion_is_loud() {
assert_loud_binary(r#"b=$(cat src.bin); echo "v=${b:-none}""#).await;
}
#[tokio::test]
async fn text_capture_interpolation_is_unaffected() {
let dir = tempdir().unwrap();
let kernel = kernel_at(dir.path());
let result = kernel.execute(r#"t=$(echo hi); echo "x=$t""#).await.unwrap();
assert_eq!(result.code, 0, "text var must still work: {}", result.err);
assert_eq!(result.text_out().trim(), "x=hi");
}
#[tokio::test]
async fn text_capture_bare_word_is_unaffected() {
let dir = tempdir().unwrap();
let kernel = kernel_at(dir.path());
let result = kernel.execute("t=$(echo hi); echo $t").await.unwrap();
assert_eq!(result.code, 0, "text var must still work: {}", result.err);
assert_eq!(result.text_out().trim(), "hi");
}
#[cfg(all(target_os = "linux", feature = "subprocess"))]
#[tokio::test]
async fn bare_word_binary_into_external_argv_is_loud() {
assert_loud_binary("b=$(cat src.bin); /bin/echo $b").await;
}
#[tokio::test]
async fn mkdir_binary_path_positional_is_loud() {
assert_loud_binary("b=$(cat src.bin); mkdir $b").await;
}
#[tokio::test]
async fn rm_binary_path_positional_is_loud() {
assert_loud_binary("b=$(cat src.bin); rm $b").await;
}
#[tokio::test]
async fn cp_binary_dest_is_loud() {
assert_loud_binary("b=$(cat src.bin); cp src.bin $b").await;
}
#[tokio::test]
async fn ls_binary_path_positional_is_loud() {
assert_loud_binary("b=$(cat src.bin); ls $b").await;
}
#[tokio::test]
async fn double_bracket_file_test_binary_path_is_loud() {
assert_loud_binary("b=$(cat src.bin); if [[ -f $b ]]; then echo hit; fi").await;
}
#[tokio::test]
async fn test_builtin_file_test_binary_path_is_loud() {
assert_loud_binary("b=$(cat src.bin); test -f $b").await;
}
#[tokio::test]
async fn redirect_target_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); echo hi > $b").await;
}
#[tokio::test]
async fn case_glob_binary_operand_is_loud() {
assert_loud_binary(
r#"b=$(cat src.bin); case $b in
x) echo matched ;;
*) echo default ;;
esac"#,
)
.await;
}
#[tokio::test]
async fn equality_binary_operand_is_loud() {
assert_loud_binary(r#"b=$(cat src.bin); if [[ $b == x ]]; then echo hit; fi"#).await;
}
#[tokio::test]
async fn membership_binary_needle_against_record_is_loud() {
assert_loud_binary(r#"b=$(cat src.bin); r={"k": 1}; if [[ $b in $r ]]; then echo hit; fi"#)
.await;
}
#[cfg(all(target_os = "linux", feature = "subprocess"))]
#[tokio::test]
async fn env_export_of_binary_value_is_loud() {
assert_loud_binary("b=$(cat src.bin); export BIN=$b; /bin/true").await;
}
#[tokio::test]
async fn cat_binary_path_positional_is_loud() {
assert_loud_binary("b=$(cat src.bin); cat $b").await;
}
#[tokio::test]
async fn head_binary_path_does_not_silently_fall_back_to_stdin() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); echo from-stdin | head $b")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(result.err.contains("cannot be used as"), "err={:?}", result.err);
assert!(
!result.text_out().contains("from-stdin"),
"must not silently read stdin instead of erroring on the binary path: {:?}",
result.text_out()
);
}
#[tokio::test]
async fn wc_binary_path_positional_is_loud() {
assert_loud_binary("b=$(cat src.bin); wc $b").await;
}
#[tokio::test]
async fn cat_list_path_positional_is_loud() {
let dir = tempdir().unwrap();
let kernel = kernel_at(dir.path());
let result = kernel.execute("xs=$(fromjson '[1,2,3]'); cat $xs").await.unwrap();
assert_ne!(result.code, 0);
assert!(result.err.contains("cannot use a list as a path"), "err={:?}", result.err);
}
#[tokio::test]
async fn cat_record_path_positional_is_loud() {
let dir = tempdir().unwrap();
let kernel = kernel_at(dir.path());
let result = kernel.execute(r#"r=$(fromjson '{"k":1}'); cat $r"#).await.unwrap();
assert_ne!(result.code, 0);
assert!(result.err.contains("cannot use a record as a path"), "err={:?}", result.err);
}
#[tokio::test]
async fn cat_bool_path_positional_is_loud() {
let dir = tempdir().unwrap();
let kernel = kernel_at(dir.path());
let result = kernel.execute("b=$(fromjson true); cat $b").await.unwrap();
assert_ne!(result.code, 0);
assert!(result.err.contains("cannot use a bool (true) as a path"), "err={:?}", result.err);
}
#[tokio::test]
async fn cat_null_path_positional_is_loud() {
let dir = tempdir().unwrap();
let kernel = kernel_at(dir.path());
let result = kernel.execute("n=$(fromjson null); cat $n").await.unwrap();
assert_ne!(result.code, 0);
assert!(result.err.contains("cannot use null as a path"), "err={:?}", result.err);
}
#[tokio::test]
async fn head_list_path_does_not_silently_fall_back_to_stdin() {
let dir = tempdir().unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("xs=$(fromjson '[1,2,3]'); echo from-stdin | head $xs")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(result.err.contains("cannot use a list as a path"), "err={:?}", result.err);
assert!(
!result.text_out().contains("from-stdin"),
"must not silently read stdin instead of erroring on the list path: {:?}",
result.text_out()
);
}
#[tokio::test]
async fn cd_binary_path_does_not_silently_go_to_home() {
assert_loud_binary("b=$(cat src.bin); cd $b").await;
}
#[tokio::test]
async fn awk_binary_path_does_not_silently_fall_back_to_stdin() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute(r#"b=$(cat src.bin); echo from-stdin | awk '{print}' $b"#)
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(result.err.contains("cannot be used as"), "err={:?}", result.err);
assert!(
!result.text_out().contains("from-stdin"),
"must not silently read stdin instead of erroring on the binary path: {:?}",
result.text_out()
);
}
#[tokio::test]
async fn basename_binary_path_is_loud() {
assert_loud_binary("b=$(cat src.bin); basename $b").await;
}
#[tokio::test]
async fn diff_binary_first_file_is_loud() {
assert_loud_binary("b=$(cat src.bin); diff $b src.bin").await;
}
#[tokio::test]
async fn sed_binary_path_does_not_silently_fall_back_to_stdin() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); echo from-stdin | sed 's/a/b/' $b")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(result.err.contains("cannot be used as"), "err={:?}", result.err);
assert!(
!result.text_out().contains("from-stdin"),
"must not silently read stdin instead of erroring on the binary path: {:?}",
result.text_out()
);
}
#[tokio::test]
async fn uniq_binary_path_does_not_silently_fall_back_to_stdin() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); echo from-stdin | uniq $b")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(result.err.contains("cannot be used as"), "err={:?}", result.err);
assert!(
!result.text_out().contains("from-stdin"),
"must not silently read stdin instead of erroring on the binary path: {:?}",
result.text_out()
);
}
#[tokio::test]
async fn jq_binary_path_does_not_silently_fall_back_to_stdin() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute(r#"b=$(cat src.bin); echo '{"from":"stdin"}' | jq '.' $b"#)
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(result.err.contains("cannot be used as"), "err={:?}", result.err);
assert!(
!result.text_out().contains("stdin"),
"must not silently read stdin instead of erroring on the binary path: {:?}",
result.text_out()
);
}
#[tokio::test]
async fn tree_binary_path_is_loud() {
assert_loud_binary("b=$(cat src.bin); tree $b").await;
}
#[tokio::test]
async fn write_binary_path_is_loud() {
assert_loud_binary(r#"b=$(cat src.bin); write $b "content""#).await;
}
#[tokio::test]
async fn ln_binary_target_is_loud() {
assert_loud_binary("b=$(cat src.bin); ln -s $b link.txt").await;
}
#[tokio::test]
async fn patch_binary_file_override_is_loud() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute(r#"b=$(cat src.bin); echo "diff" | patch --file=$b"#)
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(is_loud_binary_error(&result.err), "err={:?}", result.err);
}
#[tokio::test]
async fn validate_binary_path_does_not_silently_fall_back_to_stdin() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); echo 'echo hi' | kaish-validate $b")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(result.err.contains("cannot be used as"), "err={:?}", result.err);
}
#[tokio::test]
async fn checksum_binary_check_override_is_loud() {
assert_loud_binary("b=$(cat src.bin); checksum --check=$b").await;
}
#[tokio::test]
async fn seq_separator_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); seq --separator=$b 1 3").await;
}
#[tokio::test]
async fn cut_fields_binary_is_loud_not_blank_output() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute(r#"b=$(cat src.bin); echo "a,b,c" | cut --fields=$b -d,"#)
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(is_loud_binary_error(&result.err), "err={:?}", result.err);
assert!(
!result.text_out().contains('\n') || result.text_out().is_empty(),
"must not silently emit blank output lines: {:?}",
result.text_out()
);
}
#[tokio::test]
async fn cut_characters_binary_is_loud_not_blank_output() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); echo abcdef | cut --characters=$b")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(is_loud_binary_error(&result.err), "err={:?}", result.err);
}
#[tokio::test]
async fn awk_field_separator_binary_is_loud() {
assert_loud_binary(r#"b=$(cat src.bin); echo "a:b:c" | awk --field-separator=$b '{print $2}'"#)
.await;
}
#[tokio::test]
async fn cmp_binary_first_operand_is_loud() {
assert_loud_binary("b=$(cat src.bin); cmp $b src.bin").await;
}
#[cfg(feature = "subprocess")]
#[tokio::test]
async fn exec_binary_command_is_loud() {
assert_loud_binary("b=$(cat src.bin); exec $b arg1").await;
}
#[tokio::test]
async fn printf_binary_format_is_loud() {
assert_loud_binary("b=$(cat src.bin); printf $b").await;
}
#[cfg(feature = "subprocess")]
#[tokio::test]
async fn spawn_binary_command_is_loud() {
assert_loud_binary("b=$(cat src.bin); spawn --command=$b").await;
}
#[cfg(all(target_os = "linux", feature = "subprocess"))]
#[tokio::test]
async fn spawn_binary_cwd_is_loud() {
assert_loud_binary(r#"b=$(cat src.bin); spawn --command="/bin/true" --cwd=$b"#).await;
}
#[tokio::test]
async fn heredoc_body_binary_var_is_loud() {
assert_loud_binary("b=$(cat src.bin); cat <<EOF\n$b\nEOF").await;
}
#[tokio::test]
async fn record_key_binary_var_is_loud() {
assert_loud_binary(r#"b=$(cat src.bin); r={"$b": 1}"#).await;
}
#[tokio::test]
async fn length_of_binary_is_byte_count_not_loud() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel.execute("b=$(cat src.bin); echo ${#b}").await.unwrap();
assert_eq!(result.code, 0, "err={}", result.err);
assert_eq!(result.text_out().trim(), BIN.len().to_string());
}
#[tokio::test]
async fn awk_dash_v_binary_assignment_is_loud() {
assert_loud_binary("b=$(cat src.bin); awk -v x=$b 'BEGIN{}'").await;
}
#[tokio::test]
async fn test_builtin_raw_argv_named_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); test --foo=$b").await;
}
#[tokio::test]
async fn test_builtin_raw_argv_word_assign_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); test foo=$b").await;
}
#[tokio::test]
async fn test_builtin_bare_positional_truthiness_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); test $b").await;
}
#[tokio::test]
async fn test_builtin_dash_z_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); test -z $b").await;
}
#[tokio::test]
async fn test_builtin_dash_n_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); test -n $b").await;
}
#[tokio::test]
async fn dd_word_assign_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); dd if=$b").await;
}
#[tokio::test]
async fn alias_named_binary_value_is_loud() {
assert_loud_binary("b=$(cat src.bin); alias name=$b").await;
}
#[tokio::test]
async fn alias_positional_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); alias $b").await;
}
#[tokio::test]
async fn unalias_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); unalias $b").await;
}
#[tokio::test]
async fn unset_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); unset $b").await;
}
#[tokio::test]
async fn kill_signal_named_binary_is_loud() {
assert_loud_binary("b=$(cat src.bin); kill --signal=$b %1").await;
}
#[tokio::test]
async fn grep_ftype_binary_is_loud_not_silently_dropped() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
fs::write(dir.path().join("plain.txt"), "hello world\n").unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); grep --ftype=$b pattern plain.txt")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(
result.err.contains("cannot be used as"),
"error should name the binary problem, got err={:?}",
result.err
);
}
#[tokio::test]
async fn awk_v_bare_single_binary_occurrence_is_loud_not_silently_dropped() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); awk -v $b 'BEGIN{}'")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(
result.err.contains("cannot be used as"),
"error should name the binary problem, got err={:?}",
result.err
);
}
#[tokio::test]
async fn awk_v_multi_occurrence_binary_is_loud_not_silently_dropped() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); awk -v x=1 -v $b 'BEGIN{print x}'")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(
result.err.contains("cannot be used as"),
"error should name the binary problem, got err={:?}",
result.err
);
}
#[tokio::test]
async fn env_u_single_binary_occurrence_is_loud_not_silently_dropped() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel.execute("b=$(cat src.bin); env -u $b").await.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(
result.err.contains("cannot be used as"),
"error should name the binary problem, got err={:?}",
result.err
);
}
#[tokio::test]
async fn env_u_multi_occurrence_binary_is_loud_not_silently_dropped() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); env -u FOO -u $b")
.await
.unwrap();
assert_ne!(result.code, 0, "err={}", result.err);
assert!(
result.err.contains("cannot be used as"),
"error should name the binary problem, got err={:?}",
result.err
);
}
#[tokio::test]
async fn write_binary_content_positional_round_trips_byte_for_byte() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); write dest.bin $b")
.await
.unwrap();
assert!(result.ok(), "err={}", result.err);
let written = fs::read(dir.path().join("dest.bin")).unwrap();
assert_eq!(
written, BIN,
"binary content must round-trip byte-for-byte through a positional, \
not the `[binary: N bytes]` placeholder"
);
}
#[tokio::test]
async fn write_binary_content_named_flag_round_trips_byte_for_byte() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("b=$(cat src.bin); write dest.bin --content=$b")
.await
.unwrap();
assert!(result.ok(), "err={}", result.err);
let written = fs::read(dir.path().join("dest.bin")).unwrap();
assert_eq!(
written, BIN,
"binary content must round-trip byte-for-byte through the named --content flag, \
not error at the to_argv() boundary nor leak the `[binary: N bytes]` placeholder"
);
}
#[tokio::test]
async fn push_binary_positional_value_is_not_rejected_by_to_argv() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("src.bin"), BIN).unwrap();
let kernel = kernel_at(dir.path());
let result = kernel
.execute("xs=[]; b=$(cat src.bin); push xs $b; echo ${#xs}")
.await
.unwrap();
assert!(result.ok(), "err={}", result.err);
assert_eq!(result.text_out().trim(), "1", "push must append exactly one element");
}