1 /** 2 * Parse command line arguments from response files. 3 * 4 * This file is not shared with other compilers which use the DMD front-end. 5 * 6 * Copyright: Copyright (C) 1999-2020 by The D Language Foundation, All Rights Reserved 7 * Some portions copyright (c) 1994-1995 by Symantec 8 * Authors: $(LINK2 http://www.digitalmars.com, Walter Bright) 9 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 10 * Source: $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/root/response.d, root/_response.d) 11 * Documentation: https://dlang.org/phobos/dmd_root_response.html 12 * Coverage: https://codecov.io/gh/dlang/dmd/src/master/src/dmd/root/response.d 13 */ 14 15 module dmd.root.response; 16 17 import dmd.root.file; 18 import dmd.root.filename; 19 20 /// 21 alias responseExpand = responseExpandFrom!lookupInEnvironment; 22 23 /********************************* 24 * Expand any response files in command line. 25 * Response files are arguments that look like: 26 * @NAME 27 * The names are resolved by calling the 'lookup' function passed as a template 28 * parameter. That function is expected to first check the environment and then 29 * the file system. 30 * Arguments are separated by spaces, tabs, or newlines. These can be 31 * imbedded within arguments by enclosing the argument in "". 32 * Backslashes can be used to escape a ". 33 * A line comment can be started with #. 34 * Recursively expands nested response files. 35 * 36 * To use, put the arguments in a Strings object and call this on it. 37 * 38 * Digital Mars's MAKE program can be notified that a program can accept 39 * long command lines via environment variables by preceding the rule 40 * line for the program with a *. 41 * 42 * Params: 43 * lookup = alias to a function that is called to look up response file 44 * arguments in the environment. It is expected to accept a null- 45 * terminated string and return a mutable char[] that ends with 46 * a null-terminator or null if the response file could not be 47 * resolved. 48 * args = array containing arguments as null-terminated strings 49 * 50 * Returns: 51 * true on success, false if a response file could not be expanded. 52 */ 53 bool responseExpandFrom(alias lookup)(ref Strings args) nothrow 54 { 55 const(char)* cp; 56 bool recurse = false; 57 58 // i is updated by insertArgumentsFromResponse, so no foreach 59 for (size_t i = 0; i < args.dim;) 60 { 61 cp = args[i]; 62 if (cp[0] != '@') 63 { 64 ++i; 65 continue; 66 } 67 args.remove(i); 68 auto buffer = lookup(&cp[1]); 69 if (!buffer) { 70 /* error */ 71 /* BUG: any file buffers are not free'd */ 72 return false; 73 } 74 75 recurse = insertArgumentsFromResponse(buffer, args, i) || recurse; 76 } 77 if (recurse) 78 { 79 /* Recursively expand @filename */ 80 if (!responseExpandFrom!lookup(args)) 81 /* error */ 82 /* BUG: any file buffers are not free'd */ 83 return false; 84 } 85 return true; /* success */ 86 } 87 88 version (unittest) 89 { 90 char[] testEnvironment(const(char)* str) nothrow pure 91 { 92 import core.stdc.string: strlen; 93 import dmd.root.string : toDString; 94 switch (str.toDString()) 95 { 96 case "Foo": 97 return "foo @Bar #\0".dup; 98 case "Bar": 99 return "bar @Nil\0".dup; 100 case "Error": 101 return "@phony\0".dup; 102 case "Nil": 103 return "\0".dup; 104 default: 105 return null; 106 } 107 } 108 } 109 110 unittest 111 { 112 auto args = Strings(4); 113 args[0] = "first"; 114 args[1] = "@Foo"; 115 args[2] = "@Bar"; 116 args[3] = "last"; 117 118 assert(responseExpand!testEnvironment(args)); 119 assert(args.length == 5); 120 assert(args[0][0 .. 6] == "first\0"); 121 assert(args[1][0 .. 4] == "foo\0"); 122 assert(args[2][0 .. 4] == "bar\0"); 123 assert(args[3][0 .. 4] == "bar\0"); 124 assert(args[4][0 .. 5] == "last\0"); 125 } 126 127 unittest 128 { 129 auto args = Strings(2); 130 args[0] = "@phony"; 131 args[1] = "dummy"; 132 assert(!responseExpand!testEnvironment(args)); 133 } 134 135 unittest 136 { 137 auto args = Strings(2); 138 args[0] = "@Foo"; 139 args[1] = "@Error"; 140 assert(!responseExpand!testEnvironment(args)); 141 } 142 143 /********************************* 144 * Take the contents of a response-file 'buffer', parse it and put the resulting 145 * arguments in 'args' at 'argIndex'. 'argIndex' will be updated to point just 146 * after the inserted arguments. 147 * The logic of this should match that in setargv() 148 * 149 * Params: 150 * buffer = mutable string containing the response file 151 * args = list of arguments 152 * argIndex = position in 'args' where response arguments are inserted 153 * 154 * Returns: 155 * true if another response argument was found 156 */ 157 bool insertArgumentsFromResponse(char[] buffer, ref Strings args, ref size_t argIndex) nothrow 158 { 159 bool recurse = false; 160 bool comment = false; 161 162 for (size_t p = 0; p < buffer.length; p++) 163 { 164 //char* d; 165 size_t d = 0; 166 char c, lastc; 167 bool instring; 168 int numSlashes, nonSlashes; 169 switch (buffer[p]) 170 { 171 case 26: 172 /* ^Z marks end of file */ 173 return recurse; 174 case '\r': 175 case '\n': 176 comment = false; 177 goto case; 178 case 0: 179 case ' ': 180 case '\t': 181 continue; 182 // scan to start of argument 183 case '#': 184 comment = true; 185 continue; 186 case '@': 187 if (comment) 188 { 189 continue; 190 } 191 recurse = true; 192 goto default; 193 default: 194 /* start of new argument */ 195 if (comment) 196 { 197 continue; 198 } 199 args.insert(argIndex, &buffer[p]); 200 ++argIndex; 201 instring = false; 202 c = 0; 203 numSlashes = 0; 204 for (d = p; 1; p++) 205 { 206 lastc = c; 207 if (p >= buffer.length) 208 { 209 buffer[d] = '\0'; 210 return recurse; 211 } 212 c = buffer[p]; 213 switch (c) 214 { 215 case '"': 216 /* 217 Yes this looks strange,but this is so that we are 218 MS Compatible, tests have shown that: 219 \\\\"foo bar" gets passed as \\foo bar 220 \\\\foo gets passed as \\\\foo 221 \\\"foo gets passed as \"foo 222 and \"foo gets passed as "foo in VC! 223 */ 224 nonSlashes = numSlashes % 2; 225 numSlashes = numSlashes / 2; 226 for (; numSlashes > 0; numSlashes--) 227 { 228 d--; 229 buffer[d] = '\0'; 230 } 231 if (nonSlashes) 232 { 233 buffer[d - 1] = c; 234 } 235 else 236 { 237 instring = !instring; 238 } 239 break; 240 case 26: 241 buffer[d] = '\0'; // terminate argument 242 return recurse; 243 case '\r': 244 c = lastc; 245 continue; 246 // ignore 247 case ' ': 248 case '\t': 249 if (!instring) 250 { 251 case '\n': 252 case 0: 253 buffer[d] = '\0'; // terminate argument 254 goto Lnextarg; 255 } 256 goto default; 257 default: 258 if (c == '\\') 259 numSlashes++; 260 else 261 numSlashes = 0; 262 buffer[d++] = c; 263 break; 264 } 265 } 266 } 267 Lnextarg: 268 } 269 return recurse; 270 } 271 272 unittest 273 { 274 auto args = Strings(4); 275 args[0] = "arg0"; 276 args[1] = "arg1"; 277 args[2] = "arg2"; 278 279 char[] testData = "".dup; 280 size_t index = 1; 281 assert(insertArgumentsFromResponse(testData, args, index) == false); 282 assert(index == 1); 283 284 testData = (`\\\\"foo bar" \\\\foo \\\"foo \"foo "\"" # @comment`~'\0').dup; 285 assert(insertArgumentsFromResponse(testData, args, index) == false); 286 assert(index == 6); 287 288 assert(args[1][0 .. 9] == `\\foo bar`); 289 assert(args[2][0 .. 7] == `\\\\foo`); 290 assert(args[3][0 .. 5] == `\"foo`); 291 assert(args[4][0 .. 4] == `"foo`); 292 assert(args[5][0 .. 1] == `"`); 293 294 index = 7; 295 testData = "\t@recurse # comment\r\ntab\t\"@recurse\"\x1A after end\0".dup; 296 assert(insertArgumentsFromResponse(testData, args, index) == true); 297 assert(index == 10); 298 assert(args[7][0 .. 8] == "@recurse"); 299 assert(args[8][0 .. 3] == "tab"); 300 assert(args[9][0 .. 8] == "@recurse"); 301 } 302 303 unittest 304 { 305 auto args = Strings(0); 306 307 char[] testData = "\x1A".dup; 308 size_t index = 0; 309 assert(insertArgumentsFromResponse(testData, args, index) == false); 310 assert(index == 0); 311 312 testData = "@\r".dup; 313 assert(insertArgumentsFromResponse(testData, args, index) == true); 314 assert(index == 1); 315 assert(args[0][0 .. 2] == "@\0"); 316 317 testData = "ä&#\0".dup; 318 assert(insertArgumentsFromResponse(testData, args, index) == false); 319 assert(index == 2); 320 assert(args[1][0 .. 5] == "ä&#\0"); 321 322 testData = "one@\"word \0".dup; 323 assert(insertArgumentsFromResponse(testData, args, index) == false); 324 args[0] = "one@\"word"; 325 } 326 327 /********************************* 328 * Try to resolve the null-terminated string cp to a null-terminated char[]. 329 * 330 * The name is first searched for in the environment. If it is not 331 * there, it is searched for as a file name. 332 * 333 * Params: 334 * cp = null-terminated string to look resolve 335 * 336 * Returns: 337 * a mutable, manually allocated array containing the contents of the environment 338 * variable or file, ending with a null-terminator. 339 * The null-terminator is inside the bounds of the array. 340 * If cp could not be resolved, null is returned. 341 */ 342 private char[] lookupInEnvironment(scope const(char)* cp) nothrow { 343 344 import core.stdc.stdlib: getenv; 345 import core.stdc.string: strlen; 346 import dmd.root.rmem: mem; 347 348 if (auto p = getenv(cp)) 349 { 350 char* buffer = mem.xstrdup(p); 351 return buffer[0 .. strlen(buffer) + 1]; // include null-terminator 352 } 353 else 354 { 355 auto readResult = File.read(cp); 356 if (!readResult.success) 357 return null; 358 // take ownership of buffer (leaking) 359 return cast(char[]) readResult.extractDataZ(); 360 } 361 }