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