1use std::collections::HashMap;
11use std::fmt::{Display, Formatter};
12use std::fs::File;
13use std::io::{BufReader, Read};
14use std::path::Path;
15use std::str::FromStr;
16
17#[derive(Debug)]
21pub enum Error {
22 IoError(std::io::Error),
24
25 ParseError,
27}
28
29impl From<std::io::Error> for Error {
30 fn from(e: std::io::Error) -> Self {
31 Self::IoError(e)
32 }
33}
34
35impl std::error::Error for Error { }
36
37impl Display for Error {
38 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
39 match self {
40 Error::IoError(e) => f.write_fmt(format_args!("{}", e)),
41 Error::ParseError => f.write_str("parse error"),
42 }
43 }
44}
45
46pub type Result<T> = std::result::Result<T, Error>;
48
49#[derive(Clone, Default, Debug)]
53pub struct AFLStat {
54 pub start_time: u64,
55 pub last_update: u64,
56 pub fuzzer_pid: u32,
57 pub cycles_done: i32,
58 pub execs_done: i64,
59 pub execs_per_sec: f64,
60 pub paths_total: i64,
61 pub paths_favored: i64,
62 pub paths_found: i64,
63 pub paths_imported: i64,
64 pub max_depth: i32,
65 pub cur_path: i64,
66 pub pending_favs: i64,
67 pub pending_total: i64,
68 pub variable_paths: i64,
69 pub stability: f64,
70 pub bitmap_cvg: f64,
71 pub unique_crashes: i32,
72 pub unique_hangs: i32,
73 pub last_path: u64,
74 pub last_crash: u64,
75 pub last_hang: u64,
76 pub execs_since_crash: i64,
77 pub exec_timeout: i32,
78 pub slowest_exec_ms: i32,
79 pub peak_rss_mb: i32,
80 pub afl_banner: String,
81 pub afl_version: String,
82 pub target_mode: String,
83 pub command_line: String,
84}
85
86impl AFLStat {
87 pub fn parse(text: &str) -> Result<Self> {
89 let mut d = HashMap::<String, String>::new();
90 for ln in text.lines() {
91 if ln.trim().is_empty() {
92 continue;
93 }
94
95 let ln_data: Vec<&str> = ln.split(':').map(|s| s.trim()).collect();
96 if ln_data.len() != 2 {
97 return Err(Error::ParseError);
98 }
99 d.insert(ln_data[0].to_owned(), ln_data[1].to_owned());
100 }
101
102 Self::parse_dict(&d)
103 }
104
105 fn parse_dict(d: &HashMap<String, String>) -> Result<Self> {
106 let mut stat = Self::default();
107
108 if let Some(value) = d.get("start_time") {
109 stat.start_time = Self::parse_value(value)?;
110 }
111
112 if let Some(value) = d.get("last_update") {
113 stat.last_update = Self::parse_value(value)?;
114 }
115
116 if let Some(value) = d.get("fuzzer_pid") {
117 stat.fuzzer_pid = Self::parse_value(value)?;
118 }
119
120 if let Some(value) = d.get("cycles_done") {
121 stat.cycles_done = Self::parse_value(value)?;
122 }
123
124 if let Some(value) = d.get("execs_done") {
125 stat.execs_done = Self::parse_value(value)?;
126 }
127
128 if let Some(value) = d.get("execs_per_sec") {
129 stat.execs_per_sec = Self::parse_value(value)?;
130 }
131
132 if let Some(value) = d.get("paths_total") {
133 stat.paths_total = Self::parse_value(value)?;
134 }
135
136 if let Some(value) = d.get("paths_favored") {
137 stat.paths_favored = Self::parse_value(value)?;
138 }
139
140 if let Some(value) = d.get("paths_found") {
141 stat.paths_found = Self::parse_value(value)?;
142 }
143
144 if let Some(value) = d.get("paths_imported") {
145 stat.paths_imported = Self::parse_value(value)?;
146 }
147
148 if let Some(value) = d.get("max_depth") {
149 stat.max_depth = Self::parse_value(value)?;
150 }
151
152 if let Some(value) = d.get("cur_path") {
153 stat.cur_path = Self::parse_value(value)?;
154 }
155
156 if let Some(value) = d.get("pending_favs") {
157 stat.pending_favs = Self::parse_value(value)?;
158 }
159
160 if let Some(value) = d.get("pending_total") {
161 stat.pending_total = Self::parse_value(value)?;
162 }
163
164 if let Some(value) = d.get("variable_paths") {
165 stat.variable_paths = Self::parse_value(value)?;
166 }
167
168 if let Some(value) = d.get("stability") {
169 stat.stability = Self::parse_percentage(value)?;
170 }
171
172 if let Some(value) = d.get("bitmap_cvg") {
173 stat.bitmap_cvg = Self::parse_percentage(value)?;
174 }
175
176 if let Some(value) = d.get("unique_crashes") {
177 stat.unique_crashes = Self::parse_value(value)?;
178 }
179
180 if let Some(value) = d.get("unique_hangs") {
181 stat.unique_hangs = Self::parse_value(value)?;
182 }
183
184 if let Some(value) = d.get("last_path") {
185 stat.last_path = Self::parse_value(value)?;
186 }
187
188 if let Some(value) = d.get("last_crash") {
189 stat.last_crash = Self::parse_value(value)?;
190 }
191
192 if let Some(value) = d.get("last_hang") {
193 stat.last_hang = Self::parse_value(value)?;
194 }
195
196 if let Some(value) = d.get("execs_since_crash") {
197 stat.execs_since_crash = Self::parse_value(value)?;
198 }
199
200 if let Some(value) = d.get("exec_timeout") {
201 stat.exec_timeout = Self::parse_value(value)?;
202 }
203
204 if let Some(value) = d.get("slowest_exec_ms") {
205 stat.slowest_exec_ms = Self::parse_value(value)?;
206 }
207
208 if let Some(value) = d.get("peak_rss_mb") {
209 stat.peak_rss_mb = Self::parse_value(value)?;
210 }
211
212 if let Some(value) = d.get("afl_banner") {
213 stat.afl_banner = value.clone();
214 }
215
216 if let Some(value) = d.get("afl_version") {
217 stat.afl_version = value.clone();
218 }
219
220 if let Some(value) = d.get("target_mode") {
221 stat.target_mode = value.clone();
222 }
223
224 if let Some(value) = d.get("command_line") {
225 stat.command_line = value.clone();
226 }
227
228 Ok(stat)
229 }
230
231 fn parse_value<T>(text: &str) -> Result<T>
232 where T: FromStr {
233 T::from_str(text)
234 .map_err(|_| Error::ParseError)
235 }
236
237 fn parse_percentage(text: &str) -> Result<f64> {
238 if text.is_empty() {
239 return Err(Error::ParseError);
240 }
241
242 if !text.ends_with('%') {
243 return Err(Error::ParseError);
244 }
245
246 let text = text.trim_end_matches('%');
247 f64::from_str(text)
248 .map(|x| x / 100.0)
249 .map_err(|_| Error::ParseError)
250 }
251
252 pub fn load(stat_file: &Path) -> Result<Self> {
260 let text = {
261 let file = File::open(stat_file)?;
262 let mut reader = BufReader::new(file);
263 let mut buf = String::new();
264 reader.read_to_string(&mut buf)?;
265 buf
266 };
267 Self::parse(&text)
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn parse_value_ok() {
277 assert_eq!(10, AFLStat::parse_value::<i32>("10").unwrap());
278 }
279
280 #[test]
281 fn parse_value_bad() {
282 assert!(AFLStat::parse_value::<i32>("10x").is_err());
283 }
284
285 #[test]
286 fn parse_percentage_ok() {
287 assert_eq!(0.25, AFLStat::parse_percentage("25%").unwrap());
288 assert_eq!(1.25, AFLStat::parse_percentage("125%").unwrap());
289 }
290
291 #[test]
292 fn parse_percentage_bad() {
293 assert!(AFLStat::parse_percentage("%").is_err());
294 assert!(AFLStat::parse_percentage("0.25").is_err());
295 }
296
297 #[test]
298 fn parse_afl_stat_from_dict_ok() {
299 let mut d = HashMap::<String, String>::new();
300 d.insert(String::from("start_time"), String::from("1587396831"));
301 d.insert(String::from("last_update"), String::from("1587488608"));
302 d.insert(String::from("fuzzer_pid"), String::from("23661"));
303 d.insert(String::from("cycles_done"), String::from("1"));
304 d.insert(String::from("execs_done"), String::from("354214"));
305 d.insert(String::from("execs_per_sec"), String::from("3.86"));
306 d.insert(String::from("paths_total"), String::from("6204"));
307 d.insert(String::from("paths_favored"), String::from("631"));
308 d.insert(String::from("paths_found"), String::from("451"));
309 d.insert(String::from("paths_imported"), String::from("4642"));
310 d.insert(String::from("max_depth"), String::from("3"));
311 d.insert(String::from("cur_path"), String::from("2580"));
312 d.insert(String::from("pending_favs"), String::from("513"));
313 d.insert(String::from("pending_total"), String::from("5983"));
314 d.insert(String::from("variable_paths"), String::from("6198"));
315 d.insert(String::from("stability"), String::from("63.45%"));
316 d.insert(String::from("bitmap_cvg"), String::from("91.79%"));
317 d.insert(String::from("unique_crashes"), String::from("43"));
318 d.insert(String::from("unique_hangs"), String::from("245"));
319 d.insert(String::from("last_path"), String::from("1587488588"));
320 d.insert(String::from("last_crash"), String::from("1587487142"));
321 d.insert(String::from("last_hang"), String::from("1587486568"));
322 d.insert(String::from("execs_since_crash"), String::from("354214"));
323 d.insert(String::from("exec_timeout"), String::from("1000"));
324 d.insert(String::from("slowest_exec_ms"), String::from("1250"));
325 d.insert(String::from("peak_rss_mb"), String::from("0"));
326 d.insert(String::from("afl_banner"), String::from("fuzzer0"));
327 d.insert(String::from("afl_version"), String::from("++2.62c"));
328 d.insert(String::from("target_mode"), String::from("default"));
329 d.insert(String::from("command_line"), String::from("afl-fuzz arg1 arg2"));
330 let stat = AFLStat::parse_dict(&d).unwrap();
331 assert_correct_stat(&stat);
332 }
333
334 #[test]
335 fn parse_afl_stat_from_dict_bad() {
336 let mut d = HashMap::<String, String>::new();
339 d.insert(String::from("start_time"), String::from("1587396831"));
340 d.insert(String::from("last_update"), String::from("1587488608"));
341 d.insert(String::from("fuzzer_pid"), String::from("23661"));
342 d.insert(String::from("cycles_done"), String::from("1"));
343 d.insert(String::from("execs_done"), String::from("354214"));
344 d.insert(String::from("execs_per_sec"), String::from("3.86"));
345 d.insert(String::from("paths_total"), String::from("6204"));
346 d.insert(String::from("paths_favored"), String::from("631"));
347 d.insert(String::from("paths_found"), String::from("451"));
348 d.insert(String::from("paths_imported"), String::from("4642"));
349 d.insert(String::from("max_depth"), String::from("3"));
350 d.insert(String::from("cur_path"), String::from("2580"));
351 d.insert(String::from("pending_favs"), String::from("513"));
352 d.insert(String::from("pending_total"), String::from("5983"));
353 d.insert(String::from("variable_paths"), String::from("6198"));
354 d.insert(String::from("stability"), String::from("63.45"));
355 d.insert(String::from("bitmap_cvg"), String::from("91.79%"));
356 d.insert(String::from("unique_crashes"), String::from("43"));
357 d.insert(String::from("unique_hangs"), String::from("245"));
358 d.insert(String::from("last_path"), String::from("1587488588"));
359 d.insert(String::from("last_crash"), String::from("1587487142"));
360 d.insert(String::from("last_hang"), String::from("1587486568"));
361 d.insert(String::from("execs_since_crash"), String::from("354214"));
362 d.insert(String::from("exec_timeout"), String::from("1000"));
363 d.insert(String::from("slowest_exec_ms"), String::from("1250"));
364 d.insert(String::from("peak_rss_mb"), String::from("0"));
365 d.insert(String::from("afl_banner"), String::from("fuzzer0"));
366 d.insert(String::from("afl_version"), String::from("++2.62c"));
367 d.insert(String::from("target_mode"), String::from("default"));
368 d.insert(String::from("command_line"), String::from("afl-fuzz arg1 arg2"));
369 assert!(AFLStat::parse_dict(&d).is_err());
370 }
371
372 #[test]
373 fn parse_afl_stat_ok() {
374 let raw_stat = r#"
375 start_time : 1587396831
376 last_update : 1587488608
377 fuzzer_pid : 23661
378 cycles_done : 1
379 execs_done : 354214
380 execs_per_sec : 3.86
381 paths_total : 6204
382 paths_favored : 631
383 paths_found : 451
384 paths_imported : 4642
385 max_depth : 3
386 cur_path : 2580
387 pending_favs : 513
388 pending_total : 5983
389 variable_paths : 6198
390 stability : 63.45%
391 bitmap_cvg : 91.79%
392 unique_crashes : 43
393 unique_hangs : 245
394 last_path : 1587488588
395 last_crash : 1587487142
396 last_hang : 1587486568
397 execs_since_crash : 354214
398 exec_timeout : 1000
399 slowest_exec_ms : 1250
400 peak_rss_mb : 0
401 afl_banner : fuzzer0
402 afl_version : ++2.62c
403 target_mode : default
404 command_line : afl-fuzz arg1 arg2
405 "#;
406 let stat = AFLStat::parse(raw_stat).unwrap();
407 assert_correct_stat(&stat);
408 }
409
410 fn assert_correct_stat(stat: &AFLStat) {
411 assert_eq!(1587396831, stat.start_time);
412 assert_eq!(1587488608, stat.last_update);
413 assert_eq!(23661, stat.fuzzer_pid);
414 assert_eq!(1, stat.cycles_done);
415 assert_eq!(354214, stat.execs_done);
416 assert!((3.86 - stat.execs_per_sec).abs() < 1e-8);
417 assert_eq!(6204, stat.paths_total);
418 assert_eq!(631, stat.paths_favored);
419 assert_eq!(451, stat.paths_found);
420 assert_eq!(4642, stat.paths_imported);
421 assert_eq!(3, stat.max_depth);
422 assert_eq!(2580, stat.cur_path);
423 assert_eq!(513, stat.pending_favs);
424 assert_eq!(5983, stat.pending_total);
425 assert_eq!(6198, stat.variable_paths);
426 assert!((0.6345 - stat.stability).abs() < 1e-8);
427 assert!((0.9179 - stat.bitmap_cvg).abs() < 1e-8);
428 assert_eq!(43, stat.unique_crashes);
429 assert_eq!(245, stat.unique_hangs);
430 assert_eq!(1587488588, stat.last_path);
431 assert_eq!(1587487142, stat.last_crash);
432 assert_eq!(1587486568, stat.last_hang);
433 assert_eq!(354214, stat.execs_since_crash);
434 assert_eq!(1000, stat.exec_timeout);
435 assert_eq!(1250, stat.slowest_exec_ms);
436 assert_eq!(0, stat.peak_rss_mb);
437 assert_eq!("fuzzer0", stat.afl_banner);
438 assert_eq!("++2.62c", stat.afl_version);
439 assert_eq!("default", stat.target_mode);
440 assert_eq!("afl-fuzz arg1 arg2", stat.command_line);
441 }
442
443 #[test]
444 fn parse_afl_stat_bad() {
445 let raw_stat = r#"
448 start_time : 1587396831
449 last_update : 1587488608
450 fuzzer_pid : 23661
451 cycles_done : 1
452 execs_done : 354214
453 execs_per_sec : 3.86
454 paths_total : 6204
455 paths_favored : 631
456 paths_found : 451
457 paths_imported : 4642
458 max_depth : 3
459 cur_path : 2580
460 pending_favs : 513
461 pending_total : 5983
462 variable_paths : 6198
463 stability : 63.45
464 bitmap_cvg : 91.79%
465 unique_crashes : 43
466 unique_hangs : 245
467 last_path : 1587488588
468 last_crash : 1587487142
469 last_hang : 1587486568
470 execs_since_crash : 354214
471 exec_timeout : 1000
472 slowest_exec_ms : 1250
473 peak_rss_mb : 0
474 afl_banner : fuzzer0
475 afl_version : ++2.62c
476 target_mode : default
477 command_line : afl-fuzz arg1 arg2
478 "#;
479 assert!(AFLStat::parse(raw_stat).is_err());
480 }
481}