use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
fn main() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dict_path = root.join("fuzz/dict/parse.dict");
let corpus_dir = root.join("fuzz/corpus/parse");
let mut names = BTreeSet::new(); let mut flags = BTreeSet::new(); let mut seeds = BTreeSet::new();
let mut files = Vec::new();
collect_toml_files(&root.join("commands"), &mut files);
files.sort();
for path in &files {
if path.file_name().and_then(|s| s.to_str()) == Some("SAMPLE.toml") {
continue;
}
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let value: toml::Value =
toml::from_str(&text).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()));
collect(&value, &mut names, &mut flags, &mut seeds);
}
write_dict(&dict_path, &names, &flags);
let written = write_seeds(&corpus_dir, &seeds);
eprintln!(
"dict: {} names + {} flags -> {}",
names.len(),
flags.len(),
dict_path.display()
);
eprintln!("seeds: {written} unique invocations -> {}", corpus_dir.display());
}
fn collect_toml_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else { return };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_toml_files(&path, out);
} else if path.extension().and_then(|s| s.to_str()) == Some("toml") {
out.push(path);
}
}
}
fn collect(
v: &toml::Value,
names: &mut BTreeSet<String>,
flags: &mut BTreeSet<String>,
seeds: &mut BTreeSet<String>,
) {
match v {
toml::Value::Table(t) => {
for (k, val) in t {
match k.as_str() {
"name" => {
if let Some(s) = val.as_str() {
add_token(s, names);
}
}
"standalone" | "valued" | "bare_flags" | "main_valued" | "main_variadic"
| "eval_safe_flags" | "eval_safe_required_flags" => add_string_tokens(val, flags),
"flags" => {
if let Some(tbl) = val.as_table() {
for fk in tbl.keys() {
add_token(fk, flags);
}
}
}
"examples_safe" | "examples_denied" => add_seeds(val, seeds),
_ => {}
}
collect(val, names, flags, seeds);
}
}
toml::Value::Array(a) => {
for el in a {
collect(el, names, flags, seeds);
}
}
_ => {}
}
}
fn add_token(s: &str, set: &mut BTreeSet<String>) {
if !s.is_empty() && s.len() <= 48 && !s.chars().any(char::is_whitespace) {
set.insert(s.to_string());
}
}
fn add_string_tokens(v: &toml::Value, set: &mut BTreeSet<String>) {
if let Some(arr) = v.as_array() {
for el in arr {
if let Some(s) = el.as_str() {
add_token(s, set);
}
}
}
}
fn add_seeds(v: &toml::Value, set: &mut BTreeSet<String>) {
if let Some(arr) = v.as_array() {
for el in arr {
if let Some(s) = el.as_str()
&& !s.is_empty()
{
set.insert(s.to_string());
}
}
}
}
fn write_dict(path: &Path, names: &BTreeSet<String>, flags: &BTreeSet<String>) {
const META: &[&str] = &[
"|", "||", "&&", "&", ";", "$(", ")", "${", "}", "`", "\"", "'", ">", ">>", "<", "2>&1",
"*", "?", "~", "../", "\\", "=", " -- ", "\n", "\t",
];
let mut entries: BTreeSet<String> = BTreeSet::new();
for tok in names.iter().chain(flags.iter()).map(String::as_str).chain(META.iter().copied()) {
entries.insert(dict_entry(tok));
}
let body: String = entries.into_iter().map(|e| e + "\n").collect();
let dir = path.parent().expect("dict path has a parent");
fs::create_dir_all(dir).expect("create dict dir");
fs::write(path, format!("# Generated by gen-fuzz-corpus from commands/**/*.toml. Do not edit.\n{body}"))
.expect("write dict");
}
fn dict_entry(tok: &str) -> String {
let mut out = String::from("\"");
for b in tok.bytes() {
match b {
b'"' => out.push_str("\\\""),
b'\\' => out.push_str("\\\\"),
0x20..=0x7e => out.push(b as char),
_ => out.push_str(&format!("\\x{b:02x}")),
}
}
out.push('"');
out
}
fn write_seeds(dir: &Path, seeds: &BTreeSet<String>) -> usize {
fs::create_dir_all(dir).expect("create corpus dir");
for seed in seeds {
let digest = Sha256::digest(seed.as_bytes());
let name: String = digest.iter().take(8).map(|b| format!("{b:02x}")).collect();
fs::write(dir.join(format!("gen-{name}")), seed.as_bytes()).expect("write seed");
}
seeds.len()
}