bashkit 0.16.0

Awesomely fast virtual sandbox with bash and file system
Documentation
use std::process::Command;
use std::sync::{Arc, Mutex};

use bashkit::{Bash, Builtin, BuiltinContext, ExecOptions, ExecResult, StreamData, async_trait};

#[tokio::test]
async fn binary_base64_head_cat_pipeline_preserves_exact_bytes() {
    let mut bash = Bash::new();
    let result = bash
        .exec("printf 'AAH//kIAfw==' | base64 -d | head -c 7 | cat")
        .await
        .unwrap();

    assert_eq!(
        result.stdout.as_bytes(),
        &[0x00, 0x01, 0xff, 0xfe, b'B', 0x00, 0x7f]
    );
}

#[tokio::test]
async fn mixed_utf8_and_invalid_bytes_survive_stdin_pipeline_and_redirect() {
    let input = StreamData::from(vec![b'h', 0xc3, 0xa9, 0xff, 0x00, b'\n']);
    let mut bash = Bash::new();
    let result = bash
        .exec_with_options(
            "cat > /blob; cat /blob | base64 -w 0",
            ExecOptions::new().stdin(input),
        )
        .await
        .unwrap();

    assert_eq!(result.stdout.text().unwrap(), "aMOp/wAK\n");
    assert_eq!(
        bash.fs()
            .read_file(std::path::Path::new("/blob"))
            .await
            .unwrap(),
        [b'h', 0xc3, 0xa9, 0xff, 0x00, b'\n']
    );
}

#[tokio::test]
async fn byte_output_limit_and_streaming_callback_split_without_utf8_rewrite() {
    let chunks = Arc::new(Mutex::new(Vec::<u8>::new()));
    let callback_chunks = Arc::clone(&chunks);
    let mut bash = Bash::builder()
        .limits(bashkit::ExecutionLimits::new().max_stdout_bytes(4))
        .build();
    let result = bash
        .exec_with_options(
            "printf '/wABAg==' | base64 -d",
            ExecOptions::new().streaming(Box::new(move |stdout, _stderr| {
                callback_chunks
                    .lock()
                    .unwrap()
                    .extend_from_slice(stdout.as_bytes());
            })),
        )
        .await
        .unwrap();

    assert_eq!(result.stdout.as_bytes(), &[0xff, 0x00, 0x01, 0x02]);
    assert_eq!(*chunks.lock().unwrap(), [0xff, 0x00, 0x01, 0x02]);
}

struct CopyBytes;

#[async_trait]
impl Builtin for CopyBytes {
    async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        Ok(ExecResult::ok(ctx.stdin.cloned().unwrap_or_default()))
    }
}

#[tokio::test]
async fn custom_builtin_receives_byte_native_stdin() {
    let mut bash = Bash::builder()
        .builtin("copy-bytes", Box::new(CopyBytes))
        .build();
    let result = bash
        .exec("printf '/wD+' | base64 -d | copy-bytes")
        .await
        .unwrap();
    assert_eq!(result.stdout.as_bytes(), &[0xff, 0x00, 0xfe]);
}

#[tokio::test]
async fn execution_plan_preserves_binary_stdin() {
    let mut bash = Bash::new();
    let result = bash
        .exec("printf '/wD+' | base64 -d | timeout 1 cat")
        .await
        .unwrap();
    assert_eq!(result.stdout.as_bytes(), &[0xff, 0x00, 0xfe]);
}

#[tokio::test]
async fn c_locale_tr_filters_invalid_bytes_without_replacement_text() {
    let input = StreamData::from(vec![0xff, b'a', 0x80, b'7', 0x00, b'Z']);
    let mut bash = Bash::new();
    let result = bash
        .exec_with_options("LC_ALL=C tr -dc 'a-z0-9'", ExecOptions::new().stdin(input))
        .await
        .unwrap();

    assert_eq!(result.stdout.as_bytes(), b"a7");
}

#[tokio::test]
async fn command_substitution_drops_nuls_at_text_boundary() {
    let mut bash = Bash::new();
    let result = bash
        .exec("x=$(printf 'QQBCAA==' | base64 -d); printf '%s' \"$x\"")
        .await
        .unwrap();

    assert_eq!(result.stdout.as_bytes(), b"AB");
}

#[tokio::test]
async fn binary_pipeline_matches_real_bash_byte_for_byte() {
    let script = "printf 'AAHDqf/+QgB/' | base64 -d | head -c 8 | cat";
    let expected = Command::new("bash").args(["-c", script]).output().unwrap();
    assert!(expected.status.success());

    let mut bash = Bash::new();
    let actual = bash.exec(script).await.unwrap();
    assert_eq!(actual.stdout.as_bytes(), expected.stdout);
    assert_eq!(actual.exit_code, expected.status.code().unwrap());
}