# HG changeset patch # User David Demelier # Date 1462908460 -7200 # Node ID c7fee63ccf926907d3b53049eb01fb62e828cb94 # Parent 2aecbd638b1c67549c8d51dd68b2db0c476f582f Irccd: new configuration mechanism, #397 diff -r 2aecbd638b1c -r c7fee63ccf92 irccd/main.cpp --- a/irccd/main.cpp Wed May 04 13:38:02 2016 +0200 +++ b/irccd/main.cpp Tue May 10 21:27:40 2016 +0200 @@ -16,24 +16,51 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include + +#if defined(HAVE_GETPID) +# include +# include +# include +# include +# include +#endif + +#if defined(HAVE_DAEMON) +# include +#endif + #include -#include +#include #include #include #include #include - #include #include +using namespace fmt::literals; + using namespace irccd; namespace { std::unique_ptr instance; +void usage() +{ + log::warning() << "usage: " << sys::programName() << " [options...]\n\n"; + log::warning() << "Available options:\n"; + log::warning() << " -c, --config file specify the configuration file\n"; + log::warning() << " -f, --foreground do not run as a daemon\n"; + log::warning() << " --help show this help\n"; + log::warning() << " -p, --plugin name load a specific plugin\n"; + log::warning() << " -v, --verbose be verbose" << std::endl; + std::exit(1); +} + void stop(int) { instance->stop(); @@ -41,16 +68,15 @@ void init(int &argc, char **&argv) { - // MOVE THIS IN Application - - /* Needed for some components */ + // Needed for some components. sys::setProgramName("irccd"); path::setApplicationPath(argv[0]); - /* Default logging to console */ + // Default logging to console. log::setVerbose(false); log::setInterface(std::make_unique()); + // Register some signals. signal(SIGINT, stop); signal(SIGTERM, stop); @@ -58,25 +84,9 @@ ++ argv; } -void usage() +parser::Result parse(int &argc, char **&argv) { - log::warning() << "usage: " << sys::programName() << " [options...]\n\n"; - log::warning() << "Available options:\n"; - log::warning() << " -c, --config file specify the configuration file\n"; - log::warning() << " -f, --foreground do not run as a daemon\n"; - log::warning() << " --help show this help\n"; - log::warning() << " -p, --plugin name load a specific plugin\n"; - log::warning() << " -v, --verbose be verbose" << std::endl; - std::exit(1); -} - -} // !namespace - -int main(int argc, char **argv) -{ - init(argc, argv); - - /* Parse command line options */ + // Parse command line options. parser::Result result; try { @@ -99,7 +109,7 @@ usage(); // NOTREACHED } - + if (pair.first == "-v" || pair.first == "--verbose") { log::setVerbose(true); } @@ -109,8 +119,153 @@ usage(); } + return result; +} + +Config open(const parser::Result &result) +{ + auto it = result.find("-c"); + + if (it != result.end() || (it = result.find("--config")) != result.end()) { + try { + return Config(it->second); + } catch (const std::exception &ex) { + throw std::runtime_error("{}: {}"_format(it->second, ex.what())); + } + } + + return Config::find(); +} + +void loadPid(const std::string &path) +{ + if (path.empty()) { + return; + } + + try { +#if defined(HAVE_GETPID) + std::ofstream out(path, std::ofstream::trunc); + + if (!out) { + throw std::runtime_error("irccd: could not open pidfile {}: {}"_format(path, std::strerror(errno))); + } + + log::debug() << "irccd: pid written in " << path << std::endl; + out << getpid() << std::endl; +#else + throw std::runtime_error("irccd: pidfile option not supported on this platform"); +#endif + } catch (const std::exception &ex) { + log::warning() << "irccd: " << ex.what() << std::endl; + } +} + +void loadGid(const std::string gid) +{ + try { + if (!gid.empty()) { +#if defined(HAVE_SETGID) + sys::setGid(gid); +#else + throw std::runtime_error("irccd: gid option not supported on this platform"); +#endif + } + } catch (const std::exception &ex) { + log::warning() << "irccd: " << ex.what() << std::endl; + } +} + +void loadUid(const std::string &uid) +{ + try { + if (!uid.empty()) { +#if defined(HAVE_SETUID) + sys::setUid(uid); +#else + throw std::runtime_error("irccd: uid option not supported on this platform"); +#endif + } + } catch (const std::exception &ex) { + log::warning() << "irccd: " << ex.what() << std::endl; + } +} + +void loadForeground(bool foreground, const parser::Result &options) +{ + try { +#if defined(HAVE_DAEMON) + if (options.count("-f") == 0 && options.count("--foreground") == 0 && !foreground) { + daemon(1, 0); + } +#endif + } catch (const std::exception &ex) { + log::warning() << "irccd: " << ex.what() << std::endl; + } +} + +void load(const Config &config, const parser::Result &options) +{ + /* + * Order matters, please be careful when changing this. + * + * 1. Open logs as early as possible to use the defined outputs on any loading errors. + */ + + // [logs] and [format] sections. + config.loadLogs(); + config.loadFormats(); + + // Show message here to use the formats. + log::info() << "irccd: using " << config.path() << std::endl; + + // [general] section. + loadPid(config.pidfile()); + loadGid(config.gid()); + loadUid(config.uid()); + loadForeground(config.isForeground(), options); + + // [transport] + for (const auto &transport : config.loadTransports()) { + instance->addTransport(transport); + } + + // [server] section. + for (const auto &server : config.loadServers()) { + instance->addServer(server); + } + + // [rule] section. + for (const auto &rule : config.loadRules()) { + instance->addRule(rule); + } + + // [plugin] section. + for (const auto &plugin : config.loadPlugins()) { + instance->addPlugin(plugin); + } +} + +} // !namespace + +int main(int argc, char **argv) +{ + init(argc, argv); + + parser::Result options = parse(argc, argv); + + // Find configuration file. instance = std::make_unique(); - instance->load(Config{result}); + + try { + Config cfg = open(options); + + load(cfg, options); + } catch (const std::exception &ex) { + log::warning() << "irccd: " << ex.what() << std::endl; + return 1; + } + instance->run(); return 0; diff -r 2aecbd638b1c -r c7fee63ccf92 lib/irccd/cmd-plugin-list.cpp --- a/lib/irccd/cmd-plugin-list.cpp Wed May 04 13:38:02 2016 +0200 +++ b/lib/irccd/cmd-plugin-list.cpp Tue May 10 21:27:40 2016 +0200 @@ -43,7 +43,7 @@ json::Value list = json::array({}); for (const auto &plugin : irccd.plugins()) { - list.append(plugin.first); + list.append(plugin->name()); } response.insert("list", std::move(list)); @@ -68,4 +68,4 @@ } // !command -} +} // !irccd diff -r 2aecbd638b1c -r c7fee63ccf92 lib/irccd/cmd-server-list.cpp --- a/lib/irccd/cmd-server-list.cpp Wed May 04 13:38:02 2016 +0200 +++ b/lib/irccd/cmd-server-list.cpp Tue May 10 21:27:40 2016 +0200 @@ -40,8 +40,8 @@ auto json = json::object({}); auto list = json::array({}); - for (const auto &pair : irccd.servers()) { - list.append(pair.first); + for (const auto &server : irccd.servers()) { + list.append(server->info().name); } json.insert("list", std::move(list)); diff -r 2aecbd638b1c -r c7fee63ccf92 lib/irccd/cmd-server-reconnect.cpp --- a/lib/irccd/cmd-server-reconnect.cpp Wed May 04 13:38:02 2016 +0200 +++ b/lib/irccd/cmd-server-reconnect.cpp Tue May 10 21:27:40 2016 +0200 @@ -50,8 +50,8 @@ if (server != request.end() && server->isString()) { irccd.requireServer(server->toString())->reconnect(); } else { - for (auto &pair : irccd.servers()) { - pair.second->reconnect(); + for (auto &server : irccd.servers()) { + server->reconnect(); } } diff -r 2aecbd638b1c -r c7fee63ccf92 lib/irccd/config.cpp --- a/lib/irccd/config.cpp Wed May 04 13:38:02 2016 +0200 +++ b/lib/irccd/config.cpp Tue May 10 21:27:40 2016 +0200 @@ -16,31 +16,20 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include "sysconfig.hpp" +#include -#if defined(HAVE_GETPID) -# include -# include -# include -# include -# include -#endif - -#if defined(HAVE_DAEMON) -# include -#endif +#include #include "config.hpp" -#include "ini.hpp" -#include "logger.hpp" +#include "fs.hpp" +#include "irccd.hpp" #include "path.hpp" -#include "sockets.hpp" -#include "system.hpp" +#include "rule.hpp" +#include "server.hpp" +#include "sysconfig.hpp" #include "util.hpp" -#include "irccd.hpp" -using namespace std; -using namespace std::string_literals; +using namespace fmt::literals; namespace irccd { @@ -83,101 +72,74 @@ } }; -} // !namespace - -void Config::loadGeneral(const ini::Document &config) const +std::string get(const ini::Document &doc, const std::string §ion, const std::string &key) { - ini::Document::const_iterator sc = config.find("general"); - ini::Section::const_iterator it; - -#if defined(HAVE_GETPID) - if (sc != config.end()) { - it = sc->find("pidfile"); - - if (it != sc->end() && !it->value().empty()) { - std::string path = it->value(); - std::ofstream out(path, std::ofstream::trunc); + auto its = doc.find(section); - if (!out) { - log::warning() << "irccd: could not open pidfile " << path << ": " << std::strerror(errno) << std::endl; - } else { - log::debug() << "irccd: pid written in " << path << std::endl; - out << getpid(); - } - } + if (its == doc.end()) { + return ""; } -#endif -#if defined(HAVE_DAEMON) - /* CLI priority is higher */ - bool daemonize = m_options.count("-f") == 0 && m_options.count("--foreground") == 0; + auto ito = its->find(key); - if (daemonize && sc != config.end()) { - it = sc->find("foreground"); - - if (it != sc->end()) { - daemonize = !util::isBoolean(it->value()); - } + if (ito == its->end()) { + return ""; } - if (daemonize) { - daemon(1, 0); - } -#endif - - if (sc != config.end()) { - try { -#if defined(HAVE_SETGID) - if ((it = sc->find("gid")) != sc->end()) { - sys::setGid(it->value()); - } -#endif -#if defined(HAVE_SETUID) - if ((it = sc->find("uid")) != sc->end()) { - sys::setUid(it->value()); - } -#endif - } catch (const std::exception &ex) { - log::warning() << "irccd: could not set " << it->key() << ": " << ex.what() << std::endl; - } - } + return ito->value(); } -void Config::loadFormats(const ini::Document &config) const +ServerIdentity loadIdentity(const ini::Section &sc) { - ini::Document::const_iterator sc = config.find("format"); + assert(sc.key() == "identity"); + assert(sc.contains("name") && util::isIdentifierValid(sc["name"].value())); + + ServerIdentity identity; + + // Mandatory stuff. + identity.name = sc["name"].value(); + + // Optional stuff. + ini::Section::const_iterator it; - if (sc == config.end()) { - return; + if ((it = sc.find("username")) != sc.end()) { + identity.username = it->value(); + } + if ((it = sc.find("realname")) != sc.end()) { + identity.realname = it->value(); + } + if ((it = sc.find("nickname")) != sc.end()) { + identity.nickname = it->value(); + } + if ((it = sc.find("ctcp-version")) != sc.end()) { + identity.ctcpversion = it->value(); } - ini::Section::const_iterator it; - std::unique_ptr filter = std::make_unique(); + return identity; +} - if ((it = sc->find("debug")) != sc->cend()) { - filter->m_debug = it->value(); - } - if ((it = sc->find("info")) != sc->cend()) { - filter->m_info = it->value(); - } - if ((it = sc->find("warning")) != sc->cend()) { - filter->m_warning = it->value(); +PluginConfig loadPluginConfig(const ini::Section &sc) +{ + PluginConfig config; + + for (const auto &option : sc) { + config.emplace(option.key(), option.value()); } - log::setFilter(std::move(filter)); + return config; } -void Config::loadLogFile(const ini::Section &sc) const +std::unique_ptr loadLogFile(const ini::Section &sc) { /* * TODO: improve that with CMake options. */ #if defined(IRCCD_SYSTEM_WINDOWS) - string normal = "log.txt"; - string errors = "errors.txt"; + std::string normal = "log.txt"; + std::string errors = "errors.txt"; #else - string normal = "/var/log/irccd/log.txt"; - string errors = "/var/log/irccd/errors.txt"; + std::string normal = "/var/log/irccd/log.txt"; + std::string errors = "/var/log/irccd/errors.txt"; #endif ini::Section::const_iterator it; @@ -189,177 +151,228 @@ errors = it->value(); } - log::setInterface(make_unique(move(normal), move(errors))); + return std::make_unique(std::move(normal), std::move(errors)); } -void Config::loadLogSyslog() const +std::unique_ptr loadLogSyslog() { #if defined(HAVE_SYSLOG) - log::setInterface(make_unique()); + return std::make_unique(); #else - log::warning() << "irccd: syslog is not available on this platform" << endl; + throw std::runtime_error("logs: syslog is not available on this platform"); #endif // !HAVE_SYSLOG } -void Config::loadLogs(const ini::Document &config) const +std::shared_ptr loadTransportIp(const ini::Section &sc) { - ini::Document::const_iterator sc = config.find("logs"); + assert(sc.key() == "transport"); - if (sc == config.end()) { - return; - } - + std::shared_ptr transport; ini::Section::const_iterator it; - if ((it = sc->find("verbose")) != sc->end() && m_options.count("-v") == 0 && m_options.count("--verbose")) { - log::setVerbose(util::isBoolean(it->value())); + // Port + int port; + + if ((it = sc.find("port")) == sc.cend()) { + throw std::invalid_argument("transport: missing 'port' parameter"); + } + + try { + port = util::toNumber(it->value()); + } catch (const std::exception &) { + throw std::invalid_argument("transport: invalid port number: {}"_format(it->value())); } - if ((it = sc->find("type")) != sc->end()) { - /* Console is the default, no test case */ - if (it->value() == "file") { - loadLogFile(*sc); - } else if (it->value() == "syslog") { - loadLogSyslog(); - } else { - log::warning() << "irccd: unknown log type: " << it->value() << std::endl; + + // Address + std::string address = "*"; + + if ((it = sc.find("address")) != sc.end()) { + address = it->value(); + } + + // Domain + bool ipv6 = true; + bool ipv4 = true; + + if ((it = sc.find("domain")) != sc.end()) { + ipv6 = false; + ipv4 = false; + + for (const auto &v : *it) { + if (v == "ipv4") { + ipv4 = true; + } + if (v == "ipv6") { + ipv6 = true; + } } } + + if (ipv6) { + transport = std::make_shared(AF_INET6, move(address), port, !ipv4); + } else if (ipv4) { + transport = std::make_shared(AF_INET, move(address), port); + } else { + throw std::invalid_argument("transport: domain must at least have ipv4 or ipv6"); + } + + return transport; } -void Config::loadPlugins(Irccd &irccd, const ini::Section &sc) const +std::shared_ptr loadTransportUnix(const ini::Section &sc) { -#if defined(WITH_JS) - for (const ini::Option &option : sc) { - try { - if (option.value().empty()) { - irccd.loadPlugin(option.key(), option.key(), true); - } else { - irccd.loadPlugin(option.key(), option.value(), false); - } - } catch (const std::exception &ex) { - log::warning() << "plugin " << option.key() << ": " << ex.what() << std::endl; - } + assert(sc.key() == "transport"); + +#if !defined(IRCCD_SYSTEM_WINDOWS) + ini::Section::const_iterator it = sc.find("path"); + + if (it == sc.end()) { + throw std::invalid_argument("transport: missing 'path' parameter"); } + + return std::make_shared(it->value()); #else - (void)irccd; (void)sc; + + throw std::invalid_argument("transport: unix transport not supported on on this platform"); #endif } -void Config::loadPluginConfig(Irccd &irccd, const ini::Section &sc, string name) const +std::shared_ptr loadTransport(const ini::Section &sc) { -#if defined(WITH_JS) - PluginConfig config; + assert(sc.key() == "transport"); - for (const ini::Option &option : sc) { - config.emplace(option.key(), option.value()); + std::shared_ptr transport; + ini::Section::const_iterator it = sc.find("type"); + + if (it == sc.end()) { + throw std::invalid_argument("transport: missing 'type' parameter"); } - irccd.addPluginConfig(std::move(name), std::move(config)); -#else - (void)irccd; - (void)sc; - (void)name; -#endif + if (it->value() == "ip") { + transport = loadTransportIp(sc); + } else if (it->value() == "unix") { + transport = loadTransportUnix(sc); + } else { + throw std::invalid_argument("transport: invalid type given: {}"_format(it->value())); + } + + return transport; } -void Config::loadPlugins(Irccd &irccd, const ini::Document &config) const +Rule loadRule(const ini::Section &sc) { -#if defined(WITH_JS) - std::regex regex("^plugin\\.([A-Za-z0-9-_]+)$"); - std::smatch match; + assert(sc.key() == "rule"); + + // Simple converter from std::vector to std::unordered_set + auto toSet = [] (const std::vector &v) -> std::unordered_set { + return std::unordered_set(v.begin(), v.end()); + }; + + RuleSet servers, channels, origins, plugins, events; + RuleAction action = RuleAction::Accept; + + // Get the sets + ini::Section::const_iterator it; - /* - * Load plugin configurations before we load plugins since we use them - * when we load the plugin itself. - */ - for (const ini::Section §ion : config) { - if (regex_match(section.key(), match, regex)) { - loadPluginConfig(irccd, section, match[1]); - } + if ((it = sc.find("servers")) != sc.end()) { + servers = toSet(*it); + } + if ((it = sc.find("channels")) != sc.end()) { + channels = toSet(*it); + } + if ((it = sc.find("origins")) != sc.end()) { + origins = toSet(*it); + } + if ((it = sc.find("plugins")) != sc.end()) { + plugins = toSet(*it); + } + if ((it = sc.find("channels")) != sc.end()) { + channels = toSet(*it); } - ini::Document::const_iterator it = config.find("plugins"); - - if (it != config.end()) { - loadPlugins(irccd, *it); + // Get the action + if ((it = sc.find("action")) == sc.end()) { + throw std::invalid_argument("rule: missing 'action'' parameter"); } -#else - (void)irccd; - (void)config; - log::warning() << "irccd: JavaScript disabled, ignoring plugins" << std::endl; -#endif + if (it->value() == "drop") { + action = RuleAction::Drop; + } else if (it->value() == "accept") { + action = RuleAction::Accept; + } else { + throw std::invalid_argument("rule: invalid action given: {}"_format(it->value())); + } + + return Rule(std::move(servers), + std::move(channels), + std::move(origins), + std::move(plugins), + std::move(events), + action); } -void Config::loadServer(Irccd &irccd, const ini::Section &sc) const +std::shared_ptr loadServer(const ini::Section &sc, const Config &config) { + assert(sc.key() == "server"); + ServerInfo info; ServerIdentity identity; ServerSettings settings; - /* Name */ + // Name ini::Section::const_iterator it; if ((it = sc.find("name")) == sc.end()) { - throw std::invalid_argument("server: missing name"); + throw std::invalid_argument("server: missing 'name' parameter"); } else if (!util::isIdentifierValid(it->value())) { - throw std::invalid_argument("server " + it->value() + ": name is not valid"); - } else if (irccd.hasServer(it->value())) { - throw std::invalid_argument("server " + it->value() + ": already exists"); + throw std::invalid_argument("server: invalid identifier: {}"_format(it->value())); } info.name = it->value(); - /* Host */ + // Host if ((it = sc.find("host")) == sc.end()) { - throw std::invalid_argument("server " + info.name + ": missing host"); + throw std::invalid_argument("server {}: missing host"_format(it->value())); } info.host = it->value(); - /* Optional identity */ + // Optional identity if ((it = sc.find("identity")) != sc.end()) { - identity = irccd.findIdentity(it->value()); + identity = config.findIdentity(it->value()); } - /* Optional port */ + // Optional port if ((it = sc.find("port")) != sc.end()) { try { - info.port = std::stoi(it->value()); + info.port = util::toNumber(it->value()); } catch (const std::exception &) { - throw std::invalid_argument("server " + info.name + ": invalid port number: " + it->value()); + throw std::invalid_argument("server {}: invalid number for {}: {}"_format(info.name, it->key(), it->value())); } } - /* Optional password */ + // Optional password if ((it = sc.find("password")) != sc.end()) { info.password = it->value(); } - /* Optional flags */ + // Optional flags if ((it = sc.find("ipv6")) != sc.end() && util::isBoolean(it->value())) { info.flags |= ServerInfo::Ipv6; } if ((it = sc.find("ssl")) != sc.end()) { if (util::isBoolean(it->value())) { -#if defined(WITH_SSL) info.flags |= ServerInfo::Ssl; -#else - throw std::invalid_argument("server " + info.name + ": ssl is disabled"); -#endif } } if ((it = sc.find("ssl-verify")) != sc.end()) { if (util::isBoolean(it->value())) { -#if defined(WITH_SSL) info.flags |= ServerInfo::SslVerify; -#else - throw std::invalid_argument("server " + info.name + ": ssl is disabled"); -#endif } } - /* Options */ + + // Options if ((it = sc.find("auto-rejoin")) != sc.end() && util::isBoolean(it->value())) { settings.flags |= ServerSettings::AutoRejoin; } @@ -367,7 +380,7 @@ settings.flags |= ServerSettings::JoinInvite; } - /* Channels */ + // Channels if ((it = sc.find("channels")) != sc.end()) { for (const std::string &s : *it) { ServerChannel channel; @@ -386,297 +399,246 @@ settings.command = it->value(); } - /* Reconnect */ + // Reconnect and ping timeout try { if ((it = sc.find("reconnect-tries")) != sc.end()) { - settings.reconnectTries = std::stoi(it->value()); + settings.reconnectTries = util::toNumber(it->value()); } if ((it = sc.find("reconnect-timeout")) != sc.end()) { - settings.reconnectDelay = std::stoi(it->value()); + settings.reconnectDelay = util::toNumber(it->value()); } if ((it = sc.find("ping-timeout")) != sc.end()) { - settings.pingTimeout = std::stoi(it->value()); + settings.pingTimeout = util::toNumber(it->value()); } } catch (const std::exception &) { - throw std::invalid_argument("server " + info.name + ": invalid number for " + it->key() + ": " + it->value()); + log::warning("server {}: invalid number for {}: {}"_format(info.name, it->key(), it->value())); } - irccd.addServer(std::make_shared(std::move(info), std::move(identity), std::move(settings))); -} - -void Config::loadServers(Irccd &irccd, const ini::Document &config) const -{ - for (const ini::Section §ion : config) { - if (section.key() == "server") { - try { - loadServer(irccd, section); - } catch (const exception &ex) { - log::warning() << ex.what() << endl; - } - } - } + return std::make_shared(std::move(info), std::move(identity), std::move(settings)); } -void Config::loadIdentity(Irccd &irccd, const ini::Section &sc) const +} // !namespace + +Config Config::find() { - ServerIdentity identity; - ini::Section::const_iterator it; + for (const auto &path : path::list(path::PathConfig)) { + std::string fullpath = path + "irccd.conf"; - if ((it = sc.find("name")) == sc.end()) { - throw invalid_argument("missing name"); - } else if (!util::isIdentifierValid(it->value())) { - throw invalid_argument("identity name not valid"); + if (!fs::isReadable(fullpath)) { + continue; + } + + try { + return Config(fullpath); + } catch (const std::exception &ex) { + throw std::runtime_error("{}: {}"_format(fullpath, ex.what())); + } } - identity.name = it->value(); - - /* Optional stuff */ - if ((it = sc.find("username")) != sc.end()) { - identity.username = it->value(); - } - if ((it = sc.find("realname")) != sc.end()) { - identity.realname = it->value(); - } - if ((it = sc.find("nickname")) != sc.end()) { - identity.nickname = it->value(); - } - if ((it = sc.find("ctcp-version")) != sc.end()) { - identity.ctcpversion = it->value(); - } - - log::debug() << "identity " << identity.name << ": "; - log::debug() << "nickname=" << identity.nickname << ", username=" << identity.username << ", "; - log::debug() << "realname=" << identity.realname << ", ctcp-version=" << identity.ctcpversion << endl; - - irccd.addIdentity(move(identity)); + throw std::runtime_error("no configuration file found"); } -void Config::loadIdentities(Irccd &irccd, const ini::Document &config) const +ServerIdentity Config::findIdentity(const std::string &name) const { - for (const ini::Section §ion : config) { - if (section.key() == "identity") { - try { - loadIdentity(irccd, section); - } catch (const exception &ex) { - log::warning() << "identity: " << ex.what() << endl; - } + assert(util::isIdentifierValid(name)); + + for (const auto §ion : m_document) { + if (section.key() != "identity") { + continue; + } + + auto it = section.find("name"); + + if (it == section.end()) { + log::warning("identity: missing 'name' property"); + continue; } + if (!util::isIdentifierValid(it->value())) { + log::warning("identity: invalid identifier: {}"_format(it->value())); + continue; + } + if (it->value() != name) { + continue; + } + + return loadIdentity(section); } + + return ServerIdentity(); } -void Config::loadRule(Irccd &irccd, const ini::Section &sc) const +PluginConfig Config::findPluginConfig(const std::string &name) const +{ + assert(util::isIdentifierValid(name)); + + std::string fullname = std::string("plugin.") + name; + + for (const auto §ion : m_document) { + if (section.key() != fullname) { + continue; + } + + return loadPluginConfig(section); + } + + return PluginConfig(); +} + +bool Config::isVerbose() const noexcept +{ + return util::isBoolean(get(m_document, "logs", "verbose")); +} + +bool Config::isForeground() const noexcept { - /* Simple converter from std::vector to std::unordered_set */ - auto toSet = [] (const std::vector &v) -> std::unordered_set { - return std::unordered_set(v.begin(), v.end()); - }; + return util::isBoolean(get(m_document, "general", "foreground")); +} + +std::string Config::pidfile() const +{ + return get(m_document, "general", "pidfile"); +} + +std::string Config::uid() const +{ + return get(m_document, "general", "uid"); +} - RuleSet servers, channels, origins, plugins, events; - RuleAction action = RuleAction::Accept; +std::string Config::gid() const +{ + return get(m_document, "general", "gid"); +} - /* Get the sets */ +void Config::loadLogs() const +{ + ini::Document::const_iterator sc = m_document.find("logs"); + + if (sc == m_document.end()) { + return; + } + ini::Section::const_iterator it; - if ((it = sc.find("servers")) != sc.end()) { - servers = toSet(*it); - } - if ((it = sc.find("channels")) != sc.end()) { - channels = toSet(*it); - } - if ((it = sc.find("origins")) != sc.end()) { - origins = toSet(*it); - } - if ((it = sc.find("plugins")) != sc.end()) { - plugins = toSet(*it); - } - if ((it = sc.find("channels")) != sc.end()) { - channels = toSet(*it); - } + if ((it = sc->find("type")) != sc->end()) { + std::unique_ptr iface; - /* Get the action */ - if ((it = sc.find("action")) == sc.end()) { - throw std::invalid_argument("missing action parameter"); - } - if (it->value() == "drop") { - action = RuleAction::Drop; - } else if (it->value() == "accept") { - action = RuleAction::Accept; - } else { - throw std::invalid_argument("invalid action given: " + it->value()); - } + /* Console is the default, no test case */ + if (it->value() == "file") { + iface = loadLogFile(*sc); + } else if (it->value() == "syslog") { + iface = loadLogSyslog(); + } else { + throw std::runtime_error("logs: unknown log type: {}"_format(it->value())); + } - irccd.addRule(Rule(move(servers), move(channels), move(origins), move(plugins), move(events), action)); -} - -void Config::loadRules(Irccd &irccd, const ini::Document &config) const -{ - for (const ini::Section &sc : config) { - if (sc.key() == "rule") { - try { - loadRule(irccd, sc); - } catch (const std::exception &ex) { - log::warning() << "rule: " << ex.what() << std::endl; - } + if (iface) { + log::setInterface(std::move(iface)); } } } -void Config::loadTransportIp(Irccd &irccd, const ini::Section &sc) const +void Config::loadFormats() const { - bool ipv6 = true; - bool ipv4 = true; + ini::Document::const_iterator sc = m_document.find("format"); + if (sc == m_document.end()) { + return; + } + + std::unique_ptr filter = std::make_unique(); ini::Section::const_iterator it; - /* Port */ - int port; - - if ((it = sc.find("port")) == sc.end()) { - throw invalid_argument("missing port"); + if ((it = sc->find("debug")) != sc->cend()) { + filter->m_debug = it->value(); } - - try { - port = stoi(it->value()); - } catch (const std::exception &) { - throw std::invalid_argument("invalid port number: " + it->value()); + if ((it = sc->find("info")) != sc->cend()) { + filter->m_info = it->value(); + } + if ((it = sc->find("warning")) != sc->cend()) { + filter->m_warning = it->value(); } - /* Address*/ - std::string address = "*"; + log::setFilter(std::move(filter)); +} - if ((it = sc.find("address")) != sc.end()) { - address = it->value(); +std::vector> Config::loadTransports() const +{ + std::vector> transports; + + for (const auto §ion : m_document) { + if (section.key() == "transport") { + transports.push_back(loadTransport(section)); + } } - /* Domain */ - if ((it = sc.find("domain")) != sc.end()) { - ipv6 = false; - ipv4 = false; + return transports; +} - for (const string &v : *it) { - if (v == "ipv4") { - ipv4 = true; - } - if (v == "ipv6") { - ipv6 = true; - } +std::vector Config::loadRules() const +{ + std::vector rules; + + for (const auto §ion : m_document) { + if (section.key() == "rule") { + rules.push_back(loadRule(section)); } } - if (ipv6) { - irccd.addTransport(std::make_shared(AF_INET6, move(address), port, !ipv4)); - } else if (ipv4) { - irccd.addTransport(std::make_shared(AF_INET, move(address), port)); - } else { - throw std::invalid_argument("domain must at least have ipv4 or ipv6"); - } -} - -void Config::loadTransportUnix(Irccd &irccd, const ini::Section &sc) const -{ -#if !defined(IRCCD_SYSTEM_WINDOWS) - /* Path */ - ini::Section::const_iterator it = sc.find("path"); - - if (it == sc.end()) { - throw std::invalid_argument("missing path parameter"); - } else { - string path = sc["path"].value(); - - irccd.addTransport(std::make_shared(move(path))); - } -#else - (void)irccd; - (void)sc; - - throw std::invalid_argument("local transport not supported on on this platform"); -#endif + return rules; } -void Config::loadTransports(Irccd &irccd, const ini::Document &config) const +std::vector> Config::loadServers() const { - for (const ini::Section &sc : config) { - if (sc.key() == "transport") { - try { - ini::Section::const_iterator it = sc.find("type"); - - if (it == sc.end()) { - log::warning() << "transport: missing type parameter" << std::endl; - } else if (it->value() == "ip") { - loadTransportIp(irccd, sc); - } else if (it->value() == "unix") { - loadTransportUnix(irccd, sc); - } else { - log::warning() << "transport: invalid type given: " << std::endl; - } - } catch (const net::Error &error) { - log::warning() << "transport: " << error.function() << ": " << error.what() << std::endl; - } catch (const exception &ex) { - log::warning() << "transport: error: " << ex.what() << endl; - } - } - } -} - -bool Config::openConfig(Irccd &irccd, const string &path) const -{ - try { - /* - * Order matters, take care when you change this. - */ - ini::Document config(ini::File{path}); + std::vector> servers; - loadGeneral(config); - loadLogs(config); - loadFormats(config); - loadIdentities(irccd, config); - loadServers(irccd, config); - loadRules(irccd, config); - loadPlugins(irccd, config); - loadTransports(irccd, config); - } catch (const ini::Error &ex) { - log::warning() << sys::programName() << ": " << path << ":" << ex.line() << ":" << ex.column() << ": " << ex.what() << std::endl; - return false; - } catch (const std::exception &ex) { - log::warning() << sys::programName() << ": " << path << ": " << ex.what() << std::endl; - return false; - } - - return true; -} + for (const auto §ion : m_document) { + if (section.key() != "server") { + continue; + } -Config::Config(parser::Result options) noexcept - : m_options(move(options)) -{ -} - -void Config::load(Irccd &irccd) -{ - auto it = m_options.find("-c"); - auto found = false; - - if (it != m_options.end()) { - found = openConfig(irccd, it->second); - } else if ((it = m_options.find("--config")) != m_options.end()) { - found = openConfig(irccd, it->second); - } else { - /* Search for a configuration file */ - for (const string &path : path::list(path::PathConfig)) { - string fullpath = path + "irccd.conf"; - - log::info() << "irccd: trying " << fullpath << endl; - - if (openConfig(irccd, fullpath)) { - found = true; - break; - } + try { + servers.push_back(loadServer(section, *this)); + } catch (const std::exception &ex) { + log::warning(ex.what()); } } - if (!found) { - log::warning() << "irccd: no configuration file could be found, exiting" << endl; - exit(1); + return servers; +} + +std::vector> Config::loadPlugins() const +{ + std::vector> plugins; + + // Plugins are defined in only one section. + auto it = m_document.find("plugins"); + + if (it == m_document.end()) { + return plugins; } + + for (const auto &option : *it) { + std::string name = option.key(); + std::string path = option.value(); + + try { + log::info("plugin {}: loading"_format(name)); + + if (path.empty()) { + plugins.push_back(Plugin::find(name, findPluginConfig(name))); + } else { + log::info("plugin {}: trying {}"_format(name, path)); + plugins.push_back(std::make_shared(name, path, findPluginConfig(name))); + } + } catch (const duk::ErrorInfo &ex) { + log::warning("plugin {}: {}"_format(option.key(), ex.what())); + log::warning("plugin {}: {}"_format(option.key(), ex.stack)); + } catch (const std::exception &ex) { + log::warning("plugin {}: {}"_format(option.key(), ex.what())); + } + } + + return plugins; } } // !irccd diff -r 2aecbd638b1c -r c7fee63ccf92 lib/irccd/config.hpp --- a/lib/irccd/config.hpp Wed May 04 13:38:02 2016 +0200 +++ b/lib/irccd/config.hpp Tue May 10 21:27:40 2016 +0200 @@ -24,18 +24,21 @@ * \brief Read .ini configuration file for irccd */ -#include "options.hpp" +#include +#include +#include + +#include "ini.hpp" +#include "plugin.hpp" namespace irccd { -namespace ini { +class Rule; -class Document; -class Section; +class Server; +class ServerIdentity; -} // !ini - -class Irccd; +class TransportServer; /** * \class Config @@ -43,42 +46,127 @@ */ class Config { private: - parser::Result m_options; - - void loadGeneral(const ini::Document &config) const; - void loadFormats(const ini::Document &config) const; - void loadLogFile(const ini::Section &sc) const; - void loadLogSyslog() const; - void loadLogs(const ini::Document &config) const; - void loadPlugins(Irccd &irccd, const ini::Section &sc) const; - void loadPluginConfig(Irccd &irccd, const ini::Section &sc, std::string name) const; - void loadPlugins(Irccd &irccd, const ini::Document &config) const; - void loadServer(Irccd &irccd, const ini::Section &sc) const; - void loadServers(Irccd &irccd, const ini::Document &config) const; - void loadIdentity(Irccd &irccd, const ini::Section &sc) const; - void loadIdentities(Irccd &irccd, const ini::Document &config) const; - void loadRule(Irccd &irccd, const ini::Section &sc) const; - void loadRules(Irccd &irccd, const ini::Document &config) const; - void loadTransportIp(Irccd &irccd, const ini::Section &sc) const; - void loadTransportUnix(Irccd &irccd, const ini::Section &sc) const; - void loadTransports(Irccd &irccd, const ini::Document &config) const; - bool openConfig(Irccd &irccd, const std::string &path) const; + std::string m_path; + ini::Document m_document; public: /** - * Construct the configuration file loader. If path is empty, then the configuration file is searched through - * the standard directories. + * Search the configuration file into the standard defined paths. + * + * \return the config + * \throw std::exception on errors or if no config could be found + */ + static Config find(); + + /** + * Load the configuration from the specified path. + * + * \param path the path + */ + inline Config(std::string path) + : m_path(std::move(path)) + , m_document(ini::readFile(m_path)) + { + } + + /** + * Get the path to the configuration file. * - * \param options the option parsed at command line + * \return the path + */ + inline const std::string &path() const noexcept + { + return m_path; + } + + /** + * Find an entity if defined in the configuration file. + * + * \pre util::isValidIdentifier(name) + * \return default identity if cannot be found */ - Config(parser::Result options) noexcept; + ServerIdentity findIdentity(const std::string &name) const; + + /** + * Find a plugin configuration if defined in the configuration file. + * + * \pre util::isValidIdentifier(name) + * \return the configuration or empty if not found + */ + PluginConfig findPluginConfig(const std::string &name) const; + + /** + * Get the path to the pidfile. + * + * \return the path or empty if not defined + */ + std::string pidfile() const; /** - * Load the config into irccd. + * Get the uid. + * + * \return the uid or empty one if no one is set + */ + std::string uid() const; + + /** + * Get the gid. + * + * \return the gid or empty one if no one is set + */ + std::string gid() const; + + /** + * Check if verbosity is enabled. + * + * \return true if verbosity was requested + */ + bool isVerbose() const noexcept; + + /** + * Check if foreground is specified (= no daemonize). * - * \param irccd the irccd instance + * \return true if foreground was requested + */ + bool isForeground() const noexcept; + + /** + * Load logging interface. + */ + void loadLogs() const; + + /** + * Load formats for logging. + */ + void loadFormats() const; + + /** + * Load transports. + * + * \return the set of transports */ - void load(Irccd &irccd); + std::vector> loadTransports() const; + + /** + * Load rules. + * + * \return the rules + */ + std::vector loadRules() const; + + /** + * Get the list of servers defined. + * + * \return the list of servers + */ + std::vector> loadServers() const; + + /** + * Get the list of defined plugins. + * + * \return the list of plugins + */ + std::vector> loadPlugins() const; }; } // !irccd diff -r 2aecbd638b1c -r c7fee63ccf92 lib/irccd/fs.cpp --- a/lib/irccd/fs.cpp Wed May 04 13:38:02 2016 +0200 +++ b/lib/irccd/fs.cpp Tue May 10 21:27:40 2016 +0200 @@ -67,11 +67,14 @@ bool can(const std::string &path, const std::string &mode) { auto fp = std::fopen(path.c_str(), mode.c_str()); - auto result = fp != nullptr; + + if (fp == nullptr) { + return false; + } std::fclose(fp); - return result; + return true; } #if defined(_WIN32) diff -r 2aecbd638b1c -r c7fee63ccf92 lib/irccd/ini.cpp --- a/lib/irccd/ini.cpp Wed May 04 13:38:02 2016 +0200 +++ b/lib/irccd/ini.cpp Tue May 10 21:27:40 2016 +0200 @@ -1,5 +1,5 @@ /* - * ini.cpp -- .ini file parsing + * ini.cpp -- extended .ini file parser * * Copyright (c) 2013-2016 David Demelier * @@ -16,7 +16,6 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include #include #include #include @@ -25,16 +24,18 @@ #include #include +/* for PathIsRelative */ #if defined(_WIN32) -# include // for PathIsRelative +# include #endif #include "ini.hpp" +namespace irccd { + namespace { -using namespace irccd; -using namespace irccd::ini; +using namespace ini; using StreamIterator = std::istreambuf_iterator; using TokenIterator = std::vector::const_iterator; @@ -69,7 +70,7 @@ return isList(c) || isQuote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '='; } -void analyzeLine(int &line, int &column, StreamIterator &it) noexcept +void analyseLine(int &line, int &column, StreamIterator &it) noexcept { assert(*it == '\n'); @@ -78,7 +79,7 @@ column = 0; } -void analyzeComment(int &column, StreamIterator &it, StreamIterator end) noexcept +void analyseComment(int &column, StreamIterator &it, StreamIterator end) noexcept { assert(*it == '#'); @@ -88,7 +89,7 @@ } } -void analyzeSpaces(int &column, StreamIterator &it, StreamIterator end) noexcept +void analyseSpaces(int &column, StreamIterator &it, StreamIterator end) noexcept { assert(isSpace(*it)); @@ -98,7 +99,7 @@ } } -void analyzeList(Tokens &list, int line, int &column, StreamIterator &it) noexcept +void analyseList(Tokens &list, int line, int &column, StreamIterator &it) noexcept { assert(isList(*it)); @@ -117,7 +118,7 @@ } } -void analyzeSection(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +void analyseSection(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) { assert(*it == '['); @@ -127,16 +128,23 @@ /* Read section name */ ++ it; while (it != end && *it != ']') { - if (*it == '\n') + if (*it == '\n') { throw Error(line, column, "section not terminated, missing ']'"); - if (isReserved(*it)) + } + if (isReserved(*it)) { throw Error(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'"); + } + ++ column; value += *it++; } - if (it == end) + if (it == end) { throw Error(line, column, "section name expected after '[', got "); + } + if (value.empty()) { + throw Error(line, column, "empty section name"); + } /* Remove ']' */ ++ it; @@ -144,7 +152,7 @@ list.emplace_back(Token::Section, line, save, std::move(value)); } -void analyzeAssign(Tokens &list, int &line, int &column, StreamIterator &it) +void analyseAssign(Tokens &list, int &line, int &column, StreamIterator &it) { assert(*it == '='); @@ -152,7 +160,7 @@ ++ it; } -void analyzeQuotedWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +void analyseQuotedWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) { std::string value; int save = column; @@ -164,8 +172,9 @@ value += *it++; } - if (it == end) + if (it == end) { throw Error(line, column, "undisclosed '" + std::string(1, quote) + "', got "); + } /* Remove quote */ ++ it; @@ -173,7 +182,7 @@ list.push_back({ Token::QuotedWord, line, save, std::move(value) }); } -void analyzeWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +void analyseWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) { assert(!isReserved(*it)); @@ -188,7 +197,7 @@ list.push_back({ Token::Word, line, save, std::move(value) }); } -void analyzeInclude(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +void analyseInclude(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) { assert(*it == '@'); @@ -202,42 +211,13 @@ include += *it++; } - if (include != "include") + if (include != "include") { throw Error(line, column, "expected include after '@' token"); + } list.push_back({ Token::Include, line, save }); } -Tokens analyze(StreamIterator &it, StreamIterator end) -{ - Tokens list; - int line = 1; - int column = 0; - - while (it != end) { - if (*it == '\n') - analyzeLine(line, column, it); - else if (*it == '#') - analyzeComment(column, it, end); - else if (*it == '[') - analyzeSection(list, line, column, it, end); - else if (*it == '=') - analyzeAssign(list, line, column, it); - else if (isSpace(*it)) - analyzeSpaces(column, it, end); - else if (*it == '@') - analyzeInclude(list, line, column, it, end); - else if (isQuote(*it)) - analyzeQuotedWord(list, line, column, it, end); - else if (isList(*it)) - analyzeList(list, line, column, it); - else - analyzeWord(list, line, column, it, end); - } - - return list; -} - void parseOptionValueSimple(Option &option, TokenIterator &it) { assert(it->type() == Token::Word || it->type() == Token::QuotedWord); @@ -270,8 +250,9 @@ } } - if (it == end) + if (it == end) { throw Error(save->line(), save->column(), "unterminated list construct"); + } /* Remove ) */ ++ it; @@ -284,50 +265,53 @@ TokenIterator save = it; /* No '=' or something else? */ - if (++it == end) + if (++it == end) { throw Error(save->line(), save->column(), "expected '=' assignment, got "); - if (it->type() != Token::Assign) + } + 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) + if (it->type() == Token::Word || it->type() == Token::QuotedWord) { parseOptionValueSimple(option, it); - else if (it->type() == Token::ListBegin) + } else if (it->type() == Token::ListBegin) { parseOptionValueList(option, it, end); + } } sc.push_back(std::move(option)); } -void parseInclude(Document &doc, TokenIterator &it, TokenIterator end) +void parseInclude(Document &doc, const std::string &path, TokenIterator &it, TokenIterator end) { TokenIterator save = it; - if (++it == end) + if (++it == end) { throw Error(save->line(), save->column(), "expected file name after '@include' statement, got "); - if (it->type() != Token::Word && it->type() != Token::QuotedWord) + } + + if (it->type() != Token::Word && it->type() != Token::QuotedWord) { throw Error(it->line(), it->column(), "expected file name after '@include' statement, got " + it->value()); - if (doc.path().empty()) - throw Error(it->line(), it->column(), "'@include' statement invalid with buffer documents"); + } std::string value = (it++)->value(); std::string file; if (!isAbsolute(value)) { #if defined(_WIN32) - file = doc.path() + "\\" + value; + file = path + "\\" + value; #else - file = doc.path() + "/" + value; + file = path + "/" + value; #endif } else { file = value; } - Document child(File{file}); - - for (const auto &sc : child) + for (const auto &sc : readFile(file)) { doc.push_back(sc); + } } void parseSection(Document &doc, TokenIterator &it, TokenIterator end) @@ -339,8 +323,9 @@ /* Read until next section */ while (it != end && it->type() != Token::Section) { - if (it->type() != Token::Word) + if (it->type() != Token::Word) { throw Error(it->line(), it->column(), "unexpected token '" + it->value() + "' in section definition"); + } parseOption(sc, it, end); } @@ -348,16 +333,56 @@ doc.push_back(std::move(sc)); } -void parse(Document &doc, const Tokens &tokens) +} // !namespace + +namespace ini { + +Tokens analyse(std::istreambuf_iterator it, std::istreambuf_iterator 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(stream), {}); +} + +Document parse(const Tokens &tokens, const std::string &path) +{ + Document doc; TokenIterator it = tokens.cbegin(); TokenIterator end = tokens.cend(); while (it != end) { - /* Just ignore this */ switch (it->type()) { case Token::Include: - parseInclude(doc, it, end); + parseInclude(doc, path, it, end); break; case Token::Section: parseSection(doc, it, end); @@ -366,60 +391,44 @@ throw Error(it->line(), it->column(), "unexpected '" + it->value() + "' on root document"); } } -} -} // !namespace - -namespace irccd { - -namespace ini { - -Tokens Document::analyze(const File &file) -{ - std::fstream stream(file.path); - - if (!stream) - throw std::runtime_error(std::strerror(errno)); - - std::istreambuf_iterator it(stream); - std::istreambuf_iterator end; - - return ::analyze(it, end); + return doc; } -Tokens Document::analyze(const Buffer &buffer) +Document readFile(const std::string &filename) { - std::istringstream stream(buffer.text); - std::istreambuf_iterator it(stream); - std::istreambuf_iterator end; + /* Get parent path */ + auto parent = filename; + auto pos = parent.find_last_of("/\\"); - return ::analyze(it, end); + 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::Document(const File &file) - : m_path(file.path) +Document readString(const std::string &buffer) { - /* Update path */ - auto pos = m_path.find_last_of("/\\"); + std::istringstream iss(buffer); - if (pos != std::string::npos) - m_path.erase(pos); - else - m_path = "."; - - parse(*this, analyze(file)); + return parse(analyse(iss)); } -Document::Document(const Buffer &buffer) +void dump(const Tokens &tokens) { - parse(*this, analyze(buffer)); -} - -void Document::dump(const Tokens &tokens) -{ - for (const Token &token: tokens) + for (const Token &token: tokens) { // TODO: add better description std::cout << token.line() << ":" << token.column() << ": " << token.value() << std::endl; + } } } // !ini diff -r 2aecbd638b1c -r c7fee63ccf92 lib/irccd/ini.hpp --- a/lib/irccd/ini.hpp Wed May 04 13:38:02 2016 +0200 +++ b/lib/irccd/ini.hpp Tue May 10 21:27:40 2016 +0200 @@ -1,5 +1,5 @@ /* - * ini.hpp -- .ini file parsing + * ini.hpp -- extended .ini file parser * * Copyright (c) 2013-2016 David Demelier * @@ -21,10 +21,90 @@ /** * \file ini.hpp - * \brief Configuration file parser. + * \brief Extended .ini file parser. + * \author David Demelier + */ + +/** + * \page Ini Ini + * \brief Extended .ini file parser. + * + * - \subpage ini-syntax + */ + +/** + * \page ini-syntax Syntax + * \brief File syntax. + * + * The syntax is similar to most of `.ini` implementations as: + * + * - a section is delimited by `[name]` can be redefined multiple times, + * - an option **must** always be defined in a section, + * - empty options must be surrounded by quotes, + * - lists can not includes trailing commas, + * - include statement must always be at the beginning of files (in no sections), + * - comments starts with # until the end of line, + * - options with spaces **must** use quotes. + * + * # Basic file + * + * ````ini + * # This is a comment. + * [section] + * option1 = value1 + * option2 = "value 2 with spaces" # comment is also allowed here + * ```` + * + * # Redefinition + * + * Sections can be redefined multiple times and are kept the order they are seen. + * + * ````ini + * [section] + * value = "1" + * + * [section] + * value = "2" + * ```` + * + * The ini::Document object will contains two ini::Section. + * + * # Lists + * + * Lists are defined using `()` and commas, like values, they may have quotes. + * + * ````ini + * [section] + * names = ( "x1", "x2" ) + * + * # This is also allowed + * biglist = ( + * "abc", + * "def" + * ) + * ```` + * + * # Include statement + * + * You can split a file into several pieces, if the include statement contains a relative path, the path will be relative + * to the current file being parsed. + * + * You **must** use the include statement before any section. + * + * If the file contains spaces, use quotes. + * + * ````ini + * # main.conf + * @include "foo.conf" + * + * # foo.conf + * [section] + * option1 = value1 + * ```` */ #include +#include #include #include #include @@ -41,7 +121,7 @@ /** * \class Error - * \brief Error in a file + * \brief Error in a file. */ class Error : public std::exception { private: @@ -53,14 +133,14 @@ /** * Constructor. * - * \param l the line - * \param c the column - * \param m the message + * \param line the line + * \param column the column + * \param msg the message */ - inline Error(int l, int c, std::string m) noexcept - : m_line(l) - , m_column(c) - , m_message(std::move(m)) + inline Error(int line, int column, std::string msg) noexcept + : m_line(line) + , m_column(column) + , m_message(std::move(msg)) { } @@ -97,16 +177,16 @@ /** * \class Token - * \brief Describe a token read in the .ini source + * \brief Describe a token read in the .ini source. * * This class can be used when you want to parse a .ini file yourself. * - * \see Document::analyze + * \see analyze */ class Token { public: /** - * \brief Token type + * \brief Token type. */ enum Type { Include, //!< include statement @@ -224,30 +304,35 @@ /** * Construct an empty option. * + * \pre key must not be empty * \param key the key - * \param value the value */ inline Option(std::string key) noexcept : std::vector() , m_key(std::move(key)) { + assert(!m_key.empty()); } /** * Construct a single option. * + * \pre key must not be empty * \param key the key * \param value the value */ inline Option(std::string key, std::string value) noexcept : m_key(std::move(key)) { + assert(!m_key.empty()); + push_back(std::move(value)); } /** * Construct a list option. * + * \pre key must not be empty * \param key the key * \param values the values */ @@ -255,6 +340,7 @@ : std::vector(std::move(values)) , m_key(std::move(key)) { + assert(!m_key.empty()); } /** @@ -292,11 +378,13 @@ /** * Construct a section with its name. * + * \pre key must not be empty * \param key the key */ inline Section(std::string key) noexcept : m_key(std::move(key)) { + assert(!m_key.empty()); } /** @@ -321,30 +409,6 @@ } /** - * Access an option at the specified key. - * - * \param key the key - * \return the option - * \throw std::out_of_range if the key does not exist - */ - inline Option &operator[](const std::string &key) - { - return *find(key); - } - - /** - * Access an option at the specified key. - * - * \param key the key - * \return the option - * \throw std::out_of_range if the key does not exist - */ - inline const Option &operator[](const std::string &key) const - { - return *find(key); - } - - /** * Find an option by key and return an iterator. * * \param key the key @@ -371,104 +435,52 @@ } /** + * Access an option at the specified key. + * + * \param key the key + * \return the option + * \pre contains(key) must return true + */ + inline Option &operator[](const std::string &key) + { + assert(contains(key)); + + return *find(key); + } + + /** + * Overloaded function. + * + * \param key the key + * \return the option + * \pre contains(key) must return true + */ + inline const Option &operator[](const std::string &key) const + { + assert(contains(key)); + + return *find(key); + } + + /** * Inherited operators. */ using std::vector