use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
use std::str::FromStr;
#[derive(Debug)]
pub enum Error {
IoError(std::io::Error),
ParseError,
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Self::IoError(e)
}
}
impl std::error::Error for Error { }
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::IoError(e) => f.write_fmt(format_args!("{}", e)),
Error::ParseError => f.write_str("parse error"),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Default, Debug)]
pub struct AFLStat {
pub start_time: u64,
pub last_update: u64,
pub fuzzer_pid: u32,
pub cycles_done: i32,
pub execs_done: i64,
pub execs_per_sec: f64,
pub paths_total: i64,
pub paths_favored: i64,
pub paths_found: i64,
pub paths_imported: i64,
pub max_depth: i32,
pub cur_path: i64,
pub pending_favs: i64,
pub pending_total: i64,
pub variable_paths: i64,
pub stability: f64,
pub bitmap_cvg: f64,
pub unique_crashes: i32,
pub unique_hangs: i32,
pub last_path: u64,
pub last_crash: u64,
pub last_hang: u64,
pub execs_since_crash: i64,
pub exec_timeout: i32,
pub slowest_exec_ms: i32,
pub peak_rss_mb: i32,
pub afl_banner: String,
pub afl_version: String,
pub target_mode: String,
pub command_line: String,
}
impl AFLStat {
pub fn parse(text: &str) -> Result<Self> {
let mut d = HashMap::<String, String>::new();
for ln in text.lines() {
if ln.trim().is_empty() {
continue;
}
let ln_data: Vec<&str> = ln.split(':').map(|s| s.trim()).collect();
if ln_data.len() != 2 {
return Err(Error::ParseError);
}
d.insert(ln_data[0].to_owned(), ln_data[1].to_owned());
}
Self::parse_dict(&d)
}
fn parse_dict(d: &HashMap<String, String>) -> Result<Self> {
let mut stat = Self::default();
if let Some(value) = d.get("start_time") {
stat.start_time = Self::parse_value(value)?;
}
if let Some(value) = d.get("last_update") {
stat.last_update = Self::parse_value(value)?;
}
if let Some(value) = d.get("fuzzer_pid") {
stat.fuzzer_pid = Self::parse_value(value)?;
}
if let Some(value) = d.get("cycles_done") {
stat.cycles_done = Self::parse_value(value)?;
}
if let Some(value) = d.get("execs_done") {
stat.execs_done = Self::parse_value(value)?;
}
if let Some(value) = d.get("execs_per_sec") {
stat.execs_per_sec = Self::parse_value(value)?;
}
if let Some(value) = d.get("paths_total") {
stat.paths_total = Self::parse_value(value)?;
}
if let Some(value) = d.get("paths_favored") {
stat.paths_favored = Self::parse_value(value)?;
}
if let Some(value) = d.get("paths_found") {
stat.paths_found = Self::parse_value(value)?;
}
if let Some(value) = d.get("paths_imported") {
stat.paths_imported = Self::parse_value(value)?;
}
if let Some(value) = d.get("max_depth") {
stat.max_depth = Self::parse_value(value)?;
}
if let Some(value) = d.get("cur_path") {
stat.cur_path = Self::parse_value(value)?;
}
if let Some(value) = d.get("pending_favs") {
stat.pending_favs = Self::parse_value(value)?;
}
if let Some(value) = d.get("pending_total") {
stat.pending_total = Self::parse_value(value)?;
}
if let Some(value) = d.get("variable_paths") {
stat.variable_paths = Self::parse_value(value)?;
}
if let Some(value) = d.get("stability") {
stat.stability = Self::parse_percentage(value)?;
}
if let Some(value) = d.get("bitmap_cvg") {
stat.bitmap_cvg = Self::parse_percentage(value)?;
}
if let Some(value) = d.get("unique_crashes") {
stat.unique_crashes = Self::parse_value(value)?;
}
if let Some(value) = d.get("unique_hangs") {
stat.unique_hangs = Self::parse_value(value)?;
}
if let Some(value) = d.get("last_path") {
stat.last_path = Self::parse_value(value)?;
}
if let Some(value) = d.get("last_crash") {
stat.last_crash = Self::parse_value(value)?;
}
if let Some(value) = d.get("last_hang") {
stat.last_hang = Self::parse_value(value)?;
}
if let Some(value) = d.get("execs_since_crash") {
stat.execs_since_crash = Self::parse_value(value)?;
}
if let Some(value) = d.get("exec_timeout") {
stat.exec_timeout = Self::parse_value(value)?;
}
if let Some(value) = d.get("slowest_exec_ms") {
stat.slowest_exec_ms = Self::parse_value(value)?;
}
if let Some(value) = d.get("peak_rss_mb") {
stat.peak_rss_mb = Self::parse_value(value)?;
}
if let Some(value) = d.get("afl_banner") {
stat.afl_banner = value.clone();
}
if let Some(value) = d.get("afl_version") {
stat.afl_version = value.clone();
}
if let Some(value) = d.get("target_mode") {
stat.target_mode = value.clone();
}
if let Some(value) = d.get("command_line") {
stat.command_line = value.clone();
}
Ok(stat)
}
fn parse_value<T>(text: &str) -> Result<T>
where T: FromStr {
T::from_str(text)
.map_err(|_| Error::ParseError)
}
fn parse_percentage(text: &str) -> Result<f64> {
if text.is_empty() {
return Err(Error::ParseError);
}
if !text.ends_with('%') {
return Err(Error::ParseError);
}
let text = text.trim_end_matches('%');
f64::from_str(text)
.map(|x| x / 100.0)
.map_err(|_| Error::ParseError)
}
pub fn load(stat_file: &Path) -> Result<Self> {
let text = {
let file = File::open(stat_file)?;
let mut reader = BufReader::new(file);
let mut buf = String::new();
reader.read_to_string(&mut buf)?;
buf
};
Self::parse(&text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_value_ok() {
assert_eq!(10, AFLStat::parse_value::<i32>("10").unwrap());
}
#[test]
fn parse_value_bad() {
assert!(AFLStat::parse_value::<i32>("10x").is_err());
}
#[test]
fn parse_percentage_ok() {
assert_eq!(0.25, AFLStat::parse_percentage("25%").unwrap());
assert_eq!(1.25, AFLStat::parse_percentage("125%").unwrap());
}
#[test]
fn parse_percentage_bad() {
assert!(AFLStat::parse_percentage("%").is_err());
assert!(AFLStat::parse_percentage("0.25").is_err());
}
#[test]
fn parse_afl_stat_from_dict_ok() {
let mut d = HashMap::<String, String>::new();
d.insert(String::from("start_time"), String::from("1587396831"));
d.insert(String::from("last_update"), String::from("1587488608"));
d.insert(String::from("fuzzer_pid"), String::from("23661"));
d.insert(String::from("cycles_done"), String::from("1"));
d.insert(String::from("execs_done"), String::from("354214"));
d.insert(String::from("execs_per_sec"), String::from("3.86"));
d.insert(String::from("paths_total"), String::from("6204"));
d.insert(String::from("paths_favored"), String::from("631"));
d.insert(String::from("paths_found"), String::from("451"));
d.insert(String::from("paths_imported"), String::from("4642"));
d.insert(String::from("max_depth"), String::from("3"));
d.insert(String::from("cur_path"), String::from("2580"));
d.insert(String::from("pending_favs"), String::from("513"));
d.insert(String::from("pending_total"), String::from("5983"));
d.insert(String::from("variable_paths"), String::from("6198"));
d.insert(String::from("stability"), String::from("63.45%"));
d.insert(String::from("bitmap_cvg"), String::from("91.79%"));
d.insert(String::from("unique_crashes"), String::from("43"));
d.insert(String::from("unique_hangs"), String::from("245"));
d.insert(String::from("last_path"), String::from("1587488588"));
d.insert(String::from("last_crash"), String::from("1587487142"));
d.insert(String::from("last_hang"), String::from("1587486568"));
d.insert(String::from("execs_since_crash"), String::from("354214"));
d.insert(String::from("exec_timeout"), String::from("1000"));
d.insert(String::from("slowest_exec_ms"), String::from("1250"));
d.insert(String::from("peak_rss_mb"), String::from("0"));
d.insert(String::from("afl_banner"), String::from("fuzzer0"));
d.insert(String::from("afl_version"), String::from("++2.62c"));
d.insert(String::from("target_mode"), String::from("default"));
d.insert(String::from("command_line"), String::from("afl-fuzz arg1 arg2"));
let stat = AFLStat::parse_dict(&d).unwrap();
assert_correct_stat(&stat);
}
#[test]
fn parse_afl_stat_from_dict_bad() {
let mut d = HashMap::<String, String>::new();
d.insert(String::from("start_time"), String::from("1587396831"));
d.insert(String::from("last_update"), String::from("1587488608"));
d.insert(String::from("fuzzer_pid"), String::from("23661"));
d.insert(String::from("cycles_done"), String::from("1"));
d.insert(String::from("execs_done"), String::from("354214"));
d.insert(String::from("execs_per_sec"), String::from("3.86"));
d.insert(String::from("paths_total"), String::from("6204"));
d.insert(String::from("paths_favored"), String::from("631"));
d.insert(String::from("paths_found"), String::from("451"));
d.insert(String::from("paths_imported"), String::from("4642"));
d.insert(String::from("max_depth"), String::from("3"));
d.insert(String::from("cur_path"), String::from("2580"));
d.insert(String::from("pending_favs"), String::from("513"));
d.insert(String::from("pending_total"), String::from("5983"));
d.insert(String::from("variable_paths"), String::from("6198"));
d.insert(String::from("stability"), String::from("63.45"));
d.insert(String::from("bitmap_cvg"), String::from("91.79%"));
d.insert(String::from("unique_crashes"), String::from("43"));
d.insert(String::from("unique_hangs"), String::from("245"));
d.insert(String::from("last_path"), String::from("1587488588"));
d.insert(String::from("last_crash"), String::from("1587487142"));
d.insert(String::from("last_hang"), String::from("1587486568"));
d.insert(String::from("execs_since_crash"), String::from("354214"));
d.insert(String::from("exec_timeout"), String::from("1000"));
d.insert(String::from("slowest_exec_ms"), String::from("1250"));
d.insert(String::from("peak_rss_mb"), String::from("0"));
d.insert(String::from("afl_banner"), String::from("fuzzer0"));
d.insert(String::from("afl_version"), String::from("++2.62c"));
d.insert(String::from("target_mode"), String::from("default"));
d.insert(String::from("command_line"), String::from("afl-fuzz arg1 arg2"));
assert!(AFLStat::parse_dict(&d).is_err());
}
#[test]
fn parse_afl_stat_ok() {
let raw_stat = r#"
start_time : 1587396831
last_update : 1587488608
fuzzer_pid : 23661
cycles_done : 1
execs_done : 354214
execs_per_sec : 3.86
paths_total : 6204
paths_favored : 631
paths_found : 451
paths_imported : 4642
max_depth : 3
cur_path : 2580
pending_favs : 513
pending_total : 5983
variable_paths : 6198
stability : 63.45%
bitmap_cvg : 91.79%
unique_crashes : 43
unique_hangs : 245
last_path : 1587488588
last_crash : 1587487142
last_hang : 1587486568
execs_since_crash : 354214
exec_timeout : 1000
slowest_exec_ms : 1250
peak_rss_mb : 0
afl_banner : fuzzer0
afl_version : ++2.62c
target_mode : default
command_line : afl-fuzz arg1 arg2
"#;
let stat = AFLStat::parse(raw_stat).unwrap();
assert_correct_stat(&stat);
}
fn assert_correct_stat(stat: &AFLStat) {
assert_eq!(1587396831, stat.start_time);
assert_eq!(1587488608, stat.last_update);
assert_eq!(23661, stat.fuzzer_pid);
assert_eq!(1, stat.cycles_done);
assert_eq!(354214, stat.execs_done);
assert!((3.86 - stat.execs_per_sec).abs() < 1e-8);
assert_eq!(6204, stat.paths_total);
assert_eq!(631, stat.paths_favored);
assert_eq!(451, stat.paths_found);
assert_eq!(4642, stat.paths_imported);
assert_eq!(3, stat.max_depth);
assert_eq!(2580, stat.cur_path);
assert_eq!(513, stat.pending_favs);
assert_eq!(5983, stat.pending_total);
assert_eq!(6198, stat.variable_paths);
assert!((0.6345 - stat.stability).abs() < 1e-8);
assert!((0.9179 - stat.bitmap_cvg).abs() < 1e-8);
assert_eq!(43, stat.unique_crashes);
assert_eq!(245, stat.unique_hangs);
assert_eq!(1587488588, stat.last_path);
assert_eq!(1587487142, stat.last_crash);
assert_eq!(1587486568, stat.last_hang);
assert_eq!(354214, stat.execs_since_crash);
assert_eq!(1000, stat.exec_timeout);
assert_eq!(1250, stat.slowest_exec_ms);
assert_eq!(0, stat.peak_rss_mb);
assert_eq!("fuzzer0", stat.afl_banner);
assert_eq!("++2.62c", stat.afl_version);
assert_eq!("default", stat.target_mode);
assert_eq!("afl-fuzz arg1 arg2", stat.command_line);
}
#[test]
fn parse_afl_stat_bad() {
let raw_stat = r#"
start_time : 1587396831
last_update : 1587488608
fuzzer_pid : 23661
cycles_done : 1
execs_done : 354214
execs_per_sec : 3.86
paths_total : 6204
paths_favored : 631
paths_found : 451
paths_imported : 4642
max_depth : 3
cur_path : 2580
pending_favs : 513
pending_total : 5983
variable_paths : 6198
stability : 63.45
bitmap_cvg : 91.79%
unique_crashes : 43
unique_hangs : 245
last_path : 1587488588
last_crash : 1587487142
last_hang : 1587486568
execs_since_crash : 354214
exec_timeout : 1000
slowest_exec_ms : 1250
peak_rss_mb : 0
afl_banner : fuzzer0
afl_version : ++2.62c
target_mode : default
command_line : afl-fuzz arg1 arg2
"#;
assert!(AFLStat::parse(raw_stat).is_err());
}
}