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     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.plugin.mutate.backend : SafeOutput, Blob;
123 
124     BlobVfs vfs;
125 
126     private AbsolutePath[] restrict_dir;
127     private AbsolutePath output_dir;
128     private bool dry_run;
129 
130     this(AbsolutePath[] restrict_dir, AbsolutePath output_dir, bool dry_run) {
131         this.restrict_dir = restrict_dir;
132         this.output_dir = output_dir;
133         this.dry_run = dry_run;
134         this.vfs = new BlobVfs;
135     }
136 
137     override FilesysIO dup() {
138         return new FrontendIO(restrict_dir, output_dir, dry_run);
139     }
140 
141     override File getDevNull() {
142         return File("/dev/null", "w");
143     }
144 
145     override File getStdin() @trusted {
146         static import std.stdio;
147 
148         return std.stdio.stdin;
149     }
150 
151     override Path toRelativeRoot(Path p) @trusted {
152         import std.path : relativePath;
153 
154         return relativePath(p, output_dir).Path;
155     }
156 
157     override AbsolutePath toAbsoluteRoot(Path p) {
158         return AbsolutePath(p, output_dir);
159     }
160 
161     override AbsolutePath getOutputDir() @safe pure nothrow @nogc {
162         return output_dir;
163     }
164 
165     override SafeOutput makeOutput(AbsolutePath p) @safe {
166         verifyPathInsideRoot(output_dir, p, dry_run);
167         return SafeOutput(p, this);
168     }
169 
170     override Blob makeInput(AbsolutePath p) @safe {
171         import std.file;
172 
173         verifyPathInsideRoot(output_dir, p, dry_run);
174 
175         const uri = Uri(cast(string) p);
176         if (!vfs.exists(uri)) {
177             auto blob = vfs.get(Uri(cast(string) p));
178             vfs.open(blob);
179         }
180         return vfs.get(uri);
181     }
182 
183     override void putFile(AbsolutePath fname, const(ubyte)[] data) @safe {
184         import std.stdio : File;
185 
186         // because a Blob/SafeOutput could theoretically be created via
187         // other means than a FilesysIO.
188         // TODO fix so this validate is not needed.
189         verifyPathInsideRoot(output_dir, fname, dry_run);
190         if (!dry_run)
191             File(fname, "w").rawWrite(data);
192     }
193 
194 private:
195     // assuming that root is already a realpath
196     // TODO: replace this function with dextool.utility.isPathInsideRoot
197     static void verifyPathInsideRoot(AbsolutePath root, AbsolutePath p, bool dry_run) {
198         import std.format : format;
199         import std.string : startsWith;
200 
201         if (!dry_run && !p.startsWith((cast(string) root))) {
202             logger.tracef("Path '%s' escaping output directory (--out) '%s'", p, root);
203             throw new Exception(format("Path '%s' escaping output directory (--out) '%s'", p, root));
204         }
205     }
206 }
207 
208 final class FrontendValidateLoc : ValidateLoc {
209     private AbsolutePath[] restrict_dir;
210     private AbsolutePath output_dir;
211 
212     this(AbsolutePath[] restrict_dir, AbsolutePath output_dir) {
213         this.restrict_dir = restrict_dir;
214         this.output_dir = output_dir;
215     }
216 
217     override ValidateLoc dup() {
218         return new FrontendValidateLoc(restrict_dir, output_dir);
219     }
220 
221     override AbsolutePath getOutputDir() nothrow {
222         return this.output_dir;
223     }
224 
225     override bool shouldAnalyze(AbsolutePath p) {
226         return this.shouldAnalyze(cast(string) p);
227     }
228 
229     override bool shouldAnalyze(const string p) {
230         import std.algorithm : any;
231         import std.string : startsWith;
232 
233         auto realp = p.Path.AbsolutePath;
234 
235         bool res = any!(a => realp.startsWith(a))(restrict_dir);
236         logger.tracef(!res, "Path '%s' do not match any of [%(%s, %)]", realp, restrict_dir);
237         return res;
238     }
239 
240     override bool shouldMutate(AbsolutePath p) {
241         import std.string : startsWith;
242 
243         bool res = p.startsWith(output_dir);
244         logger.tracef(!res, "Path '%s' escaping output directory (--out) '%s'", p, output_dir);
245         return res;
246     }
247 }
248 
249 ExitStatusType modeDumpFullConfig(ref ArgParser conf) @safe {
250     import std.stdio : writeln, stderr;
251 
252     () @trusted {
253         // make it easy for a user to pipe the output to the config file
254         stderr.writeln("Dumping the configuration used. The format is TOML (.toml)");
255         stderr.writeln("If you want to use it put it in your '.dextool_mutate.toml'");
256     }();
257 
258     writeln(conf.toTOML);
259 
260     return ExitStatusType.Ok;
261 }
262 
263 ExitStatusType modeInitConfig(ref ArgParser conf) @safe {
264     import std.stdio : File;
265     import std.file : exists;
266 
267     if (exists(conf.miniConf.confFile)) {
268         logger.error("Configuration file already exists: ", conf.miniConf.confFile);
269         return ExitStatusType.Errors;
270     }
271 
272     try {
273         File(conf.miniConf.confFile, "w").write(conf.toTOML);
274         logger.info("Wrote configuration to ", conf.miniConf.confFile);
275         return ExitStatusType.Ok;
276     } catch (Exception e) {
277         logger.error(e.msg);
278     }
279 
280     return ExitStatusType.Errors;
281 }
282 
283 ExitStatusType modeAnalyze(ref ArgParser conf, ref DataAccess dacc) {
284     import dextool.plugin.mutate.backend : runAnalyzer;
285     import dextool.plugin.mutate.frontend.argparser : printFileAnalyzeHelp;
286 
287     printFileAnalyzeHelp(conf);
288 
289     return runAnalyzer(dacc.db, conf.analyze, conf.compiler, dacc.frange,
290             dacc.validateLoc, dacc.io);
291 }
292 
293 ExitStatusType modeGenerateMutant(ref ArgParser conf, ref DataAccess dacc) {
294     import dextool.plugin.mutate.backend : runGenerateMutant;
295     import dextool.plugin.mutate.backend.database.type : MutationId;
296 
297     return runGenerateMutant(dacc.db, conf.data.mutation,
298             MutationId(conf.generate.mutationId), dacc.io, dacc.validateLoc);
299 }
300 
301 ExitStatusType modeTestMutants(ref ArgParser conf, ref DataAccess dacc) {
302     import dextool.plugin.mutate.backend : makeTestMutant;
303 
304     return makeTestMutant.config(conf.mutationTest)
305         .mutations(conf.data.mutation).run(dacc.db, dacc.io);
306 }
307 
308 ExitStatusType modeReport(ref ArgParser conf, ref DataAccess dacc) {
309     import dextool.plugin.mutate.backend : runReport;
310 
311     return runReport(dacc.db, conf.data.mutation, conf.report, dacc.io);
312 }
313 
314 ExitStatusType modeAdmin(ref ArgParser conf, ref DataAccess dacc) {
315     import dextool.plugin.mutate.backend : makeAdmin;
316 
317     return makeAdmin().operation(conf.admin.adminOp).mutations(conf.data.mutation)
318         .fromStatus(conf.admin.mutantStatus).toStatus(conf.admin.mutantToStatus)
319         .testCaseRegex(conf.admin.testCaseRegex).markMutantData(conf.admin.mutationId,
320                 conf.admin.mutantRationale, dacc.io).run(dacc.db);
321 }