ConfigFile.cxx 7.46 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2015 The Music Player Daemon Project
3
 * http://www.musicpd.org
Warren Dukes's avatar
Warren Dukes committed
4 5 6 7 8 9 10 11 12 13
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
14 15 16 17
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Warren Dukes's avatar
Warren Dukes committed
18 19
 */

20
#include "config.h"
21
#include "ConfigFile.hxx"
22
#include "Data.hxx"
23
#include "Param.hxx"
24
#include "Block.hxx"
25
#include "ConfigTemplates.hxx"
26
#include "util/Tokenizer.hxx"
27
#include "util/StringUtil.hxx"
28 29
#include "util/Error.hxx"
#include "util/Domain.hxx"
30
#include "fs/Path.hxx"
31 32
#include "fs/io/FileReader.hxx"
#include "fs/io/BufferedReader.hxx"
33
#include "Log.hxx"
34

35
#include <assert.h>
Warren Dukes's avatar
Warren Dukes committed
36

37
static constexpr char CONF_COMMENT = '#';
38

39 40
static constexpr Domain config_file_domain("config_file");

41
static bool
42
config_read_name_value(ConfigBlock &block, char *input, unsigned line,
43
		       Error &error)
44
{
45 46
	Tokenizer tokenizer(input);

47
	const char *name = tokenizer.NextWord(error);
48
	if (name == nullptr) {
49
		assert(!tokenizer.IsEnd());
50 51 52
		return false;
	}

53
	const char *value = tokenizer.NextString(error);
54
	if (value == nullptr) {
55
		if (tokenizer.IsEnd()) {
56
			error.Set(config_file_domain, "Value missing");
57
		} else {
58
			assert(error.IsDefined());
59 60 61 62 63
		}

		return false;
	}

64
	if (!tokenizer.IsEnd() && tokenizer.CurrentChar() != CONF_COMMENT) {
65
		error.Set(config_file_domain, "Unknown tokens after value");
66 67 68
		return false;
	}

69
	const BlockParam *bp = block.GetBlockParam(name);
70
	if (bp != nullptr) {
71 72 73
		error.Format(config_file_domain,
			     "\"%s\" is duplicate, first defined on line %i",
			     name, bp->line);
74 75 76
		return false;
	}

77
	block.AddBlockParam(name, value, line);
78
	return true;
79 80
}

81
static ConfigBlock *
82
config_read_block(BufferedReader &reader, Error &error)
Avuton Olrich's avatar
Avuton Olrich committed
83
{
84
	auto *ret = new ConfigBlock(reader.GetLineNumber());
85

86
	while (true) {
87
		char *line = reader.ReadLine();
88
		if (line == nullptr) {
89
			delete ret;
90 91 92 93

			if (reader.Check(error))
				error.Set(config_file_domain,
					  "Expected '}' before end-of-file");
94
			return nullptr;
95
		}
96

97
		line = StripLeft(line);
98 99
		if (*line == 0 || *line == CONF_COMMENT)
			continue;
100

101 102 103
		if (*line == '}') {
			/* end of this block; return from the function
			   (and from this "while" loop) */
104

105
			line = StripLeft(line + 1);
106
			if (*line != 0 && *line != CONF_COMMENT) {
107
				delete ret;
108
				error.Format(config_file_domain,
109 110
					     "line %y: Unknown tokens after '}'",
					     reader.GetLineNumber());
Max Kellermann's avatar
Max Kellermann committed
111
				return nullptr;
112
			}
113

114
			return ret;
115
		}
116

117
		/* parse name and value */
118

119
		if (!config_read_name_value(*ret, line, reader.GetLineNumber(),
120
					    error)) {
121
			assert(*line != 0);
122
			delete ret;
123
			error.FormatPrefix("line %u: ", reader.GetLineNumber());
124
			return nullptr;
125
		}
126
	}
127 128
}

129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
gcc_nonnull_all
static void
Append(ConfigBlock *&head, ConfigBlock *p)
{
	assert(p->next == nullptr);

	auto **i = &head;
	while (*i != nullptr)
		i = &(*i)->next;

	*i = p;
}

static bool
ReadConfigBlock(ConfigData &config_data, BufferedReader &reader,
144
		const char *name, ConfigBlockOption o,
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
		Tokenizer &tokenizer,
		Error &error)
{
	const unsigned i = unsigned(o);
	const ConfigTemplate &option = config_block_templates[i];
	ConfigBlock *&head = config_data.blocks[i];

	if (head != nullptr && !option.repeatable) {
		ConfigBlock *block = head;
		error.Format(config_file_domain,
			     "config parameter \"%s\" is first defined "
			     "on line %d and redefined on line %u\n",
			     name, block->line,
			     reader.GetLineNumber());
		return false;
	}

	/* now parse the block or the value */

	if (tokenizer.CurrentChar() != '{') {
		error.Format(config_file_domain,
			     "line %u: '{' expected",
			     reader.GetLineNumber());
		return false;
	}

	char *line = StripLeft(tokenizer.Rest() + 1);
	if (*line != 0 && *line != CONF_COMMENT) {
		error.Format(config_file_domain,
			     "line %u: Unknown tokens after '{'",
			     reader.GetLineNumber());
		return false;
	}

	auto *param = config_read_block(reader, error);
	if (param == nullptr)
		return false;

	Append(head, param);
	return true;
}

187 188 189 190 191 192 193 194 195 196 197 198 199
gcc_nonnull_all
static void
Append(config_param *&head, config_param *p)
{
	assert(p->next == nullptr);

	config_param **i = &head;
	while (*i != nullptr)
		i = &(*i)->next;

	*i = p;
}

200
static bool
201 202 203 204
ReadConfigParam(ConfigData &config_data, BufferedReader &reader,
		const char *name, ConfigOption o,
		Tokenizer &tokenizer,
		Error &error)
Avuton Olrich's avatar
Avuton Olrich committed
205
{
206
	const unsigned i = unsigned(o);
207
	const ConfigTemplate &option = config_param_templates[i];
208 209 210 211 212 213 214 215 216 217 218 219 220 221
	config_param *&head = config_data.params[i];

	if (head != nullptr && !option.repeatable) {
		struct config_param *param = head;
		error.Format(config_file_domain,
			     "config parameter \"%s\" is first defined "
			     "on line %d and redefined on line %u\n",
			     name, param->line,
			     reader.GetLineNumber());
		return false;
	}

	/* now parse the block or the value */

222 223 224
	const char *value = tokenizer.NextString(error);
	if (value == nullptr) {
		if (tokenizer.IsEnd())
225
			error.Format(config_file_domain,
226
				     "line %u: Value missing",
227
				     reader.GetLineNumber());
228 229 230
		else
			error.FormatPrefix("line %u: ",
					   reader.GetLineNumber());
231

232 233
		return false;
	}
234

235 236 237 238 239 240
	if (!tokenizer.IsEnd() &&
	    tokenizer.CurrentChar() != CONF_COMMENT) {
		error.Format(config_file_domain,
			     "line %u: Unknown tokens after value",
			     reader.GetLineNumber());
		return false;
241 242
	}

243
	auto *param = new config_param(value, reader.GetLineNumber());
244 245 246 247 248 249 250
	Append(head, param);
	return true;
}

static bool
ReadConfigFile(ConfigData &config_data, BufferedReader &reader, Error &error)
{
251 252 253 254 255 256
	while (true) {
		char *line = reader.ReadLine();
		if (line == nullptr)
			return true;

		line = StripLeft(line);
257 258
		if (*line == 0 || *line == CONF_COMMENT)
			continue;
259

260 261
		/* the first token in each line is the name, followed
		   by either the value or '{' */
262

263
		Tokenizer tokenizer(line);
264
		const char *name = tokenizer.NextWord(error);
265
		if (name == nullptr) {
266
			assert(!tokenizer.IsEnd());
267
			error.FormatPrefix("line %u: ", reader.GetLineNumber());
268
			return false;
269
		}
270

271 272
		/* get the definition of that option, and check the
		   "repeatable" flag */
273

274
		const ConfigOption o = ParseConfigOptionName(name);
275
		ConfigBlockOption bo;
276 277 278 279
		if (o != ConfigOption::MAX) {
			if (!ReadConfigParam(config_data, reader, name, o,
					     tokenizer, error))
				return false;
280
		} else if ((bo = ParseConfigBlockOptionName(name)) != ConfigBlockOption::MAX) {
281
			if (!ReadConfigBlock(config_data, reader, name, bo,
282 283
					     tokenizer, error))
				return false;
284
		} else {
285 286
			error.Format(config_file_domain,
				     "unrecognized parameter in config file at "
287 288
				     "line %u: %s\n",
				     reader.GetLineNumber(), name);
289 290
			return false;
		}
Warren Dukes's avatar
Warren Dukes committed
291
	}
292
}
293 294

bool
295
ReadConfigFile(ConfigData &config_data, Path path, Error &error)
296 297 298 299
{
	assert(!path.IsNull());
	const std::string path_utf8 = path.ToUTF8();

300
	FormatDebug(config_file_domain, "loading file %s", path_utf8.c_str());
301

302 303
	FileReader file(path, error);
	if (!file.IsDefined())
304 305
		return false;

306 307 308
	BufferedReader reader(file);
	return ReadConfigFile(config_data, reader, error) &&
		reader.Check(error);
309
}