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