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 
14 import dextool.compilation_db;
15 import dextool.type : AbsolutePath, FileName, ExitStatusType;
16 
17 import dextool.plugin.mutate.frontend.argparser : ArgParser, ToolMode, Mutation;
18 import dextool.plugin.mutate.type : MutationOrder, ReportKind, MutationKind,
19     ReportLevel, AdminOperation;
20 
21 @safe class Frontend {
22     import core.time : Duration;
23     import std.typecons : Nullable;
24 
25     ExitStatusType run() {
26         return runMutate(this);
27     }
28 
29 private:
30     ArgParser.Data rawUserData;
31 
32     AbsolutePath db;
33     AbsolutePath outputDirectory;
34     AbsolutePath[] restrictDir;
35     AbsolutePath mutationCompile;
36     AbsolutePath mutationTester;
37     AbsolutePath mutationTestCaseAnalyze;
38     Nullable!Duration mutationTesterRuntime;
39     CompileCommandDB compileDb;
40 }
41 
42 @safe:
43 
44 auto buildFrontend(ref ArgParser p) {
45     import std.algorithm : map;
46     import std.array : array;
47     import core.time : dur;
48     import dextool.compilation_db;
49 
50     auto r = new Frontend;
51 
52     r.rawUserData = p.data;
53 
54     r.db = AbsolutePath(FileName(p.db));
55     r.mutationTester = AbsolutePath(FileName(p.mutationTester));
56     r.mutationCompile = AbsolutePath(FileName(p.mutationCompile));
57     if (p.mutationTestCaseAnalyze.length != 0)
58         r.mutationTestCaseAnalyze = AbsolutePath(FileName(p.mutationTestCaseAnalyze));
59 
60     r.restrictDir = p.restrictDir.map!(a => AbsolutePath(FileName(a))).array;
61     r.outputDirectory = AbsolutePath(FileName(p.outputDirectory));
62 
63     if (p.mutationTesterRuntime != 0)
64         r.mutationTesterRuntime = p.mutationTesterRuntime.dur!"msecs";
65 
66     if (p.compileDb.length != 0) {
67         r.compileDb = p.compileDb.fromArgCompileDb;
68     }
69 
70     return r;
71 }
72 
73 private:
74 
75 ExitStatusType runMutate(Frontend fe) {
76     import dextool.compilation_db : CompileCommandFilter,
77         defaultCompilerFlagFilter;
78     import dextool.user_filerange;
79     import dextool.plugin.mutate.backend : Database;
80 
81     auto fe_io = new FrontendIO(fe.restrictDir, fe.outputDirectory, fe.rawUserData.dryRun);
82     scope (success)
83         fe_io.release;
84     auto fe_validate = new FrontendValidateLoc(fe.restrictDir, fe.outputDirectory);
85 
86     auto db = Database.make(fe.db, fe.rawUserData.mutationOrder);
87 
88     auto default_filter = CompileCommandFilter(defaultCompilerFlagFilter, 1);
89     auto frange = UserFileRange(fe.compileDb, fe.rawUserData.inFiles,
90             fe.rawUserData.cflags, default_filter);
91 
92     logger.trace("ToolMode: ", fe.rawUserData.toolMode);
93 
94     final switch (fe.rawUserData.toolMode) {
95     case ToolMode.none:
96         logger.error("No mode specified");
97         return ExitStatusType.Errors;
98     case ToolMode.analyzer:
99         import dextool.plugin.mutate.backend : runAnalyzer;
100 
101         return runAnalyzer(db, frange, fe_validate, fe_io);
102     case ToolMode.generate_mutant:
103         import dextool.plugin.mutate.backend : runGenerateMutant;
104 
105         return runGenerateMutant(db, fe.rawUserData.mutation,
106                 fe.rawUserData.mutationId, fe_io, fe_validate);
107     case ToolMode.test_mutants:
108         import dextool.plugin.mutate.backend : makeTestMutant;
109 
110         // dfmt off
111         return makeTestMutant
112             .mutations(fe.rawUserData.mutation)
113             .testSuiteProgram(fe.mutationTester)
114             .compileProgram(fe.mutationCompile)
115             .testCaseAnalyzeProgram(fe.mutationTestCaseAnalyze)
116             .testSuiteTimeout(fe.mutationTesterRuntime)
117             .testCaseAnalyzeBuiltin(fe.rawUserData.mutationTestCaseBuiltin)
118             .run(db, fe_io);
119         // dfmt on
120     case ToolMode.report:
121         import dextool.plugin.mutate.backend : runReport;
122 
123         return runReport(db, fe.rawUserData.mutation, fe.rawUserData.reportKind,
124                 fe.rawUserData.reportLevel, fe.rawUserData.reportSection, fe_io);
125     case ToolMode.admin:
126         import dextool.plugin.mutate.backend : makeAdmin;
127 
128         // dfmt off
129         return makeAdmin()
130             .operation(fe.rawUserData.adminOp)
131             .mutations(fe.rawUserData.mutation)
132             .fromStatus(fe.rawUserData.mutantStatus)
133             .toStatus(fe.rawUserData.mutantToStatus)
134             .testCaseRegex(fe.rawUserData.testCaseRegex)
135             .run(db);
136         // dfmt on
137     }
138 }
139 
140 import dextool.plugin.mutate.backend : FilesysIO, ValidateLoc;
141 
142 /** Responsible for ensuring that when the output from the backend is written
143  * to a file it is within the user specified output directory.
144  *
145  * When the mode dry_run is set no files shall be written to the filesystem.
146  * Any kind of file shall be readable and "emulated" that it is writtable.
147  *
148  * Dryrun is used for testing the mutate plugin.
149  *
150  * #SPC-plugin_mutate_file_security-single_output
151  */
152 final class FrontendIO : FilesysIO {
153     import std.exception : collectException;
154     import std.stdio : File;
155     import dextool.type : AbsolutePath;
156     import dextool.plugin.mutate.backend : SafeOutput, SafeInput;
157     import dextool.vfs : VirtualFileSystem, VfsFile;
158 
159     VirtualFileSystem vfs;
160 
161     private AbsolutePath[] restrict_dir;
162     private AbsolutePath output_dir;
163     private bool dry_run;
164 
165     this(AbsolutePath[] restrict_dir, AbsolutePath output_dir, bool dry_run) {
166         this.restrict_dir = restrict_dir;
167         this.output_dir = output_dir;
168         this.dry_run = dry_run;
169     }
170 
171     void release() {
172         vfs.release();
173     }
174 
175     override File getDevNull() {
176         return File("/dev/null", "w");
177     }
178 
179     override File getStdin() {
180         static import std.stdio;
181 
182         return () @trusted{ return std.stdio.stdin; }();
183     }
184 
185     override AbsolutePath getOutputDir() @safe pure nothrow @nogc {
186         return output_dir;
187     }
188 
189     override SafeOutput makeOutput(AbsolutePath p) @safe {
190         verifyPathInsideRoot(output_dir, p, dry_run);
191         return SafeOutput(p, this);
192     }
193 
194     override SafeInput makeInput(AbsolutePath p) @safe {
195         import std.file;
196 
197         verifyPathInsideRoot(output_dir, p, dry_run);
198 
199         auto f = vfs.open(cast(FileName) p);
200         return SafeInput(f[]);
201     }
202 
203     override void putFile(AbsolutePath fname, const(ubyte)[] data) @safe {
204         import std.stdio : File;
205 
206         // because a SafeInput/SafeOutput could theoretically be created via
207         // other means than a FilesysIO.
208         // TODO fix so this validate is not needed.
209         verifyPathInsideRoot(output_dir, fname, dry_run);
210         if (!dry_run)
211             File(fname, "w").rawWrite(data);
212     }
213 
214 private:
215     static void verifyPathInsideRoot(AbsolutePath root, AbsolutePath p, bool dry_run) {
216         import std.format : format;
217         import std..string : startsWith;
218 
219         if (!dry_run && !(cast(string) p).startsWith((cast(string) root))) {
220             throw new Exception(format("Path '%s' escaping output directory (--out) '%s'", p, root));
221         }
222     }
223 }
224 
225 final class FrontendValidateLoc : ValidateLoc {
226     private AbsolutePath[] restrict_dir;
227     private AbsolutePath output_dir;
228 
229     this(AbsolutePath[] restrict_dir, AbsolutePath output_dir) {
230         this.restrict_dir = restrict_dir;
231         this.output_dir = output_dir;
232     }
233 
234     override AbsolutePath getOutputDir() nothrow {
235         return this.output_dir;
236     }
237 
238     override bool shouldAnalyze(AbsolutePath p) {
239         return this.shouldAnalyze(cast(string) p);
240     }
241 
242     override bool shouldAnalyze(string p) {
243         import std.algorithm : any;
244         import std..string : startsWith;
245 
246         return any!(a => p.startsWith(a))(restrict_dir);
247     }
248 
249     override bool shouldMutate(AbsolutePath p) {
250         import std..string : startsWith;
251 
252         return (cast(string) p).startsWith(output_dir);
253     }
254 }