// SPDX-License-Identifier: GPL-2.0-only
// Port of the UTF-8, dynamic-size subset of boofuzz primitives/string.py and delim.py.
///|
pub fn Field::delimiter(
value : String,
fuzzable? : Bool = true,
fuzz_values? : Array[Bytes] = [],
) -> Field {
let encoded = @utf8.encode(value)
let candidates : Array[TextCandidate] = []
for n in [2, 5, 10, 25, 100, 500, 1000] {
candidates.push(Repeated(encoded, n, 0))
}
candidates.push(Literal(b""))
if value == " " {
for n in [1, 2, 100] {
candidates.push(Repeated(b"\t", n, 0))
}
}
for
text in [
" ",
"\t",
"\t ".repeat(100),
"\t\r\n".repeat(100),
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"{",
"}",
"[",
"]",
"-",
"_",
"+",
"=",
":",
": ".repeat(100),
":7".repeat(100),
";",
"'",
"\"",
"/",
"\\",
"?",
"<",
">",
".",
",",
"\r",
"\n",
"\r\n".repeat(64),
"\r\n".repeat(128),
"\r\n".repeat(512),
] {
candidates.push(Literal(@utf8.encode(text)))
}
let base : Field = {
value: encoded,
count: if fuzzable {
candidates.length()
} else {
0
},
candidate: i => candidates[i].render(),
candidate_length: i => candidates[i].length(),
}
Field::with_fuzz_values(base, fuzz_values, fuzzable)
}
///|
enum TextCandidate {
Literal(Bytes)
Repeated(Bytes, Int, Int) // whole repetitions plus prefix length in bytes
Terminator(Int, Int)
} derive(Eq)
///|
pub extend TextCandidate with Eq::{not_equal, equal}
///|
fn TextCandidate::render(self : TextCandidate) -> Bytes {
match self {
Literal(b) => b
Repeated(seed, copies, prefix) =>
seed.repeat(copies) + seed[:prefix].to_owned()
Terminator(size, position) =>
b"D".repeat(position) + b"\x00" + b"D".repeat(size - position - 1)
}
}
///|
fn TextCandidate::length(self : TextCandidate) -> Int64 {
match self {
Literal(bytes) => bytes.length().to_int64()
Repeated(bytes, copies, prefix) =>
bytes.length().to_int64() * copies.to_int64() + prefix.to_int64()
Terminator(size, _) => size.to_int64()
}
}
///|
/// UTF-8 bad-string library, variable repeats and deterministic long strings.
/// Only dynamic size is supported. Enforce wire-byte limits at request rendering.
pub fn Field::text(
value : String,
fuzzable? : Bool = true,
fuzz_values? : Array[Bytes] = [],
) -> Field {
let encoded = @utf8.encode(value)
let candidates : Array[TextCandidate] = []
for text in string_library {
candidates.push(Literal(@utf8.encode(text)))
}
let max_library_chars = string_library.fold(init=0, (n, item) => {
n.max(item.length())
})
for n in [2, 10, 100] {
if value.length() > max_library_chars / n ||
!string_library.contains(value.repeat(n)) {
candidates.push(Repeated(encoded, n, 0))
}
}
let lengths = [8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 32768, 65535]
for seed in string_seeds {
let bytes = @utf8.encode(seed)
for length in lengths {
for delta in -2..<3 {
let n = length + delta
candidates.push(Repeated(bytes, n / seed.length(), n % seed.length()))
}
}
for n in [99999, 100000, 500000, 1000000] {
candidates.push(Repeated(bytes, n / seed.length(), n % seed.length()))
}
}
for i, length in lengths {
for position in string_null_indices[i] {
candidates.push(Terminator(length, position))
}
}
// With no max_len the only possible adjacent equality lies in literal entries.
let unique : Array[TextCandidate] = []
for candidate in candidates {
if unique.last() != Some(candidate) {
unique.push(candidate)
}
}
let base : Field = {
value: encoded,
count: if fuzzable {
unique.length()
} else {
0
},
candidate: fn(i) { unique[i].render() },
candidate_length: i => unique[i].length(),
}
Field::with_fuzz_values(base, fuzz_values, fuzzable)
}