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, array; 14 import std.exception : collectException; 15 import std.path : buildPath; 16 17 import dextool.compilation_db; 18 import dextool.type : Path, AbsolutePath, ExitStatusType; 19 20 import dextool.plugin.mutate.frontend.argparser; 21 import dextool.plugin.mutate.type : MutationOrder, ReportKind, MutationKind, 22 ReportLevel, AdminOperation; 23 import dextool.plugin.mutate.config; 24 25 @safe: 26 27 ExitStatusType runMutate(ArgParser conf) { 28 import my.gc : memFree; 29 30 logger.trace("ToolMode: ", conf.data.toolMode); 31 32 auto mfree = memFree; 33 34 alias Func1 = ExitStatusType function(ref ArgParser conf, ref DataAccess dacc) @safe; 35 Func1[ToolMode] modes; 36 37 modes[ToolMode.analyzer] = &modeAnalyze; 38 modes[ToolMode.generate_mutant] = &modeGenerateMutant; 39 modes[ToolMode.test_mutants] = &modeTestMutants; 40 modes[ToolMode.report] = &modeReport; 41 modes[ToolMode.admin] = &modeAdmin; 42 43 logger.info("Using ", conf.db); 44 45 try 46 if (auto f = conf.toolMode in modes) { 47 return () @trusted { 48 auto dacc = DataAccess.make(conf); 49 return (*f)(conf, dacc); 50 }(); 51 } catch (Exception e) { 52 logger.error(e.msg); 53 return ExitStatusType.Errors; 54 } 55 56 switch (conf.toolMode) { 57 case ToolMode.none: 58 logger.error("No mode specified"); 59 return ExitStatusType.Errors; 60 case ToolMode.dumpConfig: 61 return modeDumpFullConfig(conf); 62 case ToolMode.initConfig: 63 return modeInitConfig(conf); 64 default: 65 logger.error("Mode not supported. This should not happen. Contact the maintainer of dextool: ", 66 conf.data.toolMode); 67 return ExitStatusType.Errors; 68 } 69 } 70 71 private: 72 73 import dextool.plugin.mutate.backend : FilesysIO, ValidateLoc; 74 75 struct DataAccess { 76 import std.typecons : Nullable; 77 78 import dextool.compilation_db : limitOrAllRange, parse, prependFlags, addCompiler, replaceCompiler, 79 addSystemIncludes, fileRange, fromArgCompileDb, ParsedCompileCommandRange, Compiler; 80 import dextool.plugin.mutate.backend : Database; 81 82 Database db; 83 FrontendIO io; 84 FrontendValidateLoc validateLoc; 85 86 ConfigCompileDb compileDb; 87 ConfigCompiler compiler; 88 string[] inFiles; 89 90 // only generate it on demand. All modes do not require it. 91 ParsedCompileCommandRange frange() @trusted { 92 import std.algorithm : map, joiner; 93 import std.range : only; 94 95 CompileCommandDB fusedCompileDb; 96 if (!compileDb.dbs.empty) { 97 fusedCompileDb = compileDb.dbs.fromArgCompileDb; 98 } 99 100 // dfmt off 101 return ParsedCompileCommandRange.make( 102 only(fusedCompileDb.fileRange, fileRange(inFiles.map!(a => Path(a)).array, Compiler("/usr/bin/c++"))).joiner 103 .parse(compileDb.flagFilter) 104 .addCompiler(compiler.useCompilerSystemIncludes) 105 .replaceCompiler(compiler.useCompilerSystemIncludes) 106 .addSystemIncludes 107 .prependFlags(compiler.extraFlags) 108 .array); 109 // dfmt on 110 } 111 112 static auto make(ref ArgParser conf) @trusted { 113 auto fe_io = new FrontendIO(conf.workArea.restrictDir, 114 conf.workArea.outputDirectory, conf.mutationTest.dryRun); 115 auto fe_validate = new FrontendValidateLoc(conf.workArea.restrictDir, 116 conf.workArea.outputDirectory); 117 118 return DataAccess(Database.make(conf.db, conf.mutationTest.mutationOrder), 119 fe_io, fe_validate, conf.compileDb, conf.compiler, conf.data.inFiles); 120 } 121 } 122 123 /** Responsible for ensuring that when the output from the backend is written 124 * to a file it is within the user specified output directory. 125 * 126 * When the mode dry_run is set no files shall be written to the filesystem. 127 * Any kind of file shall be readable and "emulated" that it is writtable. 128 * 129 * Dryrun is used for testing the mutate plugin. 130 * 131 * #SPC-file_security-single_output 132 */ 133 final class FrontendIO : FilesysIO { 134 import std.stdio : File; 135 import blob_model; 136 import dextool.plugin.mutate.backend : SafeOutput, Blob; 137 138 BlobVfs vfs; 139 140 private AbsolutePath[] restrict_dir; 141 private AbsolutePath output_dir; 142 private bool dry_run; 143 144 this(AbsolutePath[] restrict_dir, AbsolutePath output_dir, bool dry_run) { 145 this.restrict_dir = restrict_dir; 146 this.output_dir = output_dir; 147 this.dry_run = dry_run; 148 this.vfs = new BlobVfs; 149 } 150 151 override FilesysIO dup() { 152 return new FrontendIO(restrict_dir, output_dir, dry_run); 153 } 154 155 override File getDevNull() { 156 return File("/dev/null", "w"); 157 } 158 159 override File getStdin() @trusted { 160 static import std.stdio; 161 162 return std.stdio.stdin; 163 } 164 165 override Path toRelativeRoot(Path p) @trusted { 166 import std.path : relativePath; 167 168 return relativePath(p, output_dir).Path; 169 } 170 171 override AbsolutePath toAbsoluteRoot(Path p) { 172 return AbsolutePath(buildPath(output_dir, p)); 173 } 174 175 override AbsolutePath getOutputDir() @safe pure nothrow @nogc { 176 return output_dir; 177 } 178 179 override SafeOutput makeOutput(AbsolutePath p) @safe { 180 verifyPathInsideRoot(output_dir, p, dry_run); 181 return SafeOutput(p, this); 182 } 183 184 override Blob makeInput(AbsolutePath p) @safe { 185 import std.file; 186 187 verifyPathInsideRoot(output_dir, p, dry_run); 188 189 const uri = Uri(cast(string) p); 190 if (!vfs.exists(uri)) { 191 auto blob = vfs.get(Uri(cast(string) p)); 192 vfs.open(blob); 193 } 194 return vfs.get(uri); 195 } 196 197 override void putFile(AbsolutePath fname, const(ubyte)[] data) @safe { 198 import std.stdio : File; 199 200 // because a Blob/SafeOutput could theoretically be created via 201 // other means than a FilesysIO. 202 // TODO fix so this validate is not needed. 203 verifyPathInsideRoot(output_dir, fname, dry_run); 204 if (!dry_run) 205 File(fname, "w").rawWrite(data); 206 } 207 208 private: 209 // assuming that root is already a realpath 210 // TODO: replace this function with dextool.utility.isPathInsideRoot 211 static void verifyPathInsideRoot(AbsolutePath root, AbsolutePath p, bool dry_run) { 212 import std.format : format; 213 import std.string : startsWith; 214 215 if (!dry_run && !p.toString.startsWith((cast(string) root))) { 216 throw new Exception(format("Path '%s' escaping output directory (--out) '%s'", p, root)); 217 } 218 } 219 } 220 221 final class FrontendValidateLoc : ValidateLoc { 222 private AbsolutePath[] restrict_dir; 223 private AbsolutePath output_dir; 224 225 this(AbsolutePath[] restrict_dir, AbsolutePath output_dir) { 226 this.restrict_dir = restrict_dir; 227 this.output_dir = output_dir; 228 } 229 230 override ValidateLoc dup() { 231 return new FrontendValidateLoc(restrict_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 /// Returns: if a file should be analyzed for mutants. 243 override bool shouldAnalyze(const string p) { 244 import std.algorithm : any; 245 import std.string : startsWith; 246 247 if (restrict_dir.empty) 248 return true; 249 250 auto realp = p.Path.AbsolutePath; 251 252 bool res = any!(a => realp.toString.startsWith(a.toString))(restrict_dir); 253 logger.tracef(!res, "Path '%s' do not match any of [%(%s, %)]", realp, restrict_dir); 254 return res; 255 } 256 257 /// Returns: if a file should be mutated. 258 override bool shouldMutate(AbsolutePath p) { 259 import std.file : isDir, exists; 260 import std.string : startsWith; 261 262 if (!exists(p) || isDir(p)) 263 return false; 264 265 bool res = p.toString.startsWith(output_dir.toString); 266 267 if (res) { 268 return shouldAnalyze(p); 269 } 270 return false; 271 } 272 } 273 274 ExitStatusType modeDumpFullConfig(ref ArgParser conf) @safe { 275 import std.stdio : writeln, stderr; 276 277 () @trusted { 278 // make it easy for a user to pipe the output to the config file 279 stderr.writeln("Dumping the configuration used. The format is TOML (.toml)"); 280 stderr.writeln("If you want to use it put it in your '.dextool_mutate.toml'"); 281 }(); 282 283 writeln(conf.toTOML); 284 285 return ExitStatusType.Ok; 286 } 287 288 ExitStatusType modeInitConfig(ref ArgParser conf) @safe { 289 import std.stdio : File; 290 import std.file : exists; 291 292 if (exists(conf.miniConf.confFile)) { 293 logger.error("Configuration file already exists: ", conf.miniConf.confFile); 294 return ExitStatusType.Errors; 295 } 296 297 try { 298 File(conf.miniConf.confFile, "w").write(conf.toTOML); 299 logger.info("Wrote configuration to ", conf.miniConf.confFile); 300 return ExitStatusType.Ok; 301 } catch (Exception e) { 302 logger.error(e.msg); 303 } 304 305 return ExitStatusType.Errors; 306 } 307 308 ExitStatusType modeAnalyze(ref ArgParser conf, ref DataAccess dacc) { 309 import dextool.plugin.mutate.backend : runAnalyzer; 310 import dextool.plugin.mutate.frontend.argparser : printFileAnalyzeHelp; 311 312 printFileAnalyzeHelp(conf); 313 314 return runAnalyzer(dacc.db, conf.analyze, conf.compiler, dacc.frange, 315 dacc.validateLoc, dacc.io); 316 } 317 318 ExitStatusType modeGenerateMutant(ref ArgParser conf, ref DataAccess dacc) { 319 import dextool.plugin.mutate.backend : runGenerateMutant; 320 import dextool.plugin.mutate.backend.database.type : MutationId; 321 322 return runGenerateMutant(dacc.db, conf.data.mutation, 323 MutationId(conf.generate.mutationId), dacc.io, dacc.validateLoc); 324 } 325 326 ExitStatusType modeTestMutants(ref ArgParser conf, ref DataAccess dacc) { 327 import dextool.plugin.mutate.backend : makeTestMutant; 328 329 return makeTestMutant.config(conf.mutationTest) 330 .mutations(conf.data.mutation).run(dacc.db, dacc.io); 331 } 332 333 ExitStatusType modeReport(ref ArgParser conf, ref DataAccess dacc) { 334 import dextool.plugin.mutate.backend : runReport; 335 336 return runReport(dacc.db, conf.data.mutation, conf.report, dacc.io); 337 } 338 339 ExitStatusType modeAdmin(ref ArgParser conf, ref DataAccess dacc) { 340 import dextool.plugin.mutate.backend : makeAdmin; 341 342 return makeAdmin().operation(conf.admin.adminOp).mutations(conf.data.mutation) 343 .fromStatus(conf.admin.mutantStatus).toStatus(conf.admin.mutantToStatus) 344 .testCaseRegex(conf.admin.testCaseRegex).markMutantData(conf.admin.mutationId, 345 conf.admin.mutantRationale, dacc.io).run(dacc.db); 346 }