/* Configuration file handling. * * (C) 2003-2022 Anope Team * Contact us at team@anope.org * * Please read COPYING and README for further details. * * Based on the original code of Epona by Lara. * Based on the original code of Services by Andy Church. */ #include "services.h" #include "config.h" #include "bots.h" #include "access.h" #include "opertype.h" #include "channels.h" #include "hashcomp.h" using namespace Configuration; File ServicesConf("services.conf", false); // Services configuration file name Conf *Config = NULL; Block::Block(const Anope::string &n) : name(n), linenum(-1) { } const Anope::string &Block::GetName() const { return name; } int Block::CountBlock(const Anope::string &bname) { if (!this) return 0; return blocks.count(bname); } Block* Block::GetBlock(const Anope::string &bname, int num) { if (!this) return NULL; std::pair it = blocks.equal_range(bname); for (int i = 0; it.first != it.second; ++it.first, ++i) if (i == num) return &it.first->second; return NULL; } bool Block::Set(const Anope::string &tag, const Anope::string &value) { if (!this) return false; items[tag] = value; return true; } const Block::item_map* Block::GetItems() const { if (this) return &items; else return NULL; } template<> const Anope::string Block::Get(const Anope::string &tag, const Anope::string& def) const { if (!this) return def; Anope::map::const_iterator it = items.find(tag); if (it != items.end()) return it->second; return def; } template<> time_t Block::Get(const Anope::string &tag, const Anope::string &def) const { return Anope::DoTime(Get(tag, def)); } template<> bool Block::Get(const Anope::string &tag, const Anope::string &def) const { const Anope::string &str = Get(tag, def); return !str.empty() && !str.equals_ci("no") && !str.equals_ci("off") && !str.equals_ci("false") && !str.equals_ci("0"); } static void ValidateNotEmpty(const Anope::string &block, const Anope::string &name, const Anope::string &value) { if (value.empty()) throw ConfigException("The value for <" + block + ":" + name + "> cannot be empty!"); } static void ValidateNoSpaces(const Anope::string &block, const Anope::string &name, const Anope::string &value) { if (value.find(' ') != Anope::string::npos) throw ConfigException("The value for <" + block + ":" + name + "> may not contain spaces!"); } static void ValidateNotEmptyOrSpaces(const Anope::string &block, const Anope::string &name, const Anope::string &value) { ValidateNotEmpty(block, name, value); ValidateNoSpaces(block, name, value); } template static void ValidateNotZero(const Anope::string &block, const Anope::string &name, T value) { if (!value) throw ConfigException("The value for <" + block + ":" + name + "> cannot be zero!"); } Conf::Conf() : Block("") { ReadTimeout = 0; UsePrivmsg = DefPrivmsg = false; this->LoadConf(ServicesConf); for (int i = 0; i < this->CountBlock("include"); ++i) { Block *include = this->GetBlock("include", i); const Anope::string &type = include->Get("type"), &file = include->Get("name"); File f(file, type == "executable"); this->LoadConf(f); } FOREACH_MOD(OnReload, (this)); /* Check for modified values that aren't allowed to be modified */ if (Config) { struct { Anope::string block; Anope::string name; } noreload[] = { {"serverinfo", "name"}, {"serverinfo", "description"}, {"serverinfo", "localhost"}, {"serverinfo", "id"}, {"serverinfo", "pid"}, {"networkinfo", "nicklen"}, {"networkinfo", "userlen"}, {"networkinfo", "hostlen"}, {"networkinfo", "chanlen"}, }; for (unsigned i = 0; i < sizeof(noreload) / sizeof(noreload[0]); ++i) if (this->GetBlock(noreload[i].block)->Get(noreload[i].name) != Config->GetBlock(noreload[i].block)->Get(noreload[i].name)) throw ConfigException("<" + noreload[i].block + ":" + noreload[i].name + "> can not be modified once set"); } Block *serverinfo = this->GetBlock("serverinfo"), *options = this->GetBlock("options"), *mail = this->GetBlock("mail"), *networkinfo = this->GetBlock("networkinfo"); const Anope::string &servername = serverinfo->Get("name"); ValidateNotEmptyOrSpaces("serverinfo", "name", servername); if (servername.find(' ') != Anope::string::npos || servername.find('.') == Anope::string::npos) throw ConfigException("serverinfo:name is not a valid server name"); ValidateNotEmpty("serverinfo", "description", serverinfo->Get("description")); ValidateNotEmpty("serverinfo", "pid", serverinfo->Get("pid")); ValidateNotEmpty("serverinfo", "motd", serverinfo->Get("motd")); ValidateNotZero("options", "readtimeout", options->Get("readtimeout")); ValidateNotZero("options", "warningtimeout", options->Get("warningtimeout")); ValidateNotZero("networkinfo", "nicklen", networkinfo->Get("nicklen")); ValidateNotZero("networkinfo", "userlen", networkinfo->Get("userlen")); ValidateNotZero("networkinfo", "hostlen", networkinfo->Get("hostlen")); ValidateNotZero("networkinfo", "chanlen", networkinfo->Get("chanlen")); spacesepstream(options->Get("ulineservers")).GetTokens(this->Ulines); if (mail->Get("usemail")) { Anope::string check[] = { "sendmailpath", "sendfrom", "registration_subject", "registration_message", "emailchange_subject", "emailchange_message", "memo_subject", "memo_message" }; for (unsigned i = 0; i < sizeof(check) / sizeof(Anope::string); ++i) ValidateNotEmpty("mail", check[i], mail->Get(check[i])); } this->ReadTimeout = options->Get("readtimeout"); this->UsePrivmsg = options->Get("useprivmsg"); this->UseStrictPrivmsg = options->Get("usestrictprivmsg"); this->StrictPrivmsg = !UseStrictPrivmsg ? "/msg " : "/"; { std::vector defaults; spacesepstream(this->GetModule("nickserv")->Get("defaults")).GetTokens(defaults); this->DefPrivmsg = std::find(defaults.begin(), defaults.end(), "msg") != defaults.end(); } this->DefLanguage = options->Get("defaultlanguage"); this->TimeoutCheck = options->Get("timeoutcheck"); this->NickChars = networkinfo->Get("nick_chars"); for (int i = 0; i < this->CountBlock("uplink"); ++i) { Block *uplink = this->GetBlock("uplink", i); const Anope::string &host = uplink->Get("host"); bool ipv6 = uplink->Get("ipv6"); int port = uplink->Get("port"); const Anope::string &password = uplink->Get("password"); ValidateNotEmptyOrSpaces("uplink", "host", host); ValidateNotZero("uplink", "port", port); ValidateNotEmptyOrSpaces("uplink", "password", password); if (password.find(' ') != Anope::string::npos || password[0] == ':') throw ConfigException("uplink:password is not valid"); this->Uplinks.push_back(Uplink(host, port, password, ipv6)); } for (int i = 0; i < this->CountBlock("module"); ++i) { Block *module = this->GetBlock("module", i); const Anope::string &modname = module->Get("name"); ValidateNotEmptyOrSpaces("module", "name", modname); this->ModulesAutoLoad.push_back(modname); } for (int i = 0; i < this->CountBlock("opertype"); ++i) { Block *opertype = this->GetBlock("opertype", i); const Anope::string &oname = opertype->Get("name"), &modes = opertype->Get("modes"), &inherits = opertype->Get("inherits"), &commands = opertype->Get("commands"), &privs = opertype->Get("privs"); ValidateNotEmpty("opertype", "name", oname); OperType *ot = new OperType(oname); ot->modes = modes; spacesepstream cmdstr(commands); for (Anope::string str; cmdstr.GetToken(str);) ot->AddCommand(str); spacesepstream privstr(privs); for (Anope::string str; privstr.GetToken(str);) ot->AddPriv(str); commasepstream inheritstr(inherits); for (Anope::string str; inheritstr.GetToken(str);) { /* Strip leading ' ' after , */ if (str.length() > 1 && str[0] == ' ') str.erase(str.begin()); for (unsigned j = 0; j < this->MyOperTypes.size(); ++j) { OperType *ot2 = this->MyOperTypes[j]; if (ot2->GetName().equals_ci(str)) { Log() << "Inheriting commands and privs from " << ot2->GetName() << " to " << ot->GetName(); ot->Inherits(ot2); break; } } } this->MyOperTypes.push_back(ot); } for (int i = 0; i < this->CountBlock("oper"); ++i) { Block *oper = this->GetBlock("oper", i); const Anope::string &nname = oper->Get("name"), &type = oper->Get("type"), &password = oper->Get("password"), &certfp = oper->Get("certfp"), &host = oper->Get("host"), &vhost = oper->Get("vhost"); bool require_oper = oper->Get("require_oper"); ValidateNotEmptyOrSpaces("oper", "name", nname); ValidateNotEmpty("oper", "type", type); OperType *ot = NULL; for (unsigned j = 0; j < this->MyOperTypes.size(); ++j) if (this->MyOperTypes[j]->GetName() == type) ot = this->MyOperTypes[j]; if (ot == NULL) throw ConfigException("Oper block for " + nname + " has invalid oper type " + type); Oper *o = new Oper(nname, ot); o->require_oper = require_oper; o->password = password; o->certfp = certfp; spacesepstream(host).GetTokens(o->hosts); o->vhost = vhost; this->Opers.push_back(o); } for (botinfo_map::const_iterator it = BotListByNick->begin(), it_end = BotListByNick->end(); it != it_end; ++it) it->second->conf = false; for (int i = 0; i < this->CountBlock("service"); ++i) { Block *service = this->GetBlock("service", i); const Anope::string &nick = service->Get("nick"), &user = service->Get("user"), &host = service->Get("host"), &gecos = service->Get("gecos"), &modes = service->Get("modes"), &channels = service->Get("channels"); ValidateNotEmptyOrSpaces("service", "nick", nick); ValidateNotEmptyOrSpaces("service", "user", user); ValidateNotEmptyOrSpaces("service", "host", host); ValidateNotEmpty("service", "gecos", gecos); ValidateNoSpaces("service", "channels", channels); BotInfo *bi = BotInfo::Find(nick, true); if (!bi) bi = new BotInfo(nick, user, host, gecos, modes); bi->conf = true; std::vector oldchannels = bi->botchannels; bi->botchannels.clear(); commasepstream sep(channels); for (Anope::string token; sep.GetToken(token);) { bi->botchannels.push_back(token); size_t ch = token.find('#'); Anope::string chname, want_modes; if (ch == Anope::string::npos) chname = token; else { want_modes = token.substr(0, ch); chname = token.substr(ch); } bi->Join(chname); Channel *c = Channel::Find(chname); if (!c) continue; // Can't happen c->botchannel = true; /* Remove all existing modes */ ChanUserContainer *cu = c->FindUser(bi); if (cu != NULL) for (size_t j = 0; j < cu->status.Modes().length(); ++j) c->RemoveMode(bi, ModeManager::FindChannelModeByChar(cu->status.Modes()[j]), bi->GetUID()); /* Set the new modes */ for (unsigned j = 0; j < want_modes.length(); ++j) { ChannelMode *cm = ModeManager::FindChannelModeByChar(want_modes[j]); if (cm == NULL) cm = ModeManager::FindChannelModeByChar(ModeManager::GetStatusChar(want_modes[j])); if (cm && cm->type == MODE_STATUS) c->SetMode(bi, cm, bi->GetUID()); } } for (unsigned k = 0; k < oldchannels.size(); ++k) { size_t ch = oldchannels[k].find('#'); Anope::string chname = oldchannels[k].substr(ch != Anope::string::npos ? ch : 0); bool found = false; for (unsigned j = 0; j < bi->botchannels.size(); ++j) { ch = bi->botchannels[j].find('#'); Anope::string ochname = bi->botchannels[j].substr(ch != Anope::string::npos ? ch : 0); if (chname.equals_ci(ochname)) found = true; } if (found) continue; Channel *c = Channel::Find(chname); if (c) { c->botchannel = false; bi->Part(c); } } } for (int i = 0; i < this->CountBlock("log"); ++i) { Block *log = this->GetBlock("log", i); int logage = log->Get("logage"); bool rawio = log->Get("rawio"); bool debug = log->Get("debug"); LogInfo l(logage, rawio, debug); l.bot = BotInfo::Find(log->Get("bot", "Global"), true); spacesepstream(log->Get("target")).GetTokens(l.targets); spacesepstream(log->Get("source")).GetTokens(l.sources); spacesepstream(log->Get("admin")).GetTokens(l.admin); spacesepstream(log->Get("override")).GetTokens(l.override); spacesepstream(log->Get("commands")).GetTokens(l.commands); spacesepstream(log->Get("servers")).GetTokens(l.servers); spacesepstream(log->Get("channels")).GetTokens(l.channels); spacesepstream(log->Get("users")).GetTokens(l.users); spacesepstream(log->Get("other")).GetTokens(l.normal); this->LogInfos.push_back(l); } for (botinfo_map::const_iterator it = BotListByNick->begin(), it_end = BotListByNick->end(); it != it_end; ++it) it->second->commands.clear(); for (int i = 0; i < this->CountBlock("command"); ++i) { Block *command = this->GetBlock("command", i); const Anope::string &service = command->Get("service"), &nname = command->Get("name"), &cmd = command->Get("command"), &permission = command->Get("permission"), &group = command->Get("group"); bool hide = command->Get("hide"); ValidateNotEmptyOrSpaces("command", "service", service); ValidateNotEmpty("command", "name", nname); ValidateNotEmptyOrSpaces("command", "command", cmd); BotInfo *bi = this->GetClient(service); if (!bi) continue; CommandInfo &ci = bi->SetCommand(nname, cmd, permission); ci.group = group; ci.hide = hide; } PrivilegeManager::ClearPrivileges(); for (int i = 0; i < this->CountBlock("privilege"); ++i) { Block *privilege = this->GetBlock("privilege", i); const Anope::string &nname = privilege->Get("name"), &desc = privilege->Get("desc"); int rank = privilege->Get("rank"); PrivilegeManager::AddPrivilege(Privilege(nname, desc, rank)); } for (int i = 0; i < this->CountBlock("fantasy"); ++i) { Block *fantasy = this->GetBlock("fantasy", i); const Anope::string &nname = fantasy->Get("name"), &service = fantasy->Get("command"), &permission = fantasy->Get("permission"), &group = fantasy->Get("group"); bool hide = fantasy->Get("hide"), prepend_channel = fantasy->Get("prepend_channel", "yes"); ValidateNotEmptyOrSpaces("fantasy", "name", nname); ValidateNotEmptyOrSpaces("fantasy", "command", service); CommandInfo &c = this->Fantasy[nname]; c.name = service; c.permission = permission; c.group = group; c.hide = hide; c.prepend_channel = prepend_channel; } for (int i = 0; i < this->CountBlock("command_group"); ++i) { Block *command_group = this->GetBlock("command_group", i); const Anope::string &nname = command_group->Get("name"), &description = command_group->Get("description"); CommandGroup gr; gr.name = nname; gr.description = description; this->CommandGroups.push_back(gr); } /* Below here can't throw */ if (Config) /* Clear existing conf opers */ for (nickcore_map::const_iterator it = NickCoreList->begin(), it_end = NickCoreList->end(); it != it_end; ++it) { NickCore *nc = it->second; if (nc->o && std::find(Config->Opers.begin(), Config->Opers.end(), nc->o) != Config->Opers.end()) nc->o = NULL; } /* Apply new opers */ for (unsigned i = 0; i < this->Opers.size(); ++i) { Oper *o = this->Opers[i]; NickAlias *na = NickAlias::Find(o->name); if (!na) continue; if (!na->nc || na->nc->o) { // If the account is already an oper it might mean two oper blocks for the same nick, or // something else has configured them as an oper (like a module) continue; } na->nc->o = o; Log() << "Tied oper " << na->nc->display << " to type " << o->ot->GetName(); } if (options->Get("casemap", "ascii") == "ascii") Anope::casemap = std::locale(std::locale(), new Anope::ascii_ctype()); else if (options->Get("casemap") == "rfc1459") Anope::casemap = std::locale(std::locale(), new Anope::rfc1459_ctype()); else { try { Anope::casemap = std::locale(options->Get("casemap").c_str()); } catch (const std::runtime_error &) { Log() << "Unknown casemap " << options->Get("casemap") << " - casemap not changed"; } } Anope::CaseMapRebuild(); /* Check the user keys */ if (!options->Get("seed")) Log() << "Configuration option options:seed should be set. It's for YOUR safety! Remember that!"; } Conf::~Conf() { for (unsigned i = 0; i < MyOperTypes.size(); ++i) delete MyOperTypes[i]; for (unsigned i = 0; i < Opers.size(); ++i) delete Opers[i]; } void Conf::Post(Conf *old) { /* Apply module changes */ for (unsigned i = 0; i < old->ModulesAutoLoad.size(); ++i) if (std::find(this->ModulesAutoLoad.begin(), this->ModulesAutoLoad.end(), old->ModulesAutoLoad[i]) == this->ModulesAutoLoad.end()) ModuleManager::UnloadModule(ModuleManager::FindModule(old->ModulesAutoLoad[i]), NULL); for (unsigned i = 0; i < this->ModulesAutoLoad.size(); ++i) if (std::find(old->ModulesAutoLoad.begin(), old->ModulesAutoLoad.end(), this->ModulesAutoLoad[i]) == old->ModulesAutoLoad.end()) ModuleManager::LoadModule(this->ModulesAutoLoad[i], NULL); /* Apply opertype changes, as non-conf opers still point to the old oper types */ for (unsigned i = Oper::opers.size(); i > 0; --i) { Oper *o = Oper::opers[i - 1]; /* Oper's type is in the old config, so update it */ if (std::find(old->MyOperTypes.begin(), old->MyOperTypes.end(), o->ot) != old->MyOperTypes.end()) { OperType *ot = o->ot; o->ot = NULL; for (unsigned j = 0; j < MyOperTypes.size(); ++j) if (ot->GetName() == MyOperTypes[j]->GetName()) o->ot = MyOperTypes[j]; if (o->ot == NULL) { /* Oper block has lost type */ std::vector::iterator it = std::find(old->Opers.begin(), old->Opers.end(), o); if (it != old->Opers.end()) old->Opers.erase(it); it = std::find(this->Opers.begin(), this->Opers.end(), o); if (it != this->Opers.end()) this->Opers.erase(it); delete o; } } } } Block *Conf::GetModule(Module *m) { if (!m) return NULL; return GetModule(m->name); } Block *Conf::GetModule(const Anope::string &mname) { std::map::iterator it = modules.find(mname); if (it != modules.end()) return it->second; Block* &block = modules[mname]; /* Search for the block */ for (std::pair iters = blocks.equal_range("module"); iters.first != iters.second; ++iters.first) { Block *b = &iters.first->second; if (b->Get("name") == mname) { block = b; break; } } return GetModule(mname); } BotInfo *Conf::GetClient(const Anope::string &cname) { Anope::map::iterator it = bots.find(cname); if (it != bots.end()) return BotInfo::Find(!it->second.empty() ? it->second : cname, true); Block *block = GetModule(cname.lower()); const Anope::string &client = block->Get("client"); bots[cname] = client; return GetClient(cname); } Block *Conf::GetCommand(CommandSource &source) { const Anope::string &block_name = source.c ? "fantasy" : "command"; for (std::pair iters = blocks.equal_range(block_name); iters.first != iters.second; ++iters.first) { Block *b = &iters.first->second; if (b->Get("name") == source.command) return b; } return NULL; } File::File(const Anope::string &n, bool e) : name(n), executable(e), fp(NULL) { } File::~File() { this->Close(); } const Anope::string &File::GetName() const { return this->name; } Anope::string File::GetPath() const { return (this->executable ? "" : Anope::ConfigDir + "/") + this->name; } bool File::IsOpen() const { return this->fp != NULL; } bool File::Open() { this->Close(); this->fp = (this->executable ? popen(this->name.c_str(), "r") : fopen((Anope::ConfigDir + "/" + this->name).c_str(), "r")); return this->fp != NULL; } void File::Close() { if (this->fp != NULL) { if (this->executable) pclose(this->fp); else fclose(this->fp); this->fp = NULL; } } bool File::End() const { return !this->IsOpen() || feof(this->fp); } Anope::string File::Read() { Anope::string ret; char buf[BUFSIZE]; while (fgets(buf, sizeof(buf), this->fp) != NULL) { char *nl = strchr(buf, '\n'); if (nl != NULL) *nl = 0; else if (!this->End()) { ret += buf; continue; } ret = buf; break; } return ret; } void Conf::LoadConf(File &file) { if (file.GetName().empty()) return; if (!file.Open()) throw ConfigException("File " + file.GetPath() + " could not be opened."); Anope::string itemname, wordbuffer; std::stack block_stack; int linenumber = 0; bool in_word = false, in_quote = false, in_comment = false; Log(LOG_DEBUG) << "Start to read conf " << file.GetPath(); // Start reading characters... while (!file.End()) { Anope::string line = file.Read(); ++linenumber; /* If this line is completely empty and we are in a quote, just append a newline */ if (line.empty() && in_quote) wordbuffer += "\n"; for (unsigned c = 0, len = line.length(); c < len; ++c) { char ch = line[c]; if (in_quote) { /* Strip leading white spaces from multi line quotes */ if (c == 0) { while (c < len && isspace(line[c])) ++c; ch = line[c]; } /* Allow \" in quotes */ if (ch == '\\' && c + 1 < len && line[c + 1] == '"') wordbuffer += line[++c]; else if (ch == '"') in_quote = in_word = false; else if (ch) wordbuffer += ch; } else if (in_comment) { if (ch == '*' && c + 1 < len && line[c + 1] == '/') { in_comment = false; ++c; // We might be at an eol, so continue on and process it } else continue; } else if (ch == '#' || (ch == '/' && c + 1 < len && line[c + 1] == '/')) c = len - 1; // Line comment, ignore the rest of the line (much like this one!) else if (ch == '/' && c + 1 < len && line[c + 1] == '*') { // Multiline (or less than one line) comment in_comment = true; ++c; continue; } else if (!in_word && (ch == '(' || ch == '_' || ch == ')')) ; else if (ch == '"') { // Quotes are valid only in the value position if (block_stack.empty() || itemname.empty()) { file.Close(); throw ConfigException("Unexpected quoted string: " + file.GetName() + ":" + stringify(linenumber)); } if (in_word || !wordbuffer.empty()) { file.Close(); throw ConfigException("Unexpected quoted string (prior unhandled words): " + file.GetName() + ":" + stringify(linenumber)); } in_quote = in_word = true; } else if (ch == '=') { if (block_stack.empty()) { file.Close(); throw ConfigException("Config item outside of section (or stray '='): " + file.GetName() + ":" + stringify(linenumber)); } if (!itemname.empty() || wordbuffer.empty()) { file.Close(); throw ConfigException("Stray '=' sign or item without value: " + file.GetName() + ":" + stringify(linenumber)); } in_word = false; itemname = wordbuffer; wordbuffer.clear(); } else if (ch == '{') { if (wordbuffer.empty()) { block_stack.push(NULL); // Commented or unnamed section continue; } if (!block_stack.empty() && !block_stack.top()) { // Named block inside of a commented block in_word = false; wordbuffer.clear(); block_stack.push(NULL); continue; } Block *b = block_stack.empty() ? this : block_stack.top(); block_map::iterator it = b->blocks.insert(std::make_pair(wordbuffer, Configuration::Block(wordbuffer))); b = &it->second; b->linenum = linenumber; block_stack.push(b); in_word = false; wordbuffer.clear(); continue; } else if (ch == ' ' || ch == '\r' || ch == '\t') { // Terminate word in_word = false; } else if (ch == ';' || ch == '}') ; else { if (!in_word && !wordbuffer.empty()) { file.Close(); throw ConfigException("Unexpected word: " + file.GetName() + ":" + stringify(linenumber)); } wordbuffer += ch; in_word = true; } if (ch == ';' || ch == '}' || c + 1 >= len) { bool eol = c + 1 >= len; if (!eol && in_quote) // Allow ; and } in quoted strings continue; if (in_quote) { // Quotes can span multiple lines; all we need to do is go to the next line without clearing things wordbuffer += "\n"; continue; } in_word = false; if (!itemname.empty()) { if (block_stack.empty()) { file.Close(); throw ConfigException("Stray ';' outside of block: " + file.GetName() + ":" + stringify(linenumber)); } Block *b = block_stack.top(); if (b) Log(LOG_DEBUG) << "ln " << linenumber << " EOL: s='" << b->name << "' '" << itemname << "' set to '" << wordbuffer << "'"; /* Check defines */ for (int i = 0; i < this->CountBlock("define"); ++i) { Block *define = this->GetBlock("define", i); const Anope::string &dname = define->Get("name"); if (dname == wordbuffer && define != b) wordbuffer = define->Get("value"); } if (b) b->items[itemname] = wordbuffer; wordbuffer.clear(); itemname.clear(); } if (ch == '}') { if (block_stack.empty()) { file.Close(); throw ConfigException("Stray '}': " + file.GetName() + ":" + stringify(linenumber)); } block_stack.pop(); } } } } file.Close(); if (in_comment) throw ConfigException("Unterminated multiline comment at end of file: " + file.GetName()); if (in_quote) throw ConfigException("Unterminated quote at end of file: " + file.GetName()); if (!itemname.empty() || !wordbuffer.empty()) throw ConfigException("Unexpected garbage at end of file: " + file.GetName()); if (!block_stack.empty()) throw ConfigException("Unterminated block at end of file: " + file.GetName() + ". Block was opened on line " + stringify(block_stack.top()->linenum)); }