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 FilesysIO dup() { 139 return new FrontendIO(restrict_dir, output_dir, dry_run); 140 } 141 142 override File getDevNull() { 143 return File("/dev/null", "w"); 144 } 145 146 override File getStdin() @trusted { 147 static import std.stdio; 148 149 return std.stdio.stdin; 150 } 151 152 override Path toRelativeRoot(Path p) @trusted { 153 import std.path : relativePath; 154 155 return relativePath(p, output_dir).Path; 156 } 157 158 override AbsolutePath getOutputDir() @safe pure nothrow @nogc { 159 return output_dir; 160 } 161 162 override SafeOutput makeOutput(AbsolutePath p) @safe { 163 verifyPathInsideRoot(output_dir, p, dry_run); 164 return SafeOutput(p, this); 165 } 166 167 override Blob makeInput(AbsolutePath p) @safe { 168 import std.file; 169 170 verifyPathInsideRoot(output_dir, p, dry_run); 171 172 const uri = Uri(cast(string) p); 173 if (!vfs.exists(uri)) { 174 auto blob = vfs.get(Uri(cast(string) p)); 175 vfs.open(blob); 176 } 177 return vfs.get(uri); 178 } 179 180 override void putFile(AbsolutePath fname, const(ubyte)[] data) @safe { 181 import std.stdio : File; 182 183 // because a Blob/SafeOutput could theoretically be created via 184 // other means than a FilesysIO. 185 // TODO fix so this validate is not needed. 186 verifyPathInsideRoot(output_dir, fname, dry_run); 187 if (!dry_run) 188 File(fname, "w").rawWrite(data); 189 } 190 191 private: 192 // assuming that root is already a realpath 193 // TODO: replace this function with dextool.utility.isPathInsideRoot 194 static void verifyPathInsideRoot(AbsolutePath root, AbsolutePath p, bool dry_run) { 195 import std.format : format; 196 import std.string : startsWith; 197 198 if (!dry_run && !p.startsWith((cast(string) root))) { 199 logger.tracef("Path '%s' escaping output directory (--out) '%s'", p, root); 200 throw new Exception(format("Path '%s' escaping output directory (--out) '%s'", p, root)); 201 } 202 } 203 } 204 205 final class FrontendValidateLoc : ValidateLoc { 206 private AbsolutePath[] restrict_dir; 207 private AbsolutePath output_dir; 208 209 this(AbsolutePath[] restrict_dir, AbsolutePath output_dir) { 210 this.restrict_dir = restrict_dir; 211 this.output_dir = output_dir; 212 } 213 214 override ValidateLoc dup() { 215 return new FrontendValidateLoc(restrict_dir, output_dir); 216 } 217 218 override AbsolutePath getOutputDir() nothrow { 219 return this.output_dir; 220 } 221 222 override bool shouldAnalyze(AbsolutePath p) { 223 return this.shouldAnalyze(cast(string) p); 224 } 225 226 override bool shouldAnalyze(const string p) { 227 import std.algorithm : any; 228 import std.string : startsWith; 229 230 auto realp = p.Path.AbsolutePath; 231 232 bool res = any!(a => realp.startsWith(a))(restrict_dir); 233 logger.tracef(!res, "Path '%s' do not match any of [%(%s, %)]", realp, restrict_dir); 234 return res; 235 } 236 237 override bool shouldMutate(AbsolutePath p) { 238 import std.string : startsWith; 239 240 bool res = p.startsWith(output_dir); 241 logger.tracef(!res, "Path '%s' escaping output directory (--out) '%s'", p, output_dir); 242 return res; 243 } 244 } 245 246 ExitStatusType modeDumpFullConfig(ref ArgParser conf) @safe { 247 import std.stdio : writeln, stderr; 248 249 () @trusted { 250 // make it easy for a user to pipe the output to the config file 251 stderr.writeln("Dumping the configuration used. The format is TOML (.toml)"); 252 stderr.writeln("If you want to use it put it in your '.dextool_mutate.toml'"); 253 }(); 254 255 writeln(conf.toTOML); 256 257 return ExitStatusType.Ok; 258 } 259 260 ExitStatusType modeInitConfig(ref ArgParser conf) @safe { 261 import std.stdio : File; 262 import std.file : exists; 263 264 if (exists(conf.miniConf.confFile)) { 265 logger.error("Configuration file already exists: ", conf.miniConf.confFile); 266 return ExitStatusType.Errors; 267 } 268 269 try { 270 File(conf.miniConf.confFile, "w").write(conf.toTOML); 271 logger.info("Wrote configuration to ", conf.miniConf.confFile); 272 return ExitStatusType.Ok; 273 } catch (Exception e) { 274 logger.error(e.msg); 275 } 276 277 return ExitStatusType.Errors; 278 } 279 280 ExitStatusType modeAnalyze(ref ArgParser conf, ref DataAccess dacc) { 281 import dextool.plugin.mutate.backend : runAnalyzer; 282 import dextool.plugin.mutate.frontend.argparser : printFileAnalyzeHelp; 283 284 printFileAnalyzeHelp(conf); 285 286 return runAnalyzer(dacc.db, conf.analyze, conf.compiler, dacc.frange, 287 dacc.validateLoc, dacc.io); 288 } 289 290 ExitStatusType modeGenerateMutant(ref ArgParser conf, ref DataAccess dacc) { 291 import dextool.plugin.mutate.backend : runGenerateMutant; 292 import dextool.plugin.mutate.backend.database.type : MutationId; 293 294 return runGenerateMutant(dacc.db, conf.data.mutation, 295 MutationId(conf.generate.mutationId), dacc.io, dacc.validateLoc); 296 } 297 298 ExitStatusType modeTestMutants(ref ArgParser conf, ref DataAccess dacc) { 299 import dextool.plugin.mutate.backend : makeTestMutant; 300 301 return makeTestMutant.config(conf.mutationTest) 302 .mutations(conf.data.mutation).run(dacc.db, dacc.io); 303 } 304 305 ExitStatusType modeReport(ref ArgParser conf, ref DataAccess dacc) { 306 import dextool.plugin.mutate.backend : runReport; 307 308 return runReport(dacc.db, conf.data.mutation, conf.report, dacc.io); 309 } 310 311 ExitStatusType modeAdmin(ref ArgParser conf, ref DataAccess dacc) { 312 import dextool.plugin.mutate.backend : makeAdmin; 313 314 return makeAdmin().operation(conf.admin.adminOp).mutations(conf.data.mutation) 315 .fromStatus(conf.admin.mutantStatus).toStatus(conf.admin.mutantToStatus) 316 .testCaseRegex(conf.admin.testCaseRegex).markMutantData(conf.admin.mutationId, 317 conf.admin.mutantRationale, dacc.io).run(dacc.db); 318 }