1 /**
2 * OpenAL cross platform audio library
3 * Copyright (C) 1999-2007 by authors.
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public
15 * License along with this library; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 * Or go to http://www.gnu.org/copyleft/lgpl.html
19 */
20
21 #include "config.h"
22
23 #include "alconfig.h"
24
25 #include <cstdlib>
26 #include <cctype>
27 #include <cstring>
28 #ifdef _WIN32
29 #include <windows.h>
30 #include <shlobj.h>
31 #endif
32 #ifdef __APPLE__
33 #include <CoreFoundation/CoreFoundation.h>
34 #endif
35
36 #include <algorithm>
37 #include <cstdio>
38 #include <string>
39 #include <utility>
40
41 #include "alfstream.h"
42 #include "alstring.h"
43 #include "compat.h"
44 #include "core/logging.h"
45 #include "strutils.h"
46 #include "vector.h"
47
48
49 namespace {
50
51 struct ConfigEntry {
52 std::string key;
53 std::string value;
54 };
55 al::vector<ConfigEntry> ConfOpts;
56
57
lstrip(std::string & line)58 std::string &lstrip(std::string &line)
59 {
60 size_t pos{0};
61 while(pos < line.length() && std::isspace(line[pos]))
62 ++pos;
63 line.erase(0, pos);
64 return line;
65 }
66
readline(std::istream & f,std::string & output)67 bool readline(std::istream &f, std::string &output)
68 {
69 while(f.good() && f.peek() == '\n')
70 f.ignore();
71
72 return std::getline(f, output) && !output.empty();
73 }
74
expdup(const char * str)75 std::string expdup(const char *str)
76 {
77 std::string output;
78
79 std::string envval;
80 while(*str != '\0')
81 {
82 const char *addstr;
83 size_t addstrlen;
84
85 if(str[0] != '$')
86 {
87 const char *next = std::strchr(str, '$');
88 addstr = str;
89 addstrlen = next ? static_cast<size_t>(next-str) : std::strlen(str);
90
91 str += addstrlen;
92 }
93 else
94 {
95 str++;
96 if(*str == '$')
97 {
98 const char *next = std::strchr(str+1, '$');
99 addstr = str;
100 addstrlen = next ? static_cast<size_t>(next-str) : std::strlen(str);
101
102 str += addstrlen;
103 }
104 else
105 {
106 const bool hasbraces{(*str == '{')};
107
108 if(hasbraces) str++;
109 const char *envstart = str;
110 while(std::isalnum(*str) || *str == '_')
111 ++str;
112 if(hasbraces && *str != '}')
113 continue;
114 const std::string envname{envstart, str};
115 if(hasbraces) str++;
116
117 envval = al::getenv(envname.c_str()).value_or(std::string{});
118 addstr = envval.data();
119 addstrlen = envval.length();
120 }
121 }
122 if(addstrlen == 0)
123 continue;
124
125 output.append(addstr, addstrlen);
126 }
127
128 return output;
129 }
130
LoadConfigFromFile(std::istream & f)131 void LoadConfigFromFile(std::istream &f)
132 {
133 std::string curSection;
134 std::string buffer;
135
136 while(readline(f, buffer))
137 {
138 if(lstrip(buffer).empty())
139 continue;
140
141 if(buffer[0] == '[')
142 {
143 char *line{&buffer[0]};
144 char *section = line+1;
145 char *endsection;
146
147 endsection = std::strchr(section, ']');
148 if(!endsection || section == endsection)
149 {
150 ERR(" config parse error: bad line \"%s\"\n", line);
151 continue;
152 }
153 if(endsection[1] != 0)
154 {
155 char *end = endsection+1;
156 while(std::isspace(*end))
157 ++end;
158 if(*end != 0 && *end != '#')
159 {
160 ERR(" config parse error: bad line \"%s\"\n", line);
161 continue;
162 }
163 }
164 *endsection = 0;
165
166 curSection.clear();
167 if(al::strcasecmp(section, "general") != 0)
168 {
169 do {
170 char *nextp = std::strchr(section, '%');
171 if(!nextp)
172 {
173 curSection += section;
174 break;
175 }
176
177 curSection.append(section, nextp);
178 section = nextp;
179
180 if(((section[1] >= '0' && section[1] <= '9') ||
181 (section[1] >= 'a' && section[1] <= 'f') ||
182 (section[1] >= 'A' && section[1] <= 'F')) &&
183 ((section[2] >= '0' && section[2] <= '9') ||
184 (section[2] >= 'a' && section[2] <= 'f') ||
185 (section[2] >= 'A' && section[2] <= 'F')))
186 {
187 int b{0};
188 if(section[1] >= '0' && section[1] <= '9')
189 b = (section[1]-'0') << 4;
190 else if(section[1] >= 'a' && section[1] <= 'f')
191 b = (section[1]-'a'+0xa) << 4;
192 else if(section[1] >= 'A' && section[1] <= 'F')
193 b = (section[1]-'A'+0x0a) << 4;
194 if(section[2] >= '0' && section[2] <= '9')
195 b |= (section[2]-'0');
196 else if(section[2] >= 'a' && section[2] <= 'f')
197 b |= (section[2]-'a'+0xa);
198 else if(section[2] >= 'A' && section[2] <= 'F')
199 b |= (section[2]-'A'+0x0a);
200 curSection += static_cast<char>(b);
201 section += 3;
202 }
203 else if(section[1] == '%')
204 {
205 curSection += '%';
206 section += 2;
207 }
208 else
209 {
210 curSection += '%';
211 section += 1;
212 }
213 } while(*section != 0);
214 }
215
216 continue;
217 }
218
219 auto cmtpos = std::min(buffer.find('#'), buffer.size());
220 while(cmtpos > 0 && std::isspace(buffer[cmtpos-1]))
221 --cmtpos;
222 if(!cmtpos) continue;
223 buffer.erase(cmtpos);
224
225 auto sep = buffer.find('=');
226 if(sep == std::string::npos)
227 {
228 ERR(" config parse error: malformed option line: \"%s\"\n", buffer.c_str());
229 continue;
230 }
231 auto keyend = sep++;
232 while(keyend > 0 && std::isspace(buffer[keyend-1]))
233 --keyend;
234 if(!keyend)
235 {
236 ERR(" config parse error: malformed option line: \"%s\"\n", buffer.c_str());
237 continue;
238 }
239 while(sep < buffer.size() && std::isspace(buffer[sep]))
240 sep++;
241
242 std::string fullKey;
243 if(!curSection.empty())
244 {
245 fullKey += curSection;
246 fullKey += '/';
247 }
248 fullKey += buffer.substr(0u, keyend);
249
250 std::string value{(sep < buffer.size()) ? buffer.substr(sep) : std::string{}};
251 if(value.size() > 1)
252 {
253 if((value.front() == '"' && value.back() == '"')
254 || (value.front() == '\'' && value.back() == '\''))
255 {
256 value.pop_back();
257 value.erase(value.begin());
258 }
259 }
260
261 TRACE(" found '%s' = '%s'\n", fullKey.c_str(), value.c_str());
262
263 /* Check if we already have this option set */
264 auto find_key = [&fullKey](const ConfigEntry &entry) -> bool
265 { return entry.key == fullKey; };
266 auto ent = std::find_if(ConfOpts.begin(), ConfOpts.end(), find_key);
267 if(ent != ConfOpts.end())
268 {
269 if(!value.empty())
270 ent->value = expdup(value.c_str());
271 else
272 ConfOpts.erase(ent);
273 }
274 else if(!value.empty())
275 ConfOpts.emplace_back(ConfigEntry{std::move(fullKey), expdup(value.c_str())});
276 }
277 ConfOpts.shrink_to_fit();
278 }
279
280 } // namespace
281
282
283 #ifdef _WIN32
ReadALConfig()284 void ReadALConfig()
285 {
286 WCHAR buffer[MAX_PATH];
287 if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE)
288 {
289 std::string filepath{wstr_to_utf8(buffer)};
290 filepath += "\\alsoft.ini";
291
292 TRACE("Loading config %s...\n", filepath.c_str());
293 al::ifstream f{filepath};
294 if(f.is_open())
295 LoadConfigFromFile(f);
296 }
297
298 std::string ppath{GetProcBinary().path};
299 if(!ppath.empty())
300 {
301 ppath += "\\alsoft.ini";
302 TRACE("Loading config %s...\n", ppath.c_str());
303 al::ifstream f{ppath};
304 if(f.is_open())
305 LoadConfigFromFile(f);
306 }
307
308 if(auto confpath = al::getenv(L"ALSOFT_CONF"))
309 {
310 TRACE("Loading config %s...\n", wstr_to_utf8(confpath->c_str()).c_str());
311 al::ifstream f{*confpath};
312 if(f.is_open())
313 LoadConfigFromFile(f);
314 }
315 }
316
317 #else
318
ReadALConfig()319 void ReadALConfig()
320 {
321 const char *str{"/usr/local/etc/openal-soft/alsoft.conf"};
322
323 TRACE("Loading config %s...\n", str);
324 al::ifstream f{str};
325 if(f.is_open())
326 LoadConfigFromFile(f);
327 f.close();
328
329 std::string confpaths{al::getenv("XDG_CONFIG_DIRS").value_or("/etc/xdg")};
330 /* Go through the list in reverse, since "the order of base directories
331 * denotes their importance; the first directory listed is the most
332 * important". Ergo, we need to load the settings from the later dirs
333 * first so that the settings in the earlier dirs override them.
334 */
335 std::string fname;
336 while(!confpaths.empty())
337 {
338 auto next = confpaths.find_last_of(':');
339 if(next < confpaths.length())
340 {
341 fname = confpaths.substr(next+1);
342 confpaths.erase(next);
343 }
344 else
345 {
346 fname = confpaths;
347 confpaths.clear();
348 }
349
350 if(fname.empty() || fname.front() != '/')
351 WARN("Ignoring XDG config dir: %s\n", fname.c_str());
352 else
353 {
354 if(fname.back() != '/') fname += "/alsoft.conf";
355 else fname += "alsoft.conf";
356
357 TRACE("Loading config %s...\n", fname.c_str());
358 f = al::ifstream{fname};
359 if(f.is_open())
360 LoadConfigFromFile(f);
361 }
362 fname.clear();
363 }
364
365 #ifdef __APPLE__
366 CFBundleRef mainBundle = CFBundleGetMainBundle();
367 if(mainBundle)
368 {
369 unsigned char fileName[PATH_MAX];
370 CFURLRef configURL;
371
372 if((configURL=CFBundleCopyResourceURL(mainBundle, CFSTR(".alsoftrc"), CFSTR(""), nullptr)) &&
373 CFURLGetFileSystemRepresentation(configURL, true, fileName, sizeof(fileName)))
374 {
375 f = al::ifstream{reinterpret_cast<char*>(fileName)};
376 if(f.is_open())
377 LoadConfigFromFile(f);
378 }
379 }
380 #endif
381
382 if(auto homedir = al::getenv("HOME"))
383 {
384 fname = *homedir;
385 if(fname.back() != '/') fname += "/.alsoftrc";
386 else fname += ".alsoftrc";
387
388 TRACE("Loading config %s...\n", fname.c_str());
389 f = al::ifstream{fname};
390 if(f.is_open())
391 LoadConfigFromFile(f);
392 }
393
394 if(auto configdir = al::getenv("XDG_CONFIG_HOME"))
395 {
396 fname = *configdir;
397 if(fname.back() != '/') fname += "/alsoft.conf";
398 else fname += "alsoft.conf";
399 }
400 else
401 {
402 fname.clear();
403 if(auto homedir = al::getenv("HOME"))
404 {
405 fname = *homedir;
406 if(fname.back() != '/') fname += "/.config/alsoft.conf";
407 else fname += ".config/alsoft.conf";
408 }
409 }
410 if(!fname.empty())
411 {
412 TRACE("Loading config %s...\n", fname.c_str());
413 f = al::ifstream{fname};
414 if(f.is_open())
415 LoadConfigFromFile(f);
416 }
417
418 std::string ppath{GetProcBinary().path};
419 if(!ppath.empty())
420 {
421 if(ppath.back() != '/') ppath += "/alsoft.conf";
422 else ppath += "alsoft.conf";
423
424 TRACE("Loading config %s...\n", ppath.c_str());
425 f = al::ifstream{ppath};
426 if(f.is_open())
427 LoadConfigFromFile(f);
428 }
429
430 if(auto confname = al::getenv("ALSOFT_CONF"))
431 {
432 TRACE("Loading config %s...\n", confname->c_str());
433 f = al::ifstream{*confname};
434 if(f.is_open())
435 LoadConfigFromFile(f);
436 }
437 }
438 #endif
439
GetConfigValue(const char * devName,const char * blockName,const char * keyName,const char * def)440 const char *GetConfigValue(const char *devName, const char *blockName, const char *keyName, const char *def)
441 {
442 if(!keyName)
443 return def;
444
445 std::string key;
446 if(blockName && al::strcasecmp(blockName, "general") != 0)
447 {
448 key = blockName;
449 if(devName)
450 {
451 key += '/';
452 key += devName;
453 }
454 key += '/';
455 key += keyName;
456 }
457 else
458 {
459 if(devName)
460 {
461 key = devName;
462 key += '/';
463 }
464 key += keyName;
465 }
466
467 auto iter = std::find_if(ConfOpts.cbegin(), ConfOpts.cend(),
468 [&key](const ConfigEntry &entry) -> bool
469 { return entry.key == key; }
470 );
471 if(iter != ConfOpts.cend())
472 {
473 TRACE("Found %s = \"%s\"\n", key.c_str(), iter->value.c_str());
474 if(!iter->value.empty())
475 return iter->value.c_str();
476 return def;
477 }
478
479 if(!devName)
480 {
481 TRACE("Key %s not found\n", key.c_str());
482 return def;
483 }
484 return GetConfigValue(nullptr, blockName, keyName, def);
485 }
486
ConfigValueExists(const char * devName,const char * blockName,const char * keyName)487 int ConfigValueExists(const char *devName, const char *blockName, const char *keyName)
488 {
489 const char *val = GetConfigValue(devName, blockName, keyName, "");
490 return val[0] != 0;
491 }
492
ConfigValueStr(const char * devName,const char * blockName,const char * keyName)493 al::optional<std::string> ConfigValueStr(const char *devName, const char *blockName, const char *keyName)
494 {
495 const char *val = GetConfigValue(devName, blockName, keyName, "");
496 if(!val[0]) return al::nullopt;
497
498 return al::make_optional<std::string>(val);
499 }
500
ConfigValueInt(const char * devName,const char * blockName,const char * keyName)501 al::optional<int> ConfigValueInt(const char *devName, const char *blockName, const char *keyName)
502 {
503 const char *val = GetConfigValue(devName, blockName, keyName, "");
504 if(!val[0]) return al::nullopt;
505
506 return al::make_optional(static_cast<int>(std::strtol(val, nullptr, 0)));
507 }
508
ConfigValueUInt(const char * devName,const char * blockName,const char * keyName)509 al::optional<unsigned int> ConfigValueUInt(const char *devName, const char *blockName, const char *keyName)
510 {
511 const char *val = GetConfigValue(devName, blockName, keyName, "");
512 if(!val[0]) return al::nullopt;
513
514 return al::make_optional(static_cast<unsigned int>(std::strtoul(val, nullptr, 0)));
515 }
516
ConfigValueFloat(const char * devName,const char * blockName,const char * keyName)517 al::optional<float> ConfigValueFloat(const char *devName, const char *blockName, const char *keyName)
518 {
519 const char *val = GetConfigValue(devName, blockName, keyName, "");
520 if(!val[0]) return al::nullopt;
521
522 return al::make_optional(std::strtof(val, nullptr));
523 }
524
ConfigValueBool(const char * devName,const char * blockName,const char * keyName)525 al::optional<bool> ConfigValueBool(const char *devName, const char *blockName, const char *keyName)
526 {
527 const char *val = GetConfigValue(devName, blockName, keyName, "");
528 if(!val[0]) return al::nullopt;
529
530 return al::make_optional(
531 al::strcasecmp(val, "true") == 0 || al::strcasecmp(val, "yes") == 0 ||
532 al::strcasecmp(val, "on") == 0 || atoi(val) != 0);
533 }
534
GetConfigValueBool(const char * devName,const char * blockName,const char * keyName,int def)535 int GetConfigValueBool(const char *devName, const char *blockName, const char *keyName, int def)
536 {
537 const char *val = GetConfigValue(devName, blockName, keyName, "");
538
539 if(!val[0]) return def != 0;
540 return (al::strcasecmp(val, "true") == 0 || al::strcasecmp(val, "yes") == 0 ||
541 al::strcasecmp(val, "on") == 0 || atoi(val) != 0);
542 }
543