///|
/// Named protocol items. Names are one path segment and cannot contain dots.
pub(all) enum Node {
Leaf(String, Field)
Nested(Block)
Repeat(String, String, Int, Int, Int, String?)
Sized(String, SizeSpec)
Checksummed(String, ChecksumSpec)
Mirrored(String, String)
Dynamic(String, String, Field)
}
///|
pub struct Block {
name : String
children : Array[Node]
condition : Condition?
alignment : (Int, Bytes)?
group : String?
}
///|
pub fn Block::new(name : String, children : Array[Node]) -> Block {
{
name,
children: children.copy(),
condition: None,
alignment: None,
group: None,
}
}
///|
/// Link the block to a Group field; its candidates multiply with the block's
/// own mutation cases like upstream Block(group=...) (boofuzz/blocks/block.py
/// mutations, 518c139). The target is a full field path.
pub fn Block::with_group(self : Block, target : String) -> Block {
{ ..self, group: Some(target), }
}
///|
priv enum EntryKind {
Atom(Field)
Dynamic(Field, String)
Container(Array[Int])
Repetition(String, Int, Int, Int, String?)
ComputedSize(SizeSpec)
ComputedChecksum(ChecksumSpec)
Mirror(String)
}
///|
struct Entry {
path : String
parent : Int?
kind : EntryKind
condition : Condition?
alignment : (Int, Bytes)?
group : String?
}
///|
/// Lazy case sequence tree. Upstream enumerates a block as its plain child
/// mutations followed by group products replaying that same sequence once
/// per group candidate (boofuzz/blocks/block.py mutations, 518c139).
pub enum CaseSeq {
Plain(Int)
Sequence(Array[CaseSeq])
Product(Int, CaseSeq)
}
///|
pub struct CompiledRequest {
name : String
entries : Array[Entry]
paths : Map[String, Int]
max_bytes : Int
seq : CaseSeq
}
///|
/// Build the lazy case sequence for an entry. A grouped block replays its
/// children sequence once per group candidate after the plain pass.
fn build_case_seq(
entries : Array[Entry],
paths : Map[String, Int],
groups : Map[Int, Int],
index : Int,
) -> CaseSeq raise ModelError {
match entries[index].kind {
Container(children) => {
let inner = Sequence(
children.map(child => build_case_seq(entries, paths, groups, child)),
)
match groups.get(index) {
Some(group_entry) => Sequence([inner, Product(group_entry, inner)])
None => inner
}
}
Mirror(_) => Sequence([])
kind => if kind.count() > 0 { Plain(index) } else { Sequence([]) }
}
}
///|
fn valid_name(name : String) -> Bool {
!name.is_empty() &&
!name.contains(".") &&
!name.contains("/") &&
!name.contains("+")
}
///|
fn add_node(
node : Node,
prefix : String,
parent : Int?,
entries : Array[Entry],
paths : Map[String, Int],
depth : Int,
) -> Int raise ModelError {
guard depth <= 256 else { raise Invalid("nesting depth exceeds 256") }
let name = match node {
Leaf(name, _)
| Repeat(name, _, _, _, _, _)
| Sized(name, _)
| Checksummed(name, _)
| Mirrored(name, _)
| Dynamic(name, _, _) => name
Nested(block) => block.name
}
guard valid_name(name) else { raise Invalid("invalid name: " + name) }
let path = if prefix.is_empty() { name } else { prefix + "." + name }
guard !paths.contains(path) else { raise Invalid("duplicate path: " + path) }
let index = entries.length()
paths[path] = index
let kind = match node {
Leaf(_, field) => Atom(field)
Dynamic(_, variable, field) => Dynamic(field, variable)
Nested(_) => Container([])
Sized(_, spec) => ComputedSize(spec)
Checksummed(_, spec) => ComputedChecksum(spec)
Mirrored(_, target) => Mirror(target)
Repeat(_, target, min, max, step, variable) => {
guard min >= 0 && max >= min && step > 0 else {
raise Invalid("invalid repeat range or step")
}
guard (max - min) / step < 2147483647 else {
raise Invalid("repeat count overflow")
}
Repetition(target, min, max, step, variable)
}
}
let condition = match node {
Nested(block) => block.condition
_ => None
}
let alignment = match node {
Nested(block) => block.alignment
_ => None
}
let group = match node {
Nested(block) => block.group
_ => None
}
entries.push({ path, parent, kind, condition, alignment, group, })
if node is Nested(block) {
let children = block.children.map(child => {
add_node(child, path, Some(index), entries, paths, depth + 1)
})
entries[index] = {
path,
parent,
kind: Container(children),
condition,
alignment,
group,
}
}
index
}
///|
/// Validate and snapshot a named protocol tree. Limits count encoded wire bytes.
pub fn CompiledRequest::compile(
root : Block,
max_bytes? : Int = 1048576,
) -> CompiledRequest raise ModelError {
guard max_bytes >= 0 else { raise Invalid("negative request byte limit") }
let entries : Array[Entry] = []
let paths : Map[String, Int] = Map([])
ignore(add_node(Nested(root), "", None, entries, paths, 0))
// Resolve group references; a Group candidate multiplies the cases of the
// block that references it (upstream resolves by name at mutation time).
let groups : Map[Int, Int] = Map([])
for i, entry in entries {
if entry.group is Some(target) {
let group_entry = paths
.get(target)
.unwrap_or_else(() => raise Invalid("unknown path: " + target))
guard entries[group_entry].kind is Atom(_) else {
raise Invalid("group target must be a field")
}
groups[i] = group_entry
}
}
let seq = build_case_seq(entries, paths, groups, 0)
let request = { name: root.name, entries, paths, max_bytes, seq, }
for entry in entries {
if entry.kind is Repetition(path, _, _, _, _) {
let target = request.resolve(path)
let current = request.resolve(entry.path)
guard entries[target].kind is Container(_) &&
target < current &&
!entry.path.has_prefix(path + ".") else {
raise Invalid("repeat target must be a completed preceding block")
}
}
if entry.condition is Some(condition) {
let target = request.resolve(condition.path())
guard entries[target].kind is Atom(_) ||
entries[target].kind is Dynamic(_, _) else {
raise Invalid("condition target must be a field")
}
}
if entry.kind is Mirror(target) {
// A mirror may not live inside its target's subtree; indirect cycles
// are broken by the active chain during rendering.
let target_index = request.resolve(target)
let mut parent = entry.parent
while parent is Some(p) {
guard p != target_index else {
raise Invalid("mirror target contains the mirror")
}
parent = entries[p].parent
}
}
}
request.validate_sizes()
request.validate_checksums()
ignore(request.render())
request
}
///|
pub fn CompiledRequest::name(self : CompiledRequest) -> String {
self.name
}
///|
pub fn CompiledRequest::paths(self : CompiledRequest) -> Array[String] {
self.entries.map(entry => entry.path)
}
///|
pub fn CompiledRequest::parent_path(
self : CompiledRequest,
path : String,
) -> String? raise ModelError {
self.entries[self.resolve(path)].parent.map(index => self.entries[index].path)
}
///|
fn CompiledRequest::resolve(
self : CompiledRequest,
path : String,
) -> Int raise ModelError {
self.paths
.get(path)
.unwrap_or_else(() => raise Invalid("unknown path: " + path))
}
///|
/// Multi-point render replacements: (entry index, candidate bytes) pairs.
/// Entry indices are unique by construction, so at most one pair matches
/// any node; the first match wins.
type Replacements = Array[(Int, Bytes)]
///|
fn zero_byte() -> Byte {
let zero : Int = 0
zero.to_byte()
}
///|
fn Replacements::lookup(self : Replacements, index : Int) -> Bytes? {
for pair in self {
let (target, value) = pair
if target == index {
return Some(value)
}
}
None
}
///|
fn CompiledRequest::render_node(
self : CompiledRequest,
index : Int,
replacements : Replacements,
vars : Map[String, Bytes]?,
active? : Array[Int] = [],
) -> Bytes raise ModelError {
guard self.is_visible(index, replacements, vars) else { return Bytes::new(0) }
if !(self.entries[index].kind is Repetition(_, _, _, _, _)) {
match replacements.lookup(index) {
Some(value) => {
guard value.length() <= self.max_bytes else {
raise Limit("request byte limit: " + self.entries[index].path)
}
return value
}
None => ()
}
}
match self.entries[index].kind {
ComputedChecksum(spec) => {
if active.contains(index) {
// Recursive render: upstream renders length dummy zeros
// (checksum.py _get_dummy_value), so md5/sha1 pad to 16/20.
return Bytes::makei(spec.algorithm.length(), fn(_) { zero_byte() })
}
let nested = active.copy()
nested.push(index)
let bytes = self.render_node(
self.resolve(spec.target),
replacements,
vars,
active=nested,
)
spec.algorithm.render(bytes, spec.endian)
}
ComputedSize(spec) => self.size_value(spec, replacements, vars)
Mirror(target) => {
if active.contains(index) {
return Bytes::new(0)
}
let nested = active.copy()
nested.push(index)
self.render_node(self.resolve(target), replacements, vars, active=nested)
}
Repetition(path, _, _, _, variable) => {
let count = match replacements.lookup(index) {
Some(value) => decode_repeat(value)
None =>
// Dynamic repetition resolves its count from session variables;
// outside a test case it falls back to zero like upstream's
// ProtocolSessionReference(name, default_value=0).
match variable {
Some(name) =>
match vars {
Some(map) =>
match map.get(name) {
Some(bytes) => {
guard bytes.length() >= 4 else {
raise Invalid("repeat variable needs 4 bytes")
}
decode_repeat(bytes)
}
None => raise Invalid("missing session variable: " + name)
}
None => 0
}
None => 0
}
}
let bytes = self.render_node(
self.resolve(path),
replacements,
vars,
active~,
)
guard bytes.is_empty() || count <= self.max_bytes / bytes.length() else {
raise Limit("repeated block exceeds byte limit")
}
bytes.repeat(count)
}
Dynamic(field, variable) =>
// Upstream resolves ProtocolSessionReference values from the protocol
// session inside a test case and falls back to the stored default
// outside it (fuzzable.py original_value, 518c139).
match vars {
Some(map) =>
match map.get(variable) {
Some(bytes) => bytes
None => raise Invalid("missing session variable: " + variable)
}
None => field.value
}
Atom(field) => {
guard field.value.length() <= self.max_bytes else {
raise Limit("request byte limit: " + self.entries[index].path)
}
field.value
}
Container(children) => {
let output : Array[Byte] = []
for child in children {
let bytes = self.render_node(child, replacements, vars, active~)
guard bytes.length() <= self.max_bytes - output.length() else {
raise Limit("request byte limit: " + self.entries[index].path)
}
for byte in bytes {
output.push(byte)
}
}
if self.entries[index].alignment is Some((modulus, pattern)) {
let padding = modulus - output.length() % modulus
guard padding <= self.max_bytes - output.length() else {
raise Limit("aligned block exceeds byte limit")
}
for i in 0.. Bytes raise ModelError {
self.render_node(0, [], None)
}
///|
/// Render with session variables; inside a running case dynamic fields
/// resolve strictly, matching upstream node.render(mutation_context).
pub fn CompiledRequest::render_with(
self : CompiledRequest,
vars : Map[String, Bytes]?,
) -> Bytes raise ModelError {
self.render_node(0, [], vars)
}
///|
/// Render a mutation case (parts from MutationCase::parts) with session
/// variables, so edge-callback variable writes are visible at send time.
pub fn CompiledRequest::render_case(
self : CompiledRequest,
parts : Array[(String, Int)],
vars : Map[String, Bytes]?,
) -> Bytes raise ModelError {
let replacements : Replacements = []
for part in parts {
let index = self.resolve(part.0)
replacements.push((index, self.entries[index].kind.candidate(part.1)))
}
self.render_node(0, replacements, vars)
}
///|
pub fn CompiledRequest::render_path(
self : CompiledRequest,
path : String,
) -> Bytes raise ModelError {
self.render_node(self.resolve(path), [], None)
}
///|
/// Legacy flat fields receive stable names field0, field1, ... under name.
pub fn CompiledRequest::from_request(
name : String,
request : Request,
max_bytes? : Int = 1048576,
) -> CompiledRequest raise ModelError {
let children = request.fields.mapi(fn(i, field) {
Leaf("field\{i}", Field::simple(field.default_value(), field.mutations()))
})
CompiledRequest::compile(Block::new(name, children), max_bytes~)
}