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