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