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 }