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 }