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