use std::fmt::Write as _;
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
use std::time::{Duration, Instant};
const BINARY: &str = env!("CARGO_BIN_EXE_ips-le");
const HAZARD: &str = "0177.0.0.99";
const OCTAL_READING: &str = "127.0.0.99";
const DECIMAL_READING: &str = "177.0.0.99";
const CASE_LIMIT: Duration = Duration::from_secs(20);
const FORMATS: [Option<&str>; 9] = [
Some("json"),
Some("yaml"),
Some("toml"),
Some("ini"),
Some("env"),
Some("csv"),
Some("log"),
Some("handwriting"),
None,
];
fn budget() -> Duration {
let seconds = std::env::var("IPS_LE_FUZZ_SECONDS")
.ok()
.and_then(|raw| raw.parse::<u64>().ok())
.unwrap_or(1);
Duration::from_secs(seconds)
}
fn seed() -> u64 {
std::env::var("IPS_LE_FUZZ_SEED")
.ok()
.and_then(|raw| raw.parse::<u64>().ok())
.unwrap_or(0x1b5e_0b1d_2026_0812)
}
struct Seeded(u64);
impl Seeded {
fn next(&mut self) -> u64 {
let mut state = self.0;
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
self.0 = state;
state.wrapping_mul(0x2545_f491_4f6c_dd1d)
}
fn below(&mut self, limit: usize) -> usize {
let limit = u64::try_from(limit).unwrap_or(1).max(1);
usize::try_from(self.next() % limit).unwrap_or(0)
}
fn pick<'a, T>(&mut self, from: &'a [T]) -> &'a T {
&from[self.below(from.len())]
}
}
struct Server {
child: Child,
stdin: ChildStdin,
lines: Receiver<Option<String>>,
}
impl Server {
fn start() -> Self {
let mut child = Command::new(BINARY)
.arg("mcp")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("the server starts");
let stdin = child.stdin.take().expect("stdin");
let stdout = child.stdout.take().expect("stdout");
let (sender, lines) = channel();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
if sender.send(line.ok()).is_err() {
return;
}
}
let _ = sender.send(None);
});
Self {
child,
stdin,
lines,
}
}
fn extract(&mut self, case: &str, arguments: &serde_json::Value) -> String {
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "extract_ips", "arguments": arguments },
});
writeln!(self.stdin, "{request}")
.unwrap_or_else(|error| panic!("{case}: the server stopped reading ({error})"));
self.stdin
.flush()
.unwrap_or_else(|error| panic!("{case}: could not flush ({error})"));
match self.lines.recv_timeout(CASE_LIMIT) {
Ok(Some(line)) => line,
Ok(None) => panic!("{case}: the server died — a panic, or a slice off a boundary"),
Err(RecvTimeoutError::Timeout) => {
panic!("{case}: no answer in {CASE_LIMIT:?} — a splitting rule is not terminating")
}
Err(RecvTimeoutError::Disconnected) => panic!("{case}: the server closed stdout"),
}
}
}
impl Drop for Server {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn colon_run(seeded: &mut Seeded) -> String {
match seeded.below(12) {
0 => "::".to_string(),
1 => ":".repeat(1 + seeded.below(80)),
2 => "::::::".to_string(),
3 => (0..40)
.map(|index| format!("{index:04x}"))
.collect::<Vec<String>>()
.join(":"),
4 => format!("2001:db8{}1", "::".repeat(1 + seeded.below(20))),
5 => format!("{}:1", "0:".repeat(6)),
6 => format!("{}192.0.2.{}", "0:".repeat(6), seeded.below(256)),
7 => format!(
"[2001:db8::{}]:{}",
seeded.below(65536),
seeded.below(65536)
),
8 => format!(
"2026:{:02}:{:02}:{:02}",
seeded.below(60),
seeded.below(60),
seeded.below(60)
),
9 => format!("fe80::1%eth{}", seeded.below(10)),
10 => format!("{}::", "f".repeat(1 + seeded.below(40))),
_ => format!("127.0.0.1:{}", seeded.below(65536)),
}
}
fn hyphen_run(seeded: &mut Seeded) -> String {
match seeded.below(9) {
0 => "aa-bb-cc-dd-ee-ff".to_string(),
1 => "aa:bb-cc:dd-ee:ff".to_string(),
2 => "-".repeat(1 + seeded.below(60)),
3 => format!("{}ff", "aa-".repeat(1 + seeded.below(30))),
4 => "2026-08-12".to_string(),
5 => format!("10.0.0.{}-10.0.0.{}", seeded.below(256), seeded.below(256)),
6 => "deadbeefcafe".to_string(),
7 => format!("{:012x}", seeded.next()),
_ => format!("-10.0.0.{}-", seeded.below(256)),
}
}
fn glued_run(seeded: &mut Seeded) -> String {
match seeded.below(9) {
0 => "std::net::Ipv6Addr".to_string(),
1 => "Vec::<u8>::new".to_string(),
2 => format!("v1.2.3.{}", seeded.below(100)),
3 => format!("x10.0.0.{}", seeded.below(256)),
4 => format!("10.0.0.{}z", seeded.below(256)),
5 => format!("client_ip:10.0.0.{}", seeded.below(256)),
6 => format!("from=10.0.0.{}", seeded.below(256)),
7 => format!("{}::f", "a".repeat(1 + seeded.below(40))),
_ => format!("Ipv4Addr::from_str(\"10.0.0.{}\")", seeded.below(256)),
}
}
fn bypass(seeded: &mut Seeded) -> String {
match seeded.below(10) {
0 => "010.1.1.1".to_string(),
1 => "0177.0.0.1".to_string(),
2 => "192.168.001.1".to_string(),
3 => format!("{}10.0.0.1", "0".repeat(1 + seeded.below(20))),
4 => "0.0.0.0".to_string(),
5 => "2130706433".to_string(),
6 => "3232235777".to_string(),
7 => format!("{}", seeded.next() % u64::from(u32::MAX)),
8 => "00.00.00.00".to_string(),
_ => "010.0.0.0/8".to_string(),
}
}
fn prefixed(seeded: &mut Seeded) -> String {
let base = match seeded.below(4) {
0 => "10.0.0.0".to_string(),
1 => "2001:db8::".to_string(),
2 => "::".to_string(),
_ => format!("192.168.{}.0", seeded.below(256)),
};
let prefix = match seeded.below(9) {
0 => "0".to_string(),
1 => "32".to_string(),
2 => "33".to_string(),
3 => "128".to_string(),
4 => "129".to_string(),
5 => "999".to_string(),
6 => "99999999999999999999".to_string(),
7 => String::new(),
_ => "abc".to_string(),
};
format!("{base}/{prefix}")
}
fn embedded(seeded: &mut Seeded) -> String {
match seeded.below(8) {
0 => format!("postgres://user:pw@10.0.0.{}:5432/app", seeded.below(256)),
1 => format!("http://[2001:db8::{}]:8080/api/v2", seeded.below(65536)),
2 => format!(
"[12/Aug/2026:10:30:{:02} +0000] \"GET /x\" 200",
seeded.below(60)
),
3 => format!("2026-08-12T10:30:{:02}.250Z", seeded.below(60)),
4 => format!("http://10.0.0.{}/24", seeded.below(256)),
5 => format!("::ffff:169.254.169.{}", seeded.below(256)),
6 => format!("elapsed 00:00:0{}.250", seeded.below(10)),
_ => format!("sha256:{:064x}", seeded.next()),
}
}
fn token(seeded: &mut Seeded) -> String {
match seeded.below(6) {
0 => colon_run(seeded),
1 => hyphen_run(seeded),
2 => glued_run(seeded),
3 => bypass(seeded),
4 => prefixed(seeded),
_ => embedded(seeded),
}
}
fn document(seeded: &mut Seeded) -> String {
let mut out = String::new();
if seeded.below(8) == 0 {
out.push('\u{feff}');
}
let newline = if seeded.below(4) == 0 { "\r\n" } else { "\n" };
let _ = write!(out, "bind_ip={HAZARD}{newline}");
for index in 0..=seeded.below(20) {
let token = token(seeded);
let _ = match seeded.below(7) {
0 => write!(out, " \"peer{index}_ip\": \"{token}\",{newline}"),
1 => write!(out, " peer{index}_addr: {token}{newline}"),
2 => write!(out, "PEER{index}_HOST={token}{newline}"),
3 => write!(out, "{index},{token},ok{newline}"),
4 => write!(out, "ts=2026-08-12 remote_addr={token} status=200{newline}"),
5 => write!(out, "# a comment holding {token}{newline}"),
_ => write!(out, "the value {token} appears in prose{newline}"),
};
}
if seeded.below(2) == 0 {
while out.ends_with('\n') || out.ends_with('\r') {
out.pop();
}
}
out
}
const FIELDS: [&str; 9] = [
"kind",
"text",
"line",
"column",
"key",
"normalized",
"class",
"cidr",
"refused",
];
fn check(case: &str, document: &str, raw: &str) {
let response: serde_json::Value = serde_json::from_str(raw)
.unwrap_or_else(|error| panic!("{case}: the answer is not JSON ({error}) — {raw}"));
assert!(
response.get("error").is_none(),
"{case}: the tool call failed — {raw}\ndocument:\n{document}"
);
let envelope = &response["result"]["structuredContent"];
assert_eq!(
envelope["meta"]["tool"], "extract_ips",
"{case}: not the tool's envelope — {raw}"
);
assert_eq!(envelope["ok"], true, "{case}: the scan did not run — {raw}");
assert!(
envelope["meta"]["truncated"].is_boolean(),
"{case}: truncated is not a boolean — {raw}"
);
let addresses = envelope["data"]["addresses"]
.as_array()
.unwrap_or_else(|| panic!("{case}: addresses is not a list — {raw}"));
for found in addresses {
for field in FIELDS {
assert!(
found.get(field).is_some(),
"{case}: {field} is missing from {found}"
);
}
let text = found["text"]
.as_str()
.unwrap_or_else(|| panic!("{case}: a finding has no text — {found}"));
assert!(!text.is_empty(), "{case}: an empty finding — {found}");
assert!(
document.contains(text),
"{case}: {text:?} was reported and is not in the document\n{document}"
);
assert!(
found["line"].as_u64().is_some_and(|line| line >= 1),
"{case}: line is not 1-based — {found}"
);
assert!(
found["column"].as_u64().is_some_and(|column| column >= 1),
"{case}: column is not 1-based — {found}"
);
if !found["refused"].is_null() {
assert!(
found["normalized"].is_null() && found["class"].is_null(),
"{case}: a refusal carries a reading — {found}"
);
assert!(
found["refused"]["reason"].is_string(),
"{case}: a refusal with no reason — {found}"
);
}
}
let refused = addresses
.iter()
.filter(|found| !found["refused"].is_null())
.count();
assert_eq!(
envelope["data"]["refused"].as_u64(),
u64::try_from(refused).ok(),
"{case}: the refusal count disagrees with the findings — {raw}"
);
forbidden_readings(case, raw);
}
fn forbidden_readings(case: &str, stream: &str) {
assert!(
!stream.contains(OCTAL_READING),
"{case}: the octal reading {OCTAL_READING} was resolved — {stream}"
);
assert!(
!stream.replace(HAZARD, "").contains(DECIMAL_READING),
"{case}: the decimal reading {DECIMAL_READING} was resolved — {stream}"
);
}
#[test]
fn the_scanner_survives_what_grows_out_of_its_own_splitting_rules() {
let seed = seed();
let budget = budget();
eprintln!("fuzz: seed {seed:#x}, budget {budget:?}");
let mut server = Server::start();
let mut seeded = Seeded(seed);
let mut cases = 0usize;
let deadline = Instant::now() + budget;
while Instant::now() < deadline {
let body = document(&mut seeded);
let format = *seeded.pick(&FORMATS);
let case = format!("seed {seed:#x} case {cases} format {format:?}");
let mut arguments = serde_json::json!({
"content": body,
"maxResults": 5000,
});
if let Some(format) = format {
arguments["format"] = serde_json::json!(format);
}
let raw = server.extract(&case, &arguments);
check(&case, &body, &raw);
cases += 1;
}
eprintln!("fuzz: {cases} documents, seed {seed:#x}");
assert!(cases > 0, "the fuzzer ran no documents");
}
#[test]
fn pathological_runs_terminate() {
let mut server = Server::start();
let cases: Vec<(&str, String)> = vec![
("ten thousand colons", ":".repeat(10_000)),
("five thousand double colons", "::".repeat(5_000)),
("a hex run of a hundred thousand", "a".repeat(100_000)),
("an alternating separator run", "a:1-2.".repeat(20_000)),
(
"forty thousand IPv6 groups",
(0..40_000)
.map(|index| format!("{:04x}", index % 65536))
.collect::<Vec<String>>()
.join(":"),
),
(
"ten thousand addresses on one line",
(0..10_000)
.map(|index| format!("peer{index}_ip=10.0.0.{}", index % 256))
.collect::<Vec<String>>()
.join(" "),
),
(
"ten thousand hazards on one line",
format!("{HAZARD} ").repeat(10_000),
),
(
"a prefix of twenty thousand digits",
format!("10.0.0.0/{}", "9".repeat(20_000)),
),
(
"one address glued to a hundred thousand identifier bytes",
format!("{}::f10.0.0.1", "n".repeat(100_000)),
),
];
for (name, body) in cases {
let started = Instant::now();
let raw = server.extract(
name,
&serde_json::json!({ "content": body, "maxResults": 5000 }),
);
check(name, &body, &raw);
eprintln!("fuzz: {name} answered in {:?}", started.elapsed());
}
}
#[test]
fn an_octal_hazard_survives_the_fuzzer_on_both_streams() {
const DOCUMENTS: usize = 200;
let seed = seed();
let mut seeded = Seeded(seed);
let mut corpus = String::new();
for _ in 0..DOCUMENTS {
corpus.push_str(&document(&mut seeded));
corpus.push('\n');
}
let mut child = Command::new(BINARY)
.args(["--stdin", "--format", "log"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("the binary runs");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(corpus.as_bytes())
.expect("writes");
let output = child.wait_with_output().expect("the child finishes");
assert!(
matches!(output.status.code(), Some(0 | 1)),
"the run did not finish cleanly: {:?}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
forbidden_readings("stdout", &stdout);
forbidden_readings("stderr", &stderr);
assert!(
stderr.contains("refused"),
"the corpus produced no refusal at all: {stderr}"
);
eprintln!("fuzz: {DOCUMENTS} documents through the CLI, seed {seed:#x}");
}