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