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