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-2023 by The D Language Foundation, All Rights Reserved
7  *              Some portions copyright (c) 1994-1995 by Symantec
8  * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
9  * License:     $(LINK2 https://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  *     `null` on success, or the first response file that could not be found
52  */
53 const(char)* 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.length;)
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         {
71             /* error         */
72             /* BUG: any file buffers are not free'd   */
73             return cp;
74         }
75 
76         recurse = insertArgumentsFromResponse(buffer, args, i) || recurse;
77     }
78     if (recurse)
79     {
80         /* Recursively expand @filename   */
81         if (auto missingFile = responseExpandFrom!lookup(args))
82             /* error         */
83             /* BUG: any file buffers are not free'd   */
84             return missingFile;
85     }
86     return null; /* success         */
87 }
88 
89 version (unittest)
90 {
91     char[] testEnvironment(const(char)* str) nothrow pure
92     {
93         import core.stdc.string: strlen;
94         import dmd.root.string : toDString;
95         switch (str.toDString())
96         {
97         case "Foo":
98             return "foo @Bar #\0".dup;
99         case "Bar":
100             return "bar @Nil\0".dup;
101         case "Error":
102             return "@phony\0".dup;
103         case "Nil":
104             return "\0".dup;
105         default:
106             return null;
107         }
108     }
109 }
110 
111 unittest
112 {
113     auto args = Strings(4);
114     args[0] = "first";
115     args[1] = "@Foo";
116     args[2] = "@Bar";
117     args[3] = "last";
118 
119     assert(responseExpand!testEnvironment(args) == null);
120     assert(args.length == 5);
121     assert(args[0][0 .. 6] == "first\0");
122     assert(args[1][0 .. 4] == "foo\0");
123     assert(args[2][0 .. 4] == "bar\0");
124     assert(args[3][0 .. 4] == "bar\0");
125     assert(args[4][0 .. 5] == "last\0");
126 }
127 
128 unittest
129 {
130     auto args = Strings(2);
131     args[0] = "@phony";
132     args[1] = "dummy";
133     assert(responseExpand!testEnvironment(args)[0..7] == "@phony\0");
134 }
135 
136 unittest
137 {
138     auto args = Strings(2);
139     args[0] = "@Foo";
140     args[1] = "@Error";
141     assert(responseExpand!testEnvironment(args)[0..7] == "@phony\0");
142 }
143 
144 /*********************************
145  * Take the contents of a response-file 'buffer', parse it and put the resulting
146  * arguments in 'args' at 'argIndex'. 'argIndex' will be updated to point just
147  * after the inserted arguments.
148  * The logic of this should match that in setargv()
149  *
150  * Params:
151  *     buffer = mutable string containing the response file
152  *     args = list of arguments
153  *     argIndex = position in 'args' where response arguments are inserted
154  *
155  * Returns:
156  *     true if another response argument was found
157  */
158 bool insertArgumentsFromResponse(char[] buffer, ref Strings args, ref size_t argIndex) nothrow pure
159 {
160     bool recurse = false;
161     bool comment = false;
162 
163     for (size_t p = 0; p < buffer.length; p++)
164     {
165         //char* d;
166         size_t d = 0;
167         char c, lastc;
168         bool instring;
169         int numSlashes, nonSlashes;
170         switch (buffer[p])
171         {
172         case 26:
173             /* ^Z marks end of file      */
174             return recurse;
175         case '\r':
176         case '\n':
177             comment = false;
178             goto case;
179         case 0:
180         case ' ':
181         case '\t':
182             continue;
183             // scan to start of argument
184         case '#':
185             comment = true;
186             continue;
187         case '@':
188             if (comment)
189             {
190                 continue;
191             }
192             recurse = true;
193             goto default;
194         default:
195             /* start of new argument   */
196             if (comment)
197             {
198                 continue;
199             }
200             args.insert(argIndex, &buffer[p]);
201             ++argIndex;
202             instring = false;
203             c = 0;
204             numSlashes = 0;
205             for (d = p; 1; p++)
206             {
207                 lastc = c;
208                 if (p >= buffer.length)
209                 {
210                     buffer[d] = '\0';
211                     return recurse;
212                 }
213                 c = buffer[p];
214                 switch (c)
215                 {
216                 case '"':
217                     /*
218                     Yes this looks strange,but this is so that we are
219                     MS Compatible, tests have shown that:
220                     \\\\"foo bar"  gets passed as \\foo bar
221                     \\\\foo  gets passed as \\\\foo
222                     \\\"foo gets passed as \"foo
223                     and \"foo gets passed as "foo in VC!
224                     */
225                     nonSlashes = numSlashes % 2;
226                     numSlashes = numSlashes / 2;
227                     for (; numSlashes > 0; numSlashes--)
228                     {
229                         d--;
230                         buffer[d] = '\0';
231                     }
232                     if (nonSlashes)
233                     {
234                         buffer[d - 1] = c;
235                     }
236                     else
237                     {
238                         instring = !instring;
239                     }
240                     break;
241                 case 26:
242                     buffer[d] = '\0'; // terminate argument
243                     return recurse;
244                 case '\r':
245                     c = lastc;
246                     continue;
247                     // ignore
248                 case ' ':
249                 case '\t':
250                     if (!instring)
251                     {
252                     case '\n':
253                     case 0:
254                         buffer[d] = '\0'; // terminate argument
255                         goto Lnextarg;
256                     }
257                     goto default;
258                 default:
259                     if (c == '\\')
260                         numSlashes++;
261                     else
262                         numSlashes = 0;
263                     buffer[d++] = c;
264                     break;
265                 }
266             }
267         }
268     Lnextarg:
269     }
270     return recurse;
271 }
272 
273 unittest
274 {
275     auto args = Strings(4);
276     args[0] = "arg0";
277     args[1] = "arg1";
278     args[2] = "arg2";
279 
280     char[] testData = "".dup;
281     size_t index = 1;
282     assert(insertArgumentsFromResponse(testData, args, index) == false);
283     assert(index == 1);
284 
285     testData = (`\\\\"foo bar" \\\\foo \\\"foo \"foo "\"" # @comment`~'\0').dup;
286     assert(insertArgumentsFromResponse(testData, args, index) == false);
287     assert(index == 6);
288 
289     assert(args[1][0 .. 9] == `\\foo bar`);
290     assert(args[2][0 .. 7] == `\\\\foo`);
291     assert(args[3][0 .. 5] == `\"foo`);
292     assert(args[4][0 .. 4] == `"foo`);
293     assert(args[5][0 .. 1] == `"`);
294 
295     index = 7;
296     testData = "\t@recurse # comment\r\ntab\t\"@recurse\"\x1A after end\0".dup;
297     assert(insertArgumentsFromResponse(testData, args, index) == true);
298     assert(index == 10);
299     assert(args[7][0 .. 8] == "@recurse");
300     assert(args[8][0 .. 3] == "tab");
301     assert(args[9][0 .. 8] == "@recurse");
302 }
303 
304 unittest
305 {
306     auto args = Strings(0);
307 
308     char[] testData = "\x1A".dup;
309     size_t index = 0;
310     assert(insertArgumentsFromResponse(testData, args, index) == false);
311     assert(index == 0);
312 
313     testData = "@\r".dup;
314     assert(insertArgumentsFromResponse(testData, args, index) == true);
315     assert(index == 1);
316     assert(args[0][0 .. 2] == "@\0");
317 
318     testData = "ä&#\0".dup;
319     assert(insertArgumentsFromResponse(testData, args, index) == false);
320     assert(index == 2);
321     assert(args[1][0 .. 5] == "ä&#\0");
322 
323     testData = "one@\"word \0".dup;
324     assert(insertArgumentsFromResponse(testData, args, index) == false);
325     args[0] = "one@\"word";
326 }
327 
328 /*********************************
329  * Try to resolve the null-terminated string cp to a null-terminated char[].
330  *
331  * The name is first searched for in the environment. If it is not
332  * there, it is searched for as a file name.
333  *
334  * Params:
335  *     cp = null-terminated string to look resolve
336  *
337  * Returns:
338  *     a mutable, manually allocated array containing the contents of the environment
339  *     variable or file, ending with a null-terminator.
340  *     The null-terminator is inside the bounds of the array.
341  *     If cp could not be resolved, null is returned.
342  */
343 private char[] lookupInEnvironment(scope const(char)* cp) nothrow {
344 
345     import core.stdc.stdlib: getenv;
346     import core.stdc.string: strlen;
347     import dmd.root.rmem: mem;
348 
349     if (auto p = getenv(cp))
350     {
351         char* buffer = mem.xstrdup(p);
352         return buffer[0 .. strlen(buffer) + 1]; // include null-terminator
353     }
354     else
355     {
356         import dmd.root.string : toDString;
357         auto readResult = File.read(cp.toDString());
358         if (!readResult.success)
359             return null;
360         // take ownership of buffer (leaking)
361         return cast(char[]) readResult.extractDataZ();
362     }
363 }