1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module dextool.plugin.mutate.frontend.frontend;
11 
12 import logger = std.experimental.logger;
13 import std.array : empty, array;
14 import std.exception : collectException;
15 import std.path : buildPath;
16 
17 import dextool.compilation_db;
18 import dextool.type : Path, AbsolutePath, ExitStatusType;
19 
20 import dextool.plugin.mutate.frontend.argparser;
21 import dextool.plugin.mutate.type : MutationOrder, ReportKind, MutationKind,
22     ReportLevel, AdminOperation;
23 import dextool.plugin.mutate.config;
24 
25 @safe:
26 
27 ExitStatusType runMutate(ArgParser conf) {
28     import my.gc : memFree;
29 
30     logger.trace("ToolMode: ", conf.data.toolMode);
31 
32     auto mfree = memFree;
33 
34     alias Func1 = ExitStatusType function(ref ArgParser conf, ref DataAccess dacc) @safe;
35     Func1[ToolMode] modes;
36 
37     modes[ToolMode.analyzer] = &modeAnalyze;
38     modes[ToolMode.generate_mutant] = &modeGenerateMutant;
39     modes[ToolMode.test_mutants] = &modeTestMutants;
40     modes[ToolMode.report] = &modeReport;
41     modes[ToolMode.admin] = &modeAdmin;
42 
43     logger.info("Using ", conf.db);
44 
45     try
46         if (auto f = conf.toolMode in modes) {
47             return () @trusted {
48                 auto dacc = DataAccess.make(conf);
49                 return (*f)(conf, dacc);
50             }();
51         } catch (Exception e) {
52         logger.error(e.msg);
53         return ExitStatusType.Errors;
54     }
55 
56     switch (conf.toolMode) {
57     case ToolMode.none:
58         logger.error("No mode specified");
59         return ExitStatusType.Errors;
60     case ToolMode.dumpConfig:
61         return modeDumpFullConfig(conf);
62     case ToolMode.initConfig:
63         return modeInitConfig(conf);
64     default:
65         logger.error("Mode not supported. This should not happen. Contact the maintainer of dextool: ",
66                 conf.data.toolMode);
67         return ExitStatusType.Errors;
68     }
69 }
70 
71 private:
72 
73 import dextool.plugin.mutate.backend : FilesysIO, ValidateLoc;
74 
75 struct DataAccess {
76     import std.typecons : Nullable;
77 
78     import dextool.compilation_db : limitOrAllRange, parse, prependFlags, addCompiler, replaceCompiler,
79         addSystemIncludes, fileRange, fromArgCompileDb, ParsedCompileCommandRange, Compiler;
80     import dextool.plugin.mutate.backend : Database;
81 
82     Database db;
83     FrontendIO io;
84     FrontendValidateLoc validateLoc;
85 
86     ConfigCompileDb compileDb;
87     ConfigCompiler compiler;
88     string[] inFiles;
89 
90     // only generate it on demand. All modes do not require it.
91     ParsedCompileCommandRange frange() @trusted {
92         import std.algorithm : map, joiner;
93         import std.range : only;
94 
95         CompileCommandDB fusedCompileDb;
96         if (!compileDb.dbs.empty) {
97             fusedCompileDb = compileDb.dbs.fromArgCompileDb;
98         }
99 
100         // dfmt off
101         return ParsedCompileCommandRange.make(
102             only(fusedCompileDb.fileRange, fileRange(inFiles.map!(a => Path(a)).array, Compiler("/usr/bin/c++"))).joiner
103             .parse(compileDb.flagFilter)
104             .addCompiler(compiler.useCompilerSystemIncludes)
105             .replaceCompiler(compiler.useCompilerSystemIncludes)
106             .addSystemIncludes
107             .prependFlags(compiler.extraFlags)
108             .array);
109         // dfmt on
110     }
111 
112     static auto make(ref ArgParser conf) @trusted {
113         auto fe_io = new FrontendIO(conf.workArea.restrictDir,
114                 conf.workArea.outputDirectory, conf.mutationTest.dryRun);
115         auto fe_validate = new FrontendValidateLoc(conf.workArea.restrictDir,
116                 conf.workArea.outputDirectory);
117 
118         return DataAccess(Database.make(conf.db, conf.mutationTest.mutationOrder),
119                 fe_io, fe_validate, conf.compileDb, conf.compiler, conf.data.inFiles);
120     }
121 }
122 
123 /** Responsible for ensuring that when the output from the backend is written
124  * to a file it is within the user specified output directory.
125  *
126  * When the mode dry_run is set no files shall be written to the filesystem.
127  * Any kind of file shall be readable and "emulated" that it is writtable.
128  *
129  * Dryrun is used for testing the mutate plugin.
130  *
131  * #SPC-file_security-single_output
132  */
133 final class FrontendIO : FilesysIO {
134     import std.stdio : File;
135     import blob_model;
136     import dextool.plugin.mutate.backend : SafeOutput, Blob;
137 
138     BlobVfs vfs;
139 
140     private AbsolutePath[] restrict_dir;
141     private AbsolutePath output_dir;
142     private bool dry_run;
143 
144     this(AbsolutePath[] restrict_dir, AbsolutePath output_dir, bool dry_run) {
145         this.restrict_dir = restrict_dir;
146         this.output_dir = output_dir;
147         this.dry_run = dry_run;
148         this.vfs = new BlobVfs;
149     }
150 
151     override FilesysIO dup() {
152         return new FrontendIO(restrict_dir, output_dir, dry_run);
153     }
154 
155     override File getDevNull() {
156         return File("/dev/null", "w");
157     }
158 
159     override File getStdin() @trusted {
160         static import std.stdio;
161 
162         return std.stdio.stdin;
163     }
164 
165     override Path toRelativeRoot(Path p) @trusted {
166         import std.path : relativePath;
167 
168         return relativePath(p, output_dir).Path;
169     }
170 
171     override AbsolutePath toAbsoluteRoot(Path p) {
172         return AbsolutePath(buildPath(output_dir, p));
173     }
174 
175     override AbsolutePath getOutputDir() @safe pure nothrow @nogc {
176         return output_dir;
177     }
178 
179     override SafeOutput makeOutput(AbsolutePath p) @safe {
180         verifyPathInsideRoot(output_dir, p, dry_run);
181         return SafeOutput(p, this);
182     }
183 
184     override Blob makeInput(AbsolutePath p) @safe {
185         import std.file;
186 
187         verifyPathInsideRoot(output_dir, p, dry_run);
188 
189         const uri = Uri(cast(string) p);
190         if (!vfs.exists(uri)) {
191             auto blob = vfs.get(Uri(cast(string) p));
192             vfs.open(blob);
193         }
194         return vfs.get(uri);
195     }
196 
197     override void putFile(AbsolutePath fname, const(ubyte)[] data) @safe {
198         import std.stdio : File;
199 
200         // because a Blob/SafeOutput could theoretically be created via
201         // other means than a FilesysIO.
202         // TODO fix so this validate is not needed.
203         verifyPathInsideRoot(output_dir, fname, dry_run);
204         if (!dry_run)
205             File(fname, "w").rawWrite(data);
206     }
207 
208 private:
209     // assuming that root is already a realpath
210     // TODO: replace this function with dextool.utility.isPathInsideRoot
211     static void verifyPathInsideRoot(AbsolutePath root, AbsolutePath p, bool dry_run) {
212         import std.format : format;
213         import std.string : startsWith;
214 
215         if (!dry_run && !p.toString.startsWith((cast(string) root))) {
216             throw new Exception(format("Path '%s' escaping output directory (--out) '%s'", p, root));
217         }
218     }
219 }
220 
221 final class FrontendValidateLoc : ValidateLoc {
222     private AbsolutePath[] restrict_dir;
223     private AbsolutePath output_dir;
224 
225     this(AbsolutePath[] restrict_dir, AbsolutePath output_dir) {
226         this.restrict_dir = restrict_dir;
227         this.output_dir = output_dir;
228     }
229 
230     override ValidateLoc dup() {
231         return new FrontendValidateLoc(restrict_dir, output_dir);
232     }
233 
234     override AbsolutePath getOutputDir() nothrow {
235         return this.output_dir;
236     }
237 
238     override bool shouldAnalyze(AbsolutePath p) {
239         return this.shouldAnalyze(cast(string) p);
240     }
241 
242     /// Returns: if a file should be analyzed for mutants.
243     override bool shouldAnalyze(const string p) {
244         import std.algorithm : any;
245         import std.string : startsWith;
246 
247         if (restrict_dir.empty)
248             return true;
249 
250         auto realp = p.Path.AbsolutePath;
251 
252         bool res = any!(a => realp.toString.startsWith(a.toString))(restrict_dir);
253         logger.tracef(!res, "Path '%s' do not match any of [%(%s, %)]", realp, restrict_dir);
254         return res;
255     }
256 
257     /// Returns: if a file should be mutated.
258     override bool shouldMutate(AbsolutePath p) {
259         import std.file : isDir, exists;
260         import std.string : startsWith;
261 
262         if (!exists(p) || isDir(p))
263             return false;
264 
265         bool res = p.toString.startsWith(output_dir.toString);
266 
267         if (res) {
268             return shouldAnalyze(p);
269         }
270         return false;
271     }
272 }
273 
274 ExitStatusType modeDumpFullConfig(ref ArgParser conf) @safe {
275     import std.stdio : writeln, stderr;
276 
277     () @trusted {
278         // make it easy for a user to pipe the output to the config file
279         stderr.writeln("Dumping the configuration used. The format is TOML (.toml)");
280         stderr.writeln("If you want to use it put it in your '.dextool_mutate.toml'");
281     }();
282 
283     writeln(conf.toTOML);
284 
285     return ExitStatusType.Ok;
286 }
287 
288 ExitStatusType modeInitConfig(ref ArgParser conf) @safe {
289     import std.stdio : File;
290     import std.file : exists;
291 
292     if (exists(conf.miniConf.confFile)) {
293         logger.error("Configuration file already exists: ", conf.miniConf.confFile);
294         return ExitStatusType.Errors;
295     }
296 
297     try {
298         File(conf.miniConf.confFile, "w").write(conf.toTOML);
299         logger.info("Wrote configuration to ", conf.miniConf.confFile);
300         return ExitStatusType.Ok;
301     } catch (Exception e) {
302         logger.error(e.msg);
303     }
304 
305     return ExitStatusType.Errors;
306 }
307 
308 ExitStatusType modeAnalyze(ref ArgParser conf, ref DataAccess dacc) {
309     import dextool.plugin.mutate.backend : runAnalyzer;
310     import dextool.plugin.mutate.frontend.argparser : printFileAnalyzeHelp;
311 
312     printFileAnalyzeHelp(conf);
313 
314     return runAnalyzer(dacc.db, conf.analyze, conf.compiler, dacc.frange,
315             dacc.validateLoc, dacc.io);
316 }
317 
318 ExitStatusType modeGenerateMutant(ref ArgParser conf, ref DataAccess dacc) {
319     import dextool.plugin.mutate.backend : runGenerateMutant;
320     import dextool.plugin.mutate.backend.database.type : MutationId;
321 
322     return runGenerateMutant(dacc.db, conf.data.mutation,
323             MutationId(conf.generate.mutationId), dacc.io, dacc.validateLoc);
324 }
325 
326 ExitStatusType modeTestMutants(ref ArgParser conf, ref DataAccess dacc) {
327     import dextool.plugin.mutate.backend : makeTestMutant;
328 
329     return makeTestMutant.config(conf.mutationTest)
330         .mutations(conf.data.mutation).run(dacc.db, dacc.io);
331 }
332 
333 ExitStatusType modeReport(ref ArgParser conf, ref DataAccess dacc) {
334     import dextool.plugin.mutate.backend : runReport;
335 
336     return runReport(dacc.db, conf.data.mutation, conf.report, dacc.io);
337 }
338 
339 ExitStatusType modeAdmin(ref ArgParser conf, ref DataAccess dacc) {
340     import dextool.plugin.mutate.backend : makeAdmin;
341 
342     return makeAdmin().operation(conf.admin.adminOp).mutations(conf.data.mutation)
343         .fromStatus(conf.admin.mutantStatus).toStatus(conf.admin.mutantToStatus)
344         .testCaseRegex(conf.admin.testCaseRegex).markMutantData(conf.admin.mutationId,
345                 conf.admin.mutantRationale, dacc.io).run(dacc.db);
346 }