view modules/ini/ini.cpp @ 540:fd2ba28ac54b

Js: add dukx_(get|push)_object
author David Demelier <markand@malikania.fr>
date Wed, 08 Jun 2016 22:12:58 +0200
parents b604d3dd45b7
children f48bb09bccc7
line wrap: on
line source

/*
 * ini.cpp -- extended .ini file parser
 *
 * Copyright (c) 2013-2016 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 <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"

namespace {

using namespace ini;

using StreamIterator = std::istreambuf_iterator<char>;
using TokenIterator = std::vector<Token>::const_iterator;

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

inline bool isQuote(char c) noexcept
{
	return c == '\'' || c == '"';
}

inline bool isSpace(char c) noexcept
{
	/* Custom version because std::isspace includes \n as space */
	return c == ' ' || c == '\t';
}

inline bool isList(char c) noexcept
{
	return c == '(' || c == ')' || c == ',';
}

inline bool isReserved(char c) noexcept
{
	return isList(c) || isQuote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '=';
}

void analyseLine(int &line, int &column, StreamIterator &it) noexcept
{
	assert(*it == '\n');

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

void analyseComment(int &column, StreamIterator &it, StreamIterator end) noexcept
{
	assert(*it == '#');

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

void analyseSpaces(int &column, StreamIterator &it, StreamIterator end) noexcept
{
	assert(isSpace(*it));

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

void analyseList(Tokens &list, int line, int &column, StreamIterator &it) noexcept
{
	assert(isList(*it));

	switch (*it++) {
	case '(':
		list.emplace_back(Token::ListBegin, line, column++);
		break;
	case ')':
		list.emplace_back(Token::ListEnd, line, column++);
		break;
	case ',':
		list.emplace_back(Token::Comma, line, column++);
		break;
	default:
		break;
	}
}

void analyseSection(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
{
	assert(*it == '[');

	std::string value;
	int save = column;

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

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

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

	// Remove ']'.
	++ it;

	list.emplace_back(Token::Section, line, save, std::move(value));
}

void analyseAssign(Tokens &list, int &line, int &column, StreamIterator &it)
{
	assert(*it == '=');

	list.push_back({ Token::Assign, line, column++ });
	++ it;
}

void analyseQuotedWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
{
	std::string value;
	int save = column;
	char quote = *it++;

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

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

	// Remove quote.
	++ it;

	list.push_back({ Token::QuotedWord, line, save, std::move(value) });
}

void analyseWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
{
	assert(!isReserved(*it));

	std::string value;
	int save = column;

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

	list.push_back({ Token::Word, line, save, std::move(value) });
}

void analyseInclude(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end)
{
	assert(*it == '@');

	std::string include;
	int save = column;

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

	if (include != "include")
		throw Error(line, column, "expected include after '@' token");

	list.push_back({ Token::Include, line, save });
}

void parseOptionValueSimple(Option &option, TokenIterator &it)
{
	assert(it->type() == Token::Word || it->type() == Token::QuotedWord);

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

void parseOptionValueList(Option &option, TokenIterator &it, TokenIterator end)
{
	assert(it->type() == Token::ListBegin);

	TokenIterator save = it++;

	while (it != end && it->type() != Token::ListEnd) {
		switch (it->type()) {
		case Token::Comma:
			// Previous must be a word.
			if (it[-1].type() != Token::Word && it[-1].type() != Token::QuotedWord)
				throw Error(it->line(), it->column(), "unexpected comma after '" + it[-1].value() + "'");

			++ it;
			break;
		case Token::Word:
		case Token::QuotedWord:
			option.push_back((it++)->value());
			break;
		default:
			throw Error(it->line(), it->column(), "unexpected '" + it[-1].value() + "' in list construct");
			break;
		}
	}

	if (it == end)
		throw Error(save->line(), save->column(), "unterminated list construct");

	// Remove ).
	++ it;
}

void parseOption(Section &sc, TokenIterator &it, TokenIterator end)
{
	Option option(it->value());

	TokenIterator save = it;

	// No '=' or something else?
	if (++it == end)
		throw Error(save->line(), save->column(), "expected '=' assignment, got <EOF>");
	if (it->type() != Token::Assign)
		throw Error(it->line(), it->column(), "expected '=' assignment, got " + it->value());

	// Empty options are allowed so just test for words.
	if (++it != end) {
		if (it->type() == Token::Word || it->type() == Token::QuotedWord)
			parseOptionValueSimple(option, it);
		else if (it->type() == Token::ListBegin)
			parseOptionValueList(option, it, end);
	}

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

void parseInclude(Document &doc, const std::string &path, TokenIterator &it, TokenIterator end)
{
	TokenIterator save = it;

	if (++it == end)
		throw Error(save->line(), save->column(), "expected file name after '@include' statement, got <EOF>");
	if (it->type() != Token::Word && it->type() != Token::QuotedWord)
		throw Error(it->line(), it->column(), "expected file name after '@include' statement, got " + it->value());

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

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

	for (const auto &sc : readFile(file))
		doc.push_back(sc);
}

void parseSection(Document &doc, TokenIterator &it, TokenIterator end)
{
	Section sc(it->value());

	// Skip [section].
	++ it;

	// Read until next section.
	while (it != end && it->type() != Token::Section) {
		if (it->type() != Token::Word)
			throw Error(it->line(), it->column(), "unexpected token '" + it->value() + "' in section definition");

		parseOption(sc, it, end);
	}

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

} // !namespace

namespace ini {

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

	while (it != end) {
		if (*it == '\n')
			analyseLine(line, column, it);
		else if (*it == '#')
			analyseComment(column, it, end);
		else if (*it == '[')
			analyseSection(list, line, column, it, end);
		else if (*it == '=')
			analyseAssign(list, line, column, it);
		else if (isSpace(*it))
			analyseSpaces(column, it, end);
		else if (*it == '@')
			analyseInclude(list, line, column, it, end);
		else if (isQuote(*it))
			analyseQuotedWord(list, line, column, it, end);
		else if (isList(*it))
			analyseList(list, line, column, it);
		else
			analyseWord(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;
	TokenIterator it = tokens.cbegin();
	TokenIterator end = tokens.cend();

	while (it != end) {
		switch (it->type()) {
		case Token::Include:
			parseInclude(doc, path, it, end);
			break;
		case Token::Section:
			parseSection(doc, it, end);
			break;
		default:
			throw Error(it->line(), it->column(), "unexpected '" + it->value() + "' on root document");
		}
	}

	return doc;
}

Document readFile(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 Error(0, 0, std::strerror(errno));

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

Document readString(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.line() << ":" << token.column() << ": " << token.value() << std::endl;
}

} // !ini