1 /**
2  * Parses compiler settings from a .ini file.
3  *
4  * Copyright:   Copyright (C) 1994-1998 by Symantec
5  *              Copyright (C) 2000-2023 by The D Language Foundation, All Rights Reserved
6  * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
7  * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
8  * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/dinifile.d, _dinifile.d)
9  * Documentation:  https://dlang.org/phobos/dmd_dinifile.html
10  * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/dinifile.d
11  */
12 
13 module dmd.dinifile;
14 
15 import core.stdc.ctype;
16 import core.stdc.string;
17 import core.sys.posix.stdlib;
18 import core.sys.windows.winbase;
19 import core.sys.windows.windef;
20 
21 import dmd.errors;
22 import dmd.location;
23 import dmd.root.env;
24 import dmd.root.rmem;
25 import dmd.root.filename;
26 import dmd.common.outbuffer;
27 import dmd.root.port;
28 import dmd.root.string;
29 import dmd.root.stringtable;
30 
31 private enum LOG = false;
32 
33 /*****************************
34  * Find the config file
35  * Params:
36  *      argv0 = program name (argv[0])
37  *      inifile = .ini file name
38  * Returns:
39  *      file path of the config file or NULL
40  *      Note: this is a memory leak
41  */
42 const(char)[] findConfFile(const(char)[] argv0, const(char)[] inifile)
43 {
44     static if (LOG)
45     {
46         printf("findinifile(argv0 = '%.*s', inifile = '%.*s')\n",
47                cast(int)argv0.length, argv0.ptr, cast(int)inifile.length, inifile.ptr);
48     }
49     if (FileName.absolute(inifile))
50         return inifile;
51     if (FileName.exists(inifile))
52         return inifile;
53     /* Look for inifile in the following sequence of places:
54      *      o current directory
55      *      o home directory
56      *      o exe directory (windows)
57      *      o directory off of argv0
58      *      o SYSCONFDIR=/etc (non-windows)
59      */
60     auto filename = FileName.combine(getenv("HOME").toDString, inifile);
61     if (FileName.exists(filename))
62         return filename;
63 
64     version (Posix)
65     {
66         // Retry lookup in HOME with dot preceding inifile
67         filename = FileName.combine(getenv("HOME").toDString, '.' ~ inifile);
68         if (FileName.exists(filename))
69             return filename;
70     }
71 
72     version (Windows)
73     {
74         // This fix by Tim Matthews
75         char[MAX_PATH + 1] resolved_name;
76         const len = GetModuleFileNameA(null, resolved_name.ptr, MAX_PATH + 1);
77         if (len && FileName.exists(resolved_name[0 .. len]))
78         {
79             filename = FileName.replaceName(resolved_name[0 .. len], inifile);
80             if (FileName.exists(filename))
81                 return filename;
82         }
83     }
84     filename = FileName.replaceName(argv0, inifile);
85     if (FileName.exists(filename))
86         return filename;
87     version (Posix)
88     {
89         // Search PATH for argv0
90         const p = getenv("PATH");
91         static if (LOG)
92         {
93             printf("\tPATH='%s'\n", p);
94         }
95         auto abspath = FileName.searchPath(p, argv0, false);
96         if (abspath)
97         {
98             auto absname = FileName.replaceName(abspath, inifile);
99             if (FileName.exists(absname))
100                 return absname;
101         }
102         // Resolve symbolic links
103         filename = FileName.canonicalName(abspath ? abspath : argv0);
104         if (filename)
105         {
106             filename = FileName.replaceName(filename, inifile);
107             if (FileName.exists(filename))
108                 return filename;
109         }
110         // Search SYSCONFDIR=/etc for inifile
111         filename = FileName.combine(import("SYSCONFDIR.imp"), inifile);
112     }
113     return filename;
114 }
115 
116 /**********************************
117  * Read from environment, looking for cached value first.
118  * Params:
119  *      environment = cached copy of the environment
120  *      name = name to look for
121  * Returns:
122  *      environment value corresponding to name
123  */
124 const(char)* readFromEnv(const ref StringTable!(char*) environment, const(char)* name)
125 {
126     const len = strlen(name);
127     const sv = environment.lookup(name, len);
128     if (sv && sv.value)
129         return sv.value; // get cached value
130     return getenv(name);
131 }
132 
133 /*********************************
134  * Write to our copy of the environment, not the real environment
135  */
136 private bool writeToEnv(ref StringTable!(char*) environment, char* nameEqValue)
137 {
138     auto p = strchr(nameEqValue, '=');
139     if (!p)
140         return false;
141     auto sv = environment.update(nameEqValue, p - nameEqValue);
142     sv.value = p + 1;
143     return true;
144 }
145 
146 /************************************
147  * Update real environment with our copy.
148  * Params:
149  *      environment = our copy of the environment
150  */
151 void updateRealEnvironment(ref StringTable!(char*) environment)
152 {
153     foreach (sv; environment)
154     {
155         const name = sv.toDchars();
156         const value = sv.value;
157         if (!value) // deleted?
158             continue;
159         if (putenvRestorable(name.toDString, value.toDString))
160             assert(0);
161     }
162 }
163 
164 /*****************************
165  * Read and analyze .ini file.
166  * Write the entries into environment as
167  * well as any entries in one of the specified section(s).
168  *
169  * Params:
170  *      environment = our own cache of the program environment
171  *      filename = name of the file being parsed
172  *      path = what @P will expand to
173  *      buffer = contents of configuration file
174  *      sections = section names
175  */
176 void parseConfFile(ref StringTable!(char*) environment, const(char)[] filename, const(char)[] path, const(ubyte)[] buffer, const(Strings)* sections)
177 {
178     /********************
179      * Skip spaces.
180      */
181     static inout(char)* skipspace(inout(char)* p)
182     {
183         while (isspace(*p))
184             p++;
185         return p;
186     }
187 
188     // Parse into lines
189     bool envsection = true; // default is to read
190     OutBuffer buf;
191     bool eof = false;
192     int lineNum = 0;
193     for (size_t i = 0; i < buffer.length && !eof; i++)
194     {
195     Lstart:
196         const linestart = i;
197         for (; i < buffer.length; i++)
198         {
199             switch (buffer[i])
200             {
201             case '\r':
202                 break;
203             case '\n':
204                 // Skip if it was preceded by '\r'
205                 if (i && buffer[i - 1] == '\r')
206                 {
207                     i++;
208                     goto Lstart;
209                 }
210                 break;
211             case 0:
212             case 0x1A:
213                 eof = true;
214                 break;
215             default:
216                 continue;
217             }
218             break;
219         }
220         ++lineNum;
221         buf.setsize(0);
222         // First, expand the macros.
223         // Macros are bracketed by % characters.
224     Kloop:
225         for (size_t k = 0; k < i - linestart; ++k)
226         {
227             // The line is buffer[linestart..i]
228             const line = cast(const char*)&buffer[linestart];
229             if (line[k] == '%')
230             {
231                 foreach (size_t j; k + 1 .. i - linestart)
232                 {
233                     if (line[j] != '%')
234                         continue;
235                     if (j - k == 3 && Port.memicmp(&line[k + 1], "@P", 2) == 0)
236                     {
237                         // %@P% is special meaning the path to the .ini file
238                         auto p = path;
239                         if (!p.length)
240                             p = ".";
241                         buf.writestring(p);
242                     }
243                     else
244                     {
245                         auto len2 = j - k;
246                         auto p = cast(char*)Mem.check(malloc(len2));
247                         len2--;
248                         memcpy(p, &line[k + 1], len2);
249                         p[len2] = 0;
250                         Port.strupr(p);
251                         const penv = readFromEnv(environment, p);
252                         if (penv)
253                             buf.writestring(penv);
254                         free(p);
255                     }
256                     k = j;
257                     continue Kloop;
258                 }
259             }
260             buf.writeByte(line[k]);
261         }
262 
263         // Remove trailing spaces
264         const slice = buf[];
265         auto slicelen = slice.length;
266         while (slicelen && isspace(slice[slicelen - 1]))
267             --slicelen;
268         buf.setsize(slicelen);
269 
270         auto p = buf.peekChars();
271         // The expanded line is in p.
272         // Now parse it for meaning.
273         p = skipspace(p);
274         switch (*p)
275         {
276         case ';':
277             // comment
278         case 0:
279             // blank
280             break;
281         case '[':
282             // look for [Environment]
283             p = skipspace(p + 1);
284             char* pn;
285             for (pn = p; isalnum(*pn); pn++)
286             {
287             }
288             if (*skipspace(pn) != ']')
289             {
290                 // malformed [sectionname], so just say we're not in a section
291                 envsection = false;
292                 break;
293             }
294             /* Search sectionnamev[] for p..pn and set envsection to true if it's there
295              */
296             for (size_t j = 0; 1; ++j)
297             {
298                 if (j == sections.length)
299                 {
300                     // Didn't find it
301                     envsection = false;
302                     break;
303                 }
304                 const sectionname = (*sections)[j];
305                 const len = strlen(sectionname);
306                 if (pn - p == len && Port.memicmp(p, sectionname, len) == 0)
307                 {
308                     envsection = true;
309                     break;
310                 }
311             }
312             break;
313         default:
314             if (envsection)
315             {
316                 auto pn = p;
317                 // Convert name to upper case;
318                 // remove spaces bracketing =
319                 for (; *p; p++)
320                 {
321                     if (islower(*p))
322                         *p &= ~0x20;
323                     else if (isspace(*p))
324                     {
325                         memmove(p, p + 1, strlen(p));
326                         p--;
327                     }
328                     else if (p[0] == '?' && p[1] == '=')
329                     {
330                         *p = '\0';
331                         if (readFromEnv(environment, pn))
332                         {
333                             pn = null;
334                             break;
335                         }
336                         // remove the '?' and resume parsing starting from
337                         // '=' again so the regular variable format is
338                         // parsed
339                         memmove(p, p + 1, strlen(p + 1) + 1);
340                         p--;
341                     }
342                     else if (*p == '=')
343                     {
344                         p++;
345                         while (isspace(*p))
346                             memmove(p, p + 1, strlen(p));
347                         break;
348                     }
349                 }
350                 if (pn)
351                 {
352                     auto pns = cast(char*)Mem.check(strdup(pn));
353                     if (!writeToEnv(environment, pns))
354                     {
355                         const loc = Loc(filename.xarraydup.ptr, lineNum, 0); // TODO: use r-value when `error` supports it
356                         error(loc, "use `NAME=value` syntax, not `%s`", pn);
357                         fatal();
358                     }
359                     static if (LOG)
360                     {
361                         printf("\tputenv('%s')\n", pn);
362                         //printf("getenv(\"TEST\") = '%s'\n",getenv("TEST"));
363                     }
364                 }
365             }
366             break;
367         }
368     }
369 }