Merge nucleic/sleek-thistle-egret-fyej into dev

This commit is contained in:
2026-07-18 05:19:31 -07:00
commit b6be87b72d
677 changed files with 102939 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
+46
View File
@@ -0,0 +1,46 @@
[package]
name = "brush-fuzz"
description = "Fuzz tests for brush"
publish = false
version = "0.2.2"
authors.workspace = true
categories.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
[package.metadata]
cargo-fuzz = true
[lints]
workspace = true
[dependencies]
anyhow = "1.0.102"
assert_cmd = "2.2.0"
libfuzzer-sys = "0.4"
tokio = { version = "1.52.1", features = ["rt"] }
[dependencies.brush-core]
path = "../brush-core"
[dependencies.brush-parser]
path = "../brush-parser"
features = ["arbitrary"]
[[bin]]
name = "fuzz_parse"
path = "fuzz_targets/fuzz_parse.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_arithmetic"
path = "fuzz_targets/fuzz_arithmetic.rs"
test = false
doc = false
bench = false
+97
View File
@@ -0,0 +1,97 @@
#![no_main]
#![allow(missing_docs)]
#![allow(clippy::unwrap_used)]
use std::sync::LazyLock;
use anyhow::Result;
use brush_parser::ast;
use libfuzzer_sys::fuzz_target;
static TOKIO_RT: LazyLock<tokio::runtime::Runtime> =
LazyLock::new(|| tokio::runtime::Runtime::new().unwrap());
static SHELL_TEMPLATE: LazyLock<brush_core::Shell> = LazyLock::new(|| {
TOKIO_RT
.block_on(
brush_core::Shell::builder()
.profile(brush_core::ProfileLoadBehavior::Skip)
.rc(brush_core::RcLoadBehavior::Skip)
.build(),
)
.unwrap()
});
fn eval_arithmetic(mut shell: brush_core::Shell, input: &ast::ArithmeticExpr) -> Result<()> {
const DEFAULT_TIMEOUT_IN_SECONDS: u64 = 15;
//
// Turn it back into a string so we can pass it in on the command-line.
//
let input_str = input.to_string();
//
// Instantiate a brush shell with defaults, then try to evaluate the expression.
//
let parsed_expr = brush_parser::arithmetic::parse(input_str.as_str()).ok();
let our_eval_result = if let Some(parsed_expr) = parsed_expr {
shell.eval_arithmetic(&parsed_expr).ok()
} else {
None
};
//
// Now run it under 'bash'
//
let mut oracle_cmd = std::process::Command::new("bash");
oracle_cmd
.arg("--noprofile")
.arg("--norc")
.arg("-O")
.arg("extglob")
.arg("-t");
let mut oracle_cmd = assert_cmd::Command::from_std(oracle_cmd);
oracle_cmd.timeout(std::time::Duration::from_secs(DEFAULT_TIMEOUT_IN_SECONDS));
let input = std::format!("echo \"$(( {input_str} ))\"\n");
oracle_cmd.write_stdin(input.as_bytes());
let oracle_result = oracle_cmd.output()?;
let oracle_eval_result = if oracle_result.status.success() {
let oracle_output = std::str::from_utf8(&oracle_result.stdout)?;
oracle_output.trim().parse::<i64>().ok()
} else {
None
};
//
// Compare results.
//
if our_eval_result != oracle_eval_result {
Err(anyhow::anyhow!(
"Mismatched eval results: {oracle_eval_result:?} from oracle vs. {our_eval_result:?} from our test (expr: '{input_str}', oracle result: {oracle_result:?})"
))
} else {
Ok(())
}
}
fuzz_target!(|input: ast::ArithmeticExpr| {
let s = input.to_string();
let s = s.trim();
// For now, intentionally ignore known problematic cases without actually running them.
if s.contains("+ 0")
|| s.is_empty()
|| s.contains(|c: char| c.is_ascii_control() || !c.is_ascii())
|| s.contains("$[")
// old deprecated form of arithmetic expansion
{
return;
}
let shell = SHELL_TEMPLATE.clone();
eval_arithmetic(shell, &input).unwrap();
});
+82
View File
@@ -0,0 +1,82 @@
#![no_main]
#![allow(missing_docs)]
#![allow(clippy::unwrap_used)]
use anyhow::Result;
use libfuzzer_sys::fuzz_target;
use std::sync::LazyLock;
static TOKIO_RT: LazyLock<tokio::runtime::Runtime> =
LazyLock::new(|| tokio::runtime::Runtime::new().unwrap());
static SHELL_TEMPLATE: LazyLock<brush_core::Shell> = LazyLock::new(|| {
TOKIO_RT
.block_on(
brush_core::Shell::builder()
.profile(brush_core::ProfileLoadBehavior::Skip)
.rc(brush_core::RcLoadBehavior::Skip)
.build(),
)
.unwrap()
});
#[expect(clippy::unused_async)]
async fn parse_async(shell: brush_core::Shell, input: String) -> Result<()> {
const DEFAULT_TIMEOUT_IN_SECONDS: u64 = 15;
//
// Instantiate a brush shell with defaults, then try to parse the input.
//
let our_parse_result = shell.parse_string(input.clone());
//
// Now run it under 'bash -n -t' as a crude way to see if it's at least valid syntax.
//
let mut oracle_cmd = std::process::Command::new("bash");
oracle_cmd
.arg("--noprofile")
.arg("--norc")
.arg("-O")
.arg("extglob")
.arg("-n")
.arg("-t");
let mut oracle_cmd = assert_cmd::Command::from_std(oracle_cmd);
oracle_cmd.timeout(std::time::Duration::from_secs(DEFAULT_TIMEOUT_IN_SECONDS));
let mut input = input;
input.push('\n');
oracle_cmd.write_stdin(input.as_bytes());
let oracle_result = oracle_cmd.output()?;
//
// Compare results.
//
if our_parse_result.is_ok() != oracle_result.status.success() {
Err(anyhow::anyhow!(
"Mismatched parse results: {oracle_result:?} vs {our_parse_result:?}"
))
} else {
Ok(())
}
}
fuzz_target!(|input: String| {
// Ignore known problematic cases without actually running them.
if input.is_empty()
|| input.contains(|c: char| c.is_ascii_control() || !c.is_ascii()) // non-ascii chars (or control sequences)
|| input.contains('!') // history expansions
|| (input.contains('[') && !input.contains(']')) // ???
|| input.contains("<<") // weirdness with here docs
|| input.ends_with('\\') // unterminated trailing escape char?
|| input.contains("|&")
// unimplemented bash-ism
{
return;
}
let shell = SHELL_TEMPLATE.clone();
TOKIO_RT.block_on(parse_async(shell, input)).unwrap();
});