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