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