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