view libirccd/irccd/ini.cpp @ 809:8460b4a34191

misc: reorganize namespaces, closes #952 @4h
author David Demelier <markand@malikania.fr>
date Fri, 16 Nov 2018 12:25:00 +0100
parents libirccd-core/irccd/ini.cpp@8c44bbcbbab9
children 49fa22f0b4b9
line wrap: on
line source

/*
 * ini.cpp -- extended .ini file parser
 *
 * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <cassert>
#include <cctype>
#include <cstring>
#include <iostream>
#include <iterator>
#include <fstream>
#include <sstream>
#include <stdexcept>

// for PathIsRelative.
#if defined(_WIN32)
#  include <Shlwapi.h>
#endif

#include "ini.hpp"

using namespace std::string_literals;

namespace irccd::ini {

namespace {

using stream_iterator = std::istreambuf_iterator<char>;
using token_iterator = std::vector<token>::const_iterator;

auto is_absolute(const std::string& path) noexcept -> bool
{
#if defined(_WIN32)
	return !PathIsRelative(path.c_str());
#else
	return path.size() > 0 && path[0] == '/';
#endif
}

auto is_quote(char c) noexcept -> bool
{
	return c == '\'' || c == '"';
}

auto is_space(char c) noexcept -> bool
{
	// Custom version because std::isspace includes \n as space.
	return c == ' ' || c == '\t';
}

auto is_list(char c) noexcept -> bool
{
	return c == '(' || c == ')' || c == ',';
}

auto is_reserved(char c) noexcept -> bool
{
	return is_list(c) || is_quote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '=';
}

void analyse_line(unsigned& line, unsigned& column, stream_iterator& it) noexcept
{
	assert(*it == '\n');

	++ line;
	++ it;
	column = 0;
}

void analyse_comment(unsigned& column, stream_iterator& it, stream_iterator end) noexcept
{
	assert(*it == '#');

	while (it != end && *it != '\n') {
		++ column;
		++ it;
	}
}

void analyse_spaces(unsigned& column, stream_iterator& it, stream_iterator end) noexcept
{
	assert(is_space(*it));

	while (it != end && is_space(*it)) {
		++ column;
		++ it;
	}
}

void analyse_list(tokens& list, unsigned line, unsigned& column, stream_iterator& it) noexcept
{
	assert(is_list(*it));

	switch (*it++) {
	case '(':
		list.emplace_back(token::list_begin, line, column++);
		break;
	case ')':
		list.emplace_back(token::list_end, line, column++);
		break;
	case ',':
		list.emplace_back(token::comma, line, column++);
		break;
	default:
		break;
	}
}

void analyse_section(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
{
	assert(*it == '[');

	std::string value;
	unsigned save = column;

	// Read section name.
	++ it;
	while (it != end && *it != ']') {
		if (*it == '\n')
			throw exception(line, column, "section not terminated, missing ']'");
		if (is_reserved(*it))
			throw exception(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'");

		++ column;
		value += *it++;
	}

	if (it == end)
		throw exception(line, column, "section name expected after '[', got <EOF>");
	if (value.empty())
		throw exception(line, column, "empty section name");

	// Remove ']'.
	++ it;

	list.emplace_back(token::section, line, save, std::move(value));
}

void analyse_assign(tokens& list, unsigned& line, unsigned& column, stream_iterator& it)
{
	assert(*it == '=');

	list.push_back({ token::assign, line, column++ });
	++ it;
}

void analyse_quoted_word(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
{
	std::string value;
	unsigned save = column;
	char quote = *it++;

	while (it != end && *it != quote) {
		// TODO: escape sequence
		++ column;
		value += *it++;
	}

	if (it == end)
		throw exception(line, column, "undisclosed '" + std::string(1, quote) + "', got <EOF>");

	// Remove quote.
	++ it;

	list.push_back({ token::quoted_word, line, save, std::move(value) });
}

void analyse_word(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
{
	assert(!is_reserved(*it));

	std::string value;
	unsigned save = column;

	while (it != end && !std::isspace(*it) && !is_reserved(*it)) {
		++ column;
		value += *it++;
	}

	list.push_back({ token::word, line, save, std::move(value) });
}

void analyse_include(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
{
	assert(*it == '@');

	std::string include;
	unsigned save = column;

	// Read include.
	++ it;
	while (it != end && !is_space(*it)) {
		++ column;
		include += *it++;
	}

	if (include == "include")
		list.push_back({ token::include, line, save });
	else if (include == "tryinclude")
		list.push_back({ token::tryinclude, line, save });
	else
		throw exception(line, column, "expected include or tryinclude after '@' token");
}

void parse_option_value_simple(option& option, token_iterator& it)
{
	assert(it->get_type() == token::word || it->get_type() == token::quoted_word);

	option.push_back((it++)->get_value());
}

void parse_option_value_list(option& option, token_iterator& it, token_iterator end)
{
	assert(it->get_type() == token::list_begin);

	token_iterator save = it++;

	while (it != end && it->get_type() != token::list_end) {
		switch (it->get_type()) {
		case token::comma:
			// Previous must be a word.
			if (it[-1].get_type() != token::word && it[-1].get_type() != token::quoted_word)
				throw exception(it->get_line(), it->get_column(),
				                "unexpected comma after '"s + it[-1].get_value() + "'");

			++ it;
			break;
		case token::word:
		case token::quoted_word:
			option.push_back((it++)->get_value());
			break;
		default:
			throw exception(it->get_line(), it->get_column(), "unexpected '"s + it[-1].get_value() + "' in list construct");
			break;
		}
	}

	if (it == end)
		throw exception(save->get_line(), save->get_column(), "unterminated list construct");

	// Remove ).
	++ it;
}

void parse_option(section& sc, token_iterator& it, token_iterator end)
{
	option option(it->get_value());
	token_iterator save(it);

	// No '=' or something else?
	if (++it == end)
		throw exception(save->get_line(), save->get_column(), "expected '=' assignment, got <EOF>");
	if (it->get_type() != token::assign)
		throw exception(it->get_line(), it->get_column(), "expected '=' assignment, got " + it->get_value());

	// Empty options are allowed so just test for words.
	if (++it != end) {
		if (it->get_type() == token::word || it->get_type() == token::quoted_word)
			parse_option_value_simple(option, it);
		else if (it->get_type() == token::list_begin)
			parse_option_value_list(option, it, end);
	}

	sc.push_back(std::move(option));
}

void parse_include(document& doc, const std::string& path, token_iterator& it, token_iterator end, bool required)
{
	token_iterator save(it);

	if (++it == end)
		throw exception(save->get_line(), save->get_column(), "expected file name after '@include' statement, got <EOF>");
	if (it->get_type() != token::word && it->get_type() != token::quoted_word)
		throw exception(it->get_line(), it->get_column(),
		                "expected file name after '@include' statement, got "s + it->get_value());

	std::string value = (it++)->get_value();
	std::string file;

	if (!is_absolute(value)) {
#if defined(_WIN32)
		file = path + "\\" + value;
#else
		file = path + "/" + value;
#endif
	} else
		file = value;

	try {
		/*
		 * If required is set to true, we have @include, otherwise the non-fatal
		 * @tryinclude keyword.
		 */
		for (const auto& sc : read_file(file))
			doc.push_back(sc);
	} catch (...) {
		if (required)
			throw;
	}
}

void parse_section(document& doc, token_iterator& it, token_iterator end)
{
	section sc(it->get_value());

	// Skip [section].
	++ it;

	// Read until next section.
	while (it != end && it->get_type() != token::section) {
		if (it->get_type() != token::word)
			throw exception(it->get_line(), it->get_column(),
			                "unexpected token '"s + it->get_value() + "' in section definition");

		parse_option(sc, it, end);
	}

	doc.push_back(std::move(sc));
}

} // !namespace

exception::exception(unsigned line, unsigned column, std::string msg) noexcept
	: line_(line)
	, column_(column)
	, message_(std::move(msg))
{
}

auto exception::line() const noexcept -> unsigned
{
	return line_;
}

auto exception::column() const noexcept -> unsigned
{
	return column_;
}

auto exception::what() const noexcept -> const char*
{
	return message_.c_str();
}

token::token(type type, unsigned line, unsigned column, std::string value) noexcept
	: type_(type)
	, line_(line)
	, column_(column)
{
	switch (type) {
	case include:
		value_ = "@include";
		break;
	case tryinclude:
		value_ = "@tryinclude";
		break;
	case section:
	case word:
	case quoted_word:
		value_ = value;
		break;
	case assign:
		value_ = "=";
		break;
	case list_begin:
		value_ = "(";
		break;
	case list_end:
		value_ = ")";
		break;
	case comma:
		value_ = ",";
		break;
	default:
		break;
	}
}

auto token::get_type() const noexcept -> type
{
	return type_;
}

auto token::get_line() const noexcept -> unsigned
{
	return line_;
}

auto token::get_column() const noexcept -> unsigned
{
	return column_;
}

auto token::get_value() const noexcept -> const std::string&
{
	return value_;
}

option::option(std::string key) noexcept
	: std::vector<std::string>()
	, key_(std::move(key))
{
	assert(!key_.empty());
}

option::option(std::string key, std::string value) noexcept
	: key_(std::move(key))
{
	assert(!key_.empty());

	push_back(std::move(value));
}

option::option(std::string key, std::vector<std::string> values) noexcept
	: std::vector<std::string>(std::move(values))
	, key_(std::move(key))
{
	assert(!key_.empty());
}

auto option::get_key() const noexcept -> const std::string&
{
	return key_;
}

auto option::get_value() const noexcept -> const std::string&
{
	static std::string dummy;

	return empty() ? dummy : (*this)[0];
}

section::section(std::string key) noexcept
	: key_(std::move(key))
{
	assert(!key_.empty());
}

auto section::get_key() const noexcept -> const std::string&
{
	return key_;
}

auto section::contains(std::string_view key) const noexcept -> bool
{
	return find(key) != end();
}

auto section::get(std::string_view key) const noexcept -> option
{
	auto it = find(key);

	if (it == end())
		return option(std::string(key));

	return *it;
}

auto section::find(std::string_view key) noexcept -> iterator
{
	return std::find_if(begin(), end(), [&] (const auto& o) {
		return o.get_key() == key;
	});
}

auto section::find(std::string_view key) const noexcept -> const_iterator
{
	return std::find_if(cbegin(), cend(), [&] (const auto& o) {
		return o.get_key() == key;
	});
}

auto section::operator[](std::string_view key) -> option&
{
	assert(contains(key));

	return *find(key);
}

auto section::operator[](std::string_view key) const -> const option&
{
	assert(contains(key));

	return *find(key);
}

auto document::contains(std::string_view key) const noexcept -> bool
{
	return find(key) != end();
}

auto document::get(std::string_view key) const noexcept -> section
{
	auto it = find(key);

	if (it == end())
		return section(std::string(key));

	return *it;
}

auto document::find(std::string_view key) noexcept -> iterator
{
	return std::find_if(begin(), end(), [&] (const auto& o) {
		return o.get_key() == key;
	});
}

auto document::find(std::string_view key) const noexcept -> const_iterator
{
	return std::find_if(cbegin(), cend(), [&] (const auto& o) {
		return o.get_key() == key;
	});
}

auto document::operator[](std::string_view key) -> section&
{
	assert(contains(key));

	return *find(key);
}

auto document::operator[](std::string_view key) const -> const section&
{
	assert(contains(key));

	return *find(key);
}

tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end)
{
	tokens list;
	unsigned line = 1;
	unsigned column = 0;

	while (it != end) {
		if (*it == '\n')
			analyse_line(line, column, it);
		else if (*it == '#')
			analyse_comment(column, it, end);
		else if (*it == '[')
			analyse_section(list, line, column, it, end);
		else if (*it == '=')
			analyse_assign(list, line, column, it);
		else if (is_space(*it))
			analyse_spaces(column, it, end);
		else if (*it == '@')
			analyse_include(list, line, column, it, end);
		else if (is_quote(*it))
			analyse_quoted_word(list, line, column, it, end);
		else if (is_list(*it))
			analyse_list(list, line, column, it);
		else
			analyse_word(list, line, column, it, end);
	}

	return list;
}

tokens analyse(std::istream& stream)
{
	return analyse(std::istreambuf_iterator<char>(stream), {});
}

document parse(const tokens& tokens, const std::string& path)
{
	document doc;
	token_iterator it = tokens.cbegin();
	token_iterator end = tokens.cend();

	while (it != end) {
		switch (it->get_type()) {
		case token::include:
			parse_include(doc, path, it, end, true);
			break;
		case token::tryinclude:
			parse_include(doc, path, it, end, false);
			break;
		case token::section:
			parse_section(doc, it, end);
			break;
		default:
			throw exception(it->get_line(), it->get_column(),
			                "unexpected '"s + it->get_value() + "' on root document");
		}
	}

	return doc;
}

document read_file(const std::string& filename)
{
	// Get parent path.
	auto parent = filename;
	auto pos = parent.find_last_of("/\\");

	if (pos != std::string::npos)
		parent.erase(pos);
	else
		parent = ".";

	std::ifstream input(filename);

	if (!input)
		throw exception(0, 0, std::strerror(errno));

	return parse(analyse(input), parent);
}

document read_string(const std::string& buffer)
{
	std::istringstream iss(buffer);

	return parse(analyse(iss));
}

void dump(const tokens& tokens)
{
	for (const token& token: tokens) {
		// TODO: add better description
		std::cout << token.get_line() << ":" << token.get_column() << ": " << token.get_value() << std::endl;
	}
}

} // !irccd::ini