lez-fuzzing/scripts/add_fuzz_target.py

178 lines
5.2 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""Fully automates registering a new cargo-fuzz target.
Usage:
python3 scripts/add_fuzz_target.py <TARGET_NAME>
Where TARGET_NAME is the full binary name, e.g. fuzz_my_feature.
Actions performed:
1. Appends a [[bin]] entry to fuzz/Cargo.toml
2. Inserts TARGET_NAME into every YAML matrix block in
.github/workflows/fuzz.yml (smoke-fuzz, regression)
3. Inserts TARGET_NAME into the perf-baseline shell for-loop in
.github/workflows/fuzz.yml
Run from the repository root.
"""
import re
import sys
from pathlib import Path
# Target names must follow the cargo-fuzz binary naming convention.
_TARGET_RE = re.compile(r"^fuzz_[a-z][a-z0-9_]*$")
def append_cargo_bin(target: str, cargo_toml: Path) -> None:
"""Append a [[bin]] entry to fuzz/Cargo.toml if not already present."""
content = cargo_toml.read_text()
if f'name = "{target}"' in content:
print(f" SKIP fuzz/Cargo.toml — [[bin]] {target!r} already present")
return
entry = (
f"\n[[bin]]\n"
f'name = "{target}"\n'
f'path = "fuzz_targets/{target}.rs"\n'
f"test = false\n"
f"bench = false\n"
)
cargo_toml.write_text(content.rstrip("\n") + "\n" + entry)
print(f" [+] fuzz/Cargo.toml — added [[bin]] {target!r}")
def insert_into_yaml_matrices(target: str, content: str) -> tuple[str, int]:
"""Insert target into YAML strategy matrix blocks.
Matches blocks of the form::
target:
- fuzz_a
- fuzz_b
and appends `` - <target>`` after the last existing entry.
"""
pattern = re.compile(
r"( target:\n(?: - fuzz_\w+\n)+)",
re.MULTILINE,
)
def add_target(m: re.Match) -> str:
return m.group(0) + f" - {target}\n"
new_content, count = pattern.subn(add_target, content)
return new_content, count
def insert_into_shell_loop(target: str, content: str) -> tuple[str, int]:
"""Insert target into a 'for target in ... ; do' shell loop.
The last entry in the loop ends with ``; do``. We change it to end with
a backslash continuation and append the new entry with ``; do``.
Example before::
fuzz_block_verification; do
After::
fuzz_block_verification \\
fuzz_new_target; do
"""
# Match the last fuzz target in the for-loop: " fuzz_xxx; do"
# Indentation: 12 spaces (inside a run: | block).
pattern = re.compile(r"( fuzz_\w+)(; do)", re.MULTILINE)
# We only want to replace the *last* occurrence (the closing entry).
matches = list(pattern.finditer(content))
if not matches:
return content, 0
if len(matches) > 1:
print(
f" ERROR: found {len(matches)} shell loops matching the pattern; "
"cannot determine which one to update. "
"Please edit .github/workflows/fuzz.yml manually.",
file=sys.stderr,
)
sys.exit(1)
m = matches[-1]
replacement = f"{m.group(1)} \\\n {target}{m.group(2)}"
new_content = content[: m.start()] + replacement + content[m.end() :]
return new_content, 1
def insert_into_workflow(target: str, workflow: Path) -> None:
"""Update all target lists in the fuzz workflow file."""
content = workflow.read_text()
if target in content:
print(f" SKIP .github/workflows/fuzz.yml — {target!r} already present")
return
# 1. YAML matrix blocks (smoke-fuzz, regression)
content, yaml_count = insert_into_yaml_matrices(target, content)
if yaml_count:
print(
f" [+] .github/workflows/fuzz.yml — inserted {target!r} into "
f"{yaml_count} YAML matrix block(s)"
)
else:
print(
f" ERROR: no YAML matrix blocks matched in {workflow} — please edit manually",
file=sys.stderr,
)
sys.exit(1)
# 2. Shell for-loop (perf-baseline)
content, loop_count = insert_into_shell_loop(target, content)
if loop_count:
print(
f" [+] .github/workflows/fuzz.yml — inserted {target!r} into "
f"perf-baseline shell loop"
)
else:
print(
f" ERROR: perf-baseline shell loop not found in {workflow} — please edit manually",
file=sys.stderr,
)
sys.exit(1)
workflow.write_text(content)
def main() -> None:
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <TARGET_NAME>", file=sys.stderr)
sys.exit(1)
target = sys.argv[1]
if not _TARGET_RE.match(target):
print(
f"ERROR: target name must match fuzz_[a-z][a-z0-9_]*, got: {target!r}",
file=sys.stderr,
)
sys.exit(1)
root = Path(__file__).parent.parent # repository root
cargo_toml = root / "fuzz" / "Cargo.toml"
workflow = root / ".github" / "workflows" / "fuzz.yml"
if not cargo_toml.exists():
print(f"ERROR: {cargo_toml} not found", file=sys.stderr)
sys.exit(1)
if not workflow.exists():
print(f"ERROR: {workflow} not found", file=sys.stderr)
sys.exit(1)
append_cargo_bin(target, cargo_toml)
insert_into_workflow(target, workflow)
if __name__ == "__main__":
main()