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;
14 import std.exception : collectException;
15 
16 import dextool.compilation_db;
17 import dextool.type : Path, AbsolutePath, ExitStatusType;
18 
19 import dextool.plugin.mutate.frontend.argparser;
20 import dextool.plugin.mutate.type : MutationOrder, ReportKind, MutationKind,
21     ReportLevel, AdminOperation;
22 import dextool.plugin.mutate.config;
23 
24 @safe:
25 
26 ExitStatusType runMutate(ArgParser conf) {
27     import dextool.gc : MemFree;
28 
29     logger.trace("ToolMode: ", conf.data.toolMode);
30 
31     MemFree mfree;
32     mfree.start;
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 : CompileCommandFilter,
79         defaultCompilerFlagFilter, fromArgCompileDb;
80     import dextool.plugin.mutate.backend : Database;
81     import dextool.user_filerange;
82 
83     Database db;
84     FrontendIO io;
85     FrontendValidateLoc validateLoc;
86 
87     ConfigCompileDb compileDb;
88     ConfigCompiler compiler;
89     string[] inFiles;
90 
91     // only generate it on demand. All modes do not require it.
92     UserFileRange frange() @trusted {
93         CompileCommandDB fusedCompileDb;
94         if (!compileDb.dbs.empty) {
95             fusedCompileDb = compileDb.dbs.fromArgCompileDb;
96         }
97 
98         return UserFileRange(fusedCompileDb, inFiles, compiler.extraFlags,
99                 compileDb.flagFilter, compiler.useCompilerSystemIncludes);
100     }
101 
102     static auto make(ref ArgParser conf) @trusted {
103         auto fe_io = new FrontendIO(conf.workArea.restrictDir,
104                 conf.workArea.outputDirectory, conf.mutationTest.dryRun);
105         auto fe_validate = new FrontendValidateLoc(conf.workArea.restrictDir,
106                 conf.workArea.outputDirectory);
107 
108         return DataAccess(Database.make(conf.db, conf.mutationTest.mutationOrder),
109                 fe_io, fe_validate, conf.compileDb, conf.compiler, conf.data.inFiles);
110     }
111 }
112 
113 /** Responsible for ensuring that when the output from the backend is written
114  * to a file it is within the user specified output directory.
115  *
116  * When the mode dry_run is set no files shall be written to the filesystem.
117  * Any kind of file shall be readable and "emulated" that it is writtable.
118  *
119  * Dryrun is used for testing the mutate plugin.
120  *
121  * #SPC-file_security-single_output
122  */
123 final class FrontendIO : FilesysIO {
124     import std.exception : collectException;
125     import std.stdio : File;
126     import blob_model;
127     import dextool.plugin.mutate.backend : SafeOutput, Blob;
128 
129     BlobVfs vfs;
130 
131     private AbsolutePath[] restrict_dir;
132     private AbsolutePath output_dir;
133     private bool dry_run;
134 
135     this(AbsolutePath[] restrict_dir, AbsolutePath output_dir, bool dry_run) {
136         this.restrict_dir = restrict_dir;
137         this.output_dir = output_dir;
138         this.dry_run = dry_run;
139         this.vfs = new BlobVfs;
140     }
141 
142     override FilesysIO dup() {
143         return new FrontendIO(restrict_dir, output_dir, dry_run);
144     }
145 
146     override File getDevNull() {
147         return File("/dev/null", "w");
148     }
149 
150     override File getStdin() @trusted {
151         static import std.stdio;
152 
153         return std.stdio.stdin;
154     }
155 
156     override Path toRelativeRoot(Path p) @trusted {
157         import std.path : relativePath;
158 
159         return relativePath(p, output_dir).Path;
160     }
161 
162     override AbsolutePath toAbsoluteRoot(Path p) {
163         return AbsolutePath(p, output_dir);
164     }
165 
166     override AbsolutePath getOutputDir() @safe pure nothrow @nogc {
167         return output_dir;
168     }
169 
170     override SafeOutput makeOutput(AbsolutePath p) @safe {
171         verifyPathInsideRoot(output_dir, p, dry_run);
172         return SafeOutput(p, this);
173     }
174 
175     override Blob makeInput(AbsolutePath p) @safe {
176         import std.file;
177 
178         verifyPathInsideRoot(output_dir, p, dry_run);
179 
180         const uri = Uri(cast(string) p);
181         if (!vfs.exists(uri)) {
182             auto blob = vfs.get(Uri(cast(string) p));
183             vfs.open(blob);
184         }
185         return vfs.get(uri);
186     }
187 
188     override void putFile(AbsolutePath fname, const(ubyte)[] data) @safe {
189         import std.stdio : File;
190 
191         // because a Blob/SafeOutput could theoretically be created via
192         // other means than a FilesysIO.
193         // TODO fix so this validate is not needed.
194         verifyPathInsideRoot(output_dir, fname, dry_run);
195         if (!dry_run)
196             File(fname, "w").rawWrite(data);
197     }
198 
199 private:
200     // assuming that root is already a realpath
201     // TODO: replace this function with dextool.utility.isPathInsideRoot
202     static void verifyPathInsideRoot(AbsolutePath root, AbsolutePath p, bool dry_run) {
203         import std.format : format;
204         import std.string : startsWith;
205 
206         if (!dry_run && !p.startsWith((cast(string) root))) {
207             logger.tracef("Path '%s' escaping output directory (--out) '%s'", p, root);
208             throw new Exception(format("Path '%s' escaping output directory (--out) '%s'", p, root));
209         }
210     }
211 }
212 
213 final class FrontendValidateLoc : ValidateLoc {
214     private AbsolutePath[] restrict_dir;
215     private AbsolutePath output_dir;
216 
217     this(AbsolutePath[] restrict_dir, AbsolutePath output_dir) {
218         this.restrict_dir = restrict_dir;
219         this.output_dir = output_dir;
220     }
221 
222     override ValidateLoc dup() {
223         return new FrontendValidateLoc(restrict_dir, output_dir);
224     }
225 
226     override AbsolutePath getOutputDir() nothrow {
227         return this.output_dir;
228     }
229 
230     override bool shouldAnalyze(AbsolutePath p) {
231         return this.shouldAnalyze(cast(string) p);
232     }
233 
234     /// Returns: if a file should be analyzed for mutants.
235     override bool shouldAnalyze(const string p) {
236         import std.algorithm : any;
237         import std.string : startsWith;
238 
239         if (restrict_dir.empty)
240             return true;
241 
242         auto realp = p.Path.AbsolutePath;
243 
244         bool res = any!(a => realp.startsWith(a))(restrict_dir);
245         logger.tracef(!res, "Path '%s' do not match any of [%(%s, %)]", realp, restrict_dir);
246         return res;
247     }
248 
249     /// Returns: if a file should be mutated.
250     override bool shouldMutate(AbsolutePath p) {
251         import std.file : isDir, exists;
252         import std.string : startsWith;
253 
254         if (!exists(p) || isDir(p))
255             return false;
256 
257         bool res = p.startsWith(output_dir);
258         logger.tracef(!res, "Path '%s' escaping output directory (--out) '%s'", p, output_dir);
259 
260         if (res) {
261             return shouldAnalyze(p);
262         }
263         return false;
264     }
265 }
266 
267 ExitStatusType modeDumpFullConfig(ref ArgParser conf) @safe {
268     import std.stdio : writeln, stderr;
269 
270     () @trusted {
271         // make it easy for a user to pipe the output to the config file
272         stderr.writeln("Dumping the configuration used. The format is TOML (.toml)");
273         stderr.writeln("If you want to use it put it in your '.dextool_mutate.toml'");
274     }();
275 
276     writeln(conf.toTOML);
277 
278     return ExitStatusType.Ok;
279 }
280 
281 ExitStatusType modeInitConfig(ref ArgParser conf) @safe {
282     import std.stdio : File;
283     import std.file : exists;
284 
285     if (exists(conf.miniConf.confFile)) {
286         logger.error("Configuration file already exists: ", conf.miniConf.confFile);
287         return ExitStatusType.Errors;
288     }
289 
290     try {
291         File(conf.miniConf.confFile, "w").write(conf.toTOML);
292         logger.info("Wrote configuration to ", conf.miniConf.confFile);
293         return ExitStatusType.Ok;
294     } catch (Exception e) {
295         logger.error(e.msg);
296     }
297 
298     return ExitStatusType.Errors;
299 }
300 
301 ExitStatusType modeAnalyze(ref ArgParser conf, ref DataAccess dacc) {
302     import dextool.plugin.mutate.backend : runAnalyzer;
303     import dextool.plugin.mutate.frontend.argparser : printFileAnalyzeHelp;
304 
305     printFileAnalyzeHelp(conf);
306 
307     return runAnalyzer(dacc.db, conf.analyze, conf.compiler, dacc.frange,
308             dacc.validateLoc, dacc.io);
309 }
310 
311 ExitStatusType modeGenerateMutant(ref ArgParser conf, ref DataAccess dacc) {
312     import dextool.plugin.mutate.backend : runGenerateMutant;
313     import dextool.plugin.mutate.backend.database.type : MutationId;
314 
315     return runGenerateMutant(dacc.db, conf.data.mutation,
316             MutationId(conf.generate.mutationId), dacc.io, dacc.validateLoc);
317 }
318 
319 ExitStatusType modeTestMutants(ref ArgParser conf, ref DataAccess dacc) {
320     import dextool.plugin.mutate.backend : makeTestMutant;
321 
322     return makeTestMutant.config(conf.mutationTest)
323         .mutations(conf.data.mutation).run(dacc.db, dacc.io);
324 }
325 
326 ExitStatusType modeReport(ref ArgParser conf, ref DataAccess dacc) {
327     import dextool.plugin.mutate.backend : runReport;
328 
329     return runReport(dacc.db, conf.data.mutation, conf.report, dacc.io);
330 }
331 
332 ExitStatusType modeAdmin(ref ArgParser conf, ref DataAccess dacc) {
333     import dextool.plugin.mutate.backend : makeAdmin;
334 
335     return makeAdmin().operation(conf.admin.adminOp).mutations(conf.data.mutation)
336         .fromStatus(conf.admin.mutantStatus).toStatus(conf.admin.mutantToStatus)
337         .testCaseRegex(conf.admin.testCaseRegex).markMutantData(conf.admin.mutationId,
338                 conf.admin.mutantRationale, dacc.io).run(dacc.db);
339 }