1- #![ deny( clippy:: disallowed_macros, clippy:: expect_used, clippy:: unwrap_used) ]
2-
31use anyhow:: { Context , Result , bail, ensure} ;
42use clap:: Parser ;
53use std:: ffi:: OsStr ;
@@ -9,6 +7,7 @@ use std::process::{Command, ExitStatus, Stdio};
97use super :: common;
108
119const AFL_SRC_PATH : & str = "AFLplusplus" ;
10+ const AFLPLUSPLUS_URL : & str = "https://github.com/AFLplusplus/AFLplusplus" ;
1211
1312// https://github.com/rust-fuzz/afl.rs/issues/148
1413#[ cfg( target_os = "macos" ) ]
@@ -26,60 +25,153 @@ pub struct Args {
2625 #[ clap( long, help = "Build AFL++ for the default toolchain" ) ]
2726 pub build : bool ,
2827
29- #[ clap( long, help = "Rebuild AFL++ if it was already built" ) ]
28+ #[ clap(
29+ long,
30+ help = "Rebuild AFL++ if it was already built. Note: AFL++ will be built without plugins \
31+ if `--plugins` is not passed."
32+ ) ]
3033 pub force : bool ,
3134
3235 #[ clap( long, help = "Enable building of LLVM plugins" ) ]
3336 pub plugins : bool ,
3437
38+ #[ clap(
39+ long,
40+ help = "Update to <TAG> instead of the latest stable version" ,
41+ requires = "update"
42+ ) ]
43+ pub tag : Option < String > ,
44+
45+ #[ clap(
46+ long,
47+ help = "Update AFL++ to the latest stable version (preserving plugins, if applicable)"
48+ ) ]
49+ pub update : bool ,
50+
3551 #[ clap( long, help = "Show build output" ) ]
3652 pub verbose : bool ,
3753}
3854
3955pub fn config ( args : & Args ) -> Result < ( ) > {
4056 let archive_file_path = common:: archive_file_path ( ) ?;
41- if !args. force && archive_file_path. exists ( ) && args. plugins == common:: plugins_installed ( ) ? {
57+
58+ if !args. force
59+ && !args. update
60+ && archive_file_path. exists ( )
61+ && args. plugins == common:: plugins_installed ( ) ?
62+ {
4263 let version = common:: afl_rustc_version ( ) ?;
4364 bail ! (
4465 "AFL LLVM runtime was already built for Rust {version}; run `cargo afl config --build \
4566 --force` to rebuild it."
4667 ) ;
4768 }
4869
49- let afl_src_dir = Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) ) . join ( AFL_SRC_PATH ) ;
50- let afl_src_dir_str = & afl_src_dir. to_string_lossy ( ) ;
51-
52- let tempdir = tempfile:: tempdir ( ) . with_context ( || "could not create temporary directory" ) ?;
70+ // smoelius: If updating and AFL++ was built with plugins before, build with plugins again.
71+ let args = Args {
72+ plugins : if args. update {
73+ common:: plugins_installed ( ) . is_ok_and ( |is_true| is_true)
74+ } else {
75+ args. plugins
76+ } ,
77+ tag : args. tag . clone ( ) ,
78+ ..* args
79+ } ;
5380
54- if afl_src_dir. join ( ".git" ) . is_dir ( ) {
55- let success = Command :: new ( "git" )
56- . args ( [ "clone" , afl_src_dir_str, & * tempdir. path ( ) . to_string_lossy ( ) ] )
57- . status ( )
58- . as_ref ( )
59- . is_ok_and ( ExitStatus :: success) ;
60- ensure ! ( success, "could not run 'git'" ) ;
61- } else {
62- copy_aflplusplus_submodule ( & tempdir. path ( ) . join ( AFL_SRC_PATH ) ) ?;
81+ let aflplusplus_dir =
82+ common:: aflplusplus_dir ( ) . with_context ( || "could not determine AFLplusplus directory" ) ?;
83+
84+ // smoelius: The AFLplusplus directory could be in one of three possible states:
85+ //
86+ // 1. Nonexistent
87+ // 2. Initialized with a copy of the AFLplusplus submodule from afl.rs's source tree
88+ // 3. Cloned from `AFLPLUSPLUS_URL`
89+ //
90+ // If we are not updating and the AFLplusplus directory is nonexistent: initialize the directory
91+ // with a copy of the AFLplusplus submodule from afl.rs's source tree (the `else` case in the
92+ // next `if` statement).
93+ //
94+ // If we are updating and the AFLplusplus directory is a copy of the AFLplusplus submodule from
95+ // afl.rs's source tree: remove it and create a new directory by cloning AFL++ (the `else` case
96+ // in `update_to_stable_or_tag`).
97+ //
98+ // Finally, if we are updating: check out either `origin/stable` or the tag that was passed.
99+ if args. update {
100+ let rev_prev = if is_repo ( & aflplusplus_dir) ? {
101+ rev ( & aflplusplus_dir) . map ( Some ) ?
102+ } else {
103+ None
104+ } ;
105+
106+ update_to_stable_or_tag ( & aflplusplus_dir, args. tag . as_deref ( ) ) ?;
107+
108+ let rev_curr = rev ( & aflplusplus_dir) ?;
109+
110+ if rev_prev == Some ( rev_curr) && !args. force {
111+ eprintln ! ( "Nothing to do. Pass `--force` to force rebuilding." ) ;
112+ return Ok ( ( ) ) ;
113+ }
114+ } else if !aflplusplus_dir. join ( ".git" ) . try_exists ( ) ? {
115+ copy_aflplusplus_submodule ( & aflplusplus_dir) ?;
63116 }
64117
65- let work_dir = tempdir. path ( ) . join ( AFL_SRC_PATH ) ;
66-
67- build_afl ( args, & work_dir) ?;
68- build_afl_llvm_runtime ( args, & work_dir) ?;
118+ build_afl ( & args, & aflplusplus_dir) ?;
119+ build_afl_llvm_runtime ( & args, & aflplusplus_dir) ?;
69120
70121 if args. plugins {
71- copy_afl_llvm_plugins ( args, & work_dir ) ?;
122+ copy_afl_llvm_plugins ( & args, & aflplusplus_dir ) ?;
72123 }
73124
74125 let afl_dir = common:: afl_dir ( ) ?;
75- let Some ( dir ) = afl_dir. parent ( ) . map ( Path :: to_path_buf ) else {
126+ let Some ( afl_dir_parent ) = afl_dir. parent ( ) else {
76127 bail ! ( "could not get afl dir parent" ) ;
77128 } ;
78- eprintln ! ( "Artifacts written to {}" , dir . display( ) ) ;
129+ eprintln ! ( "Artifacts written to {}" , afl_dir_parent . display( ) ) ;
79130
80131 Ok ( ( ) )
81132}
82133
134+ fn update_to_stable_or_tag ( aflplusplus_dir : & Path , tag : Option < & str > ) -> Result < ( ) > {
135+ if is_repo ( aflplusplus_dir) ? {
136+ let success = Command :: new ( "git" )
137+ . arg ( "fetch" )
138+ . current_dir ( aflplusplus_dir)
139+ . status ( )
140+ . as_ref ( )
141+ . is_ok_and ( ExitStatus :: success) ;
142+ ensure ! ( success, "could not run 'git fetch'" ) ;
143+ } else {
144+ remove_aflplusplus_dir ( aflplusplus_dir) . unwrap_or_default ( ) ;
145+ let success = Command :: new ( "git" )
146+ . args ( [
147+ "clone" ,
148+ AFLPLUSPLUS_URL ,
149+ & * aflplusplus_dir. to_string_lossy ( ) ,
150+ ] )
151+ . status ( )
152+ . as_ref ( )
153+ . is_ok_and ( ExitStatus :: success) ;
154+ ensure ! ( success, "could not run 'git clone'" ) ;
155+ }
156+
157+ let mut command = Command :: new ( "git" ) ;
158+ command. arg ( "checkout" ) ;
159+ if let Some ( tag) = tag {
160+ command. arg ( tag) ;
161+ } else {
162+ command. arg ( "origin/stable" ) ;
163+ }
164+ command. current_dir ( aflplusplus_dir) ;
165+ let success = command. status ( ) . as_ref ( ) . is_ok_and ( ExitStatus :: success) ;
166+ ensure ! ( success, "could not run 'git checkout'" ) ;
167+
168+ Ok ( ( ) )
169+ }
170+
171+ fn remove_aflplusplus_dir ( aflplusplus_dir : & Path ) -> Result < ( ) > {
172+ std:: fs:: remove_dir_all ( aflplusplus_dir) . map_err ( Into :: into)
173+ }
174+
83175fn copy_aflplusplus_submodule ( aflplusplus_dir : & Path ) -> Result < ( ) > {
84176 let afl_src_dir = Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) ) . join ( AFL_SRC_PATH ) ;
85177 let afl_src_dir_str = & afl_src_dir. to_string_lossy ( ) ;
@@ -104,6 +196,28 @@ fn copy_aflplusplus_submodule(aflplusplus_dir: &Path) -> Result<()> {
104196 Ok ( ( ) )
105197}
106198
199+ // smoelius: `dot_git` will refer to an ASCII text file if it was copied from the AFLplusplus
200+ // submodule from afl.rs's source tree.
201+ fn is_repo ( aflplusplus_dir : & Path ) -> Result < bool > {
202+ let dot_git = aflplusplus_dir. join ( ".git" ) ;
203+ if dot_git. try_exists ( ) ? {
204+ Ok ( dot_git. is_dir ( ) )
205+ } else {
206+ Ok ( false )
207+ }
208+ }
209+
210+ fn rev ( dir : & Path ) -> Result < String > {
211+ let mut command = Command :: new ( "git" ) ;
212+ command. args ( [ "rev-parse" , "HEAD" ] ) ;
213+ command. current_dir ( dir) ;
214+ let output = command
215+ . output ( )
216+ . with_context ( || "could not run `git rev-parse`" ) ?;
217+ ensure ! ( output. status. success( ) , "`git rev-parse` failed" ) ;
218+ String :: from_utf8 ( output. stdout ) . map_err ( Into :: into)
219+ }
220+
107221fn build_afl ( args : & Args , work_dir : & Path ) -> Result < ( ) > {
108222 // if you had already installed cargo-afl previously you **must** clean AFL++
109223 let afl_dir = common:: afl_dir ( ) ?;
0 commit comments