/*
 * Copyright (C) 1996-2023 The Squid Software Foundation and contributors
 *
 * Squid software is distributed under GPLv2+ license and includes
 * contributions from numerous individuals and organizations.
 * Please see the COPYING and CONTRIBUTORS files for details.
 */

/* DEBUG: none          Generate squid.conf.default and cf_parser.cci */

/*****************************************************************************
 * Abstract:    This program parses the input file and generates code and
 *      files used to configure the variables in squid.
 *      (ie it creates the squid.conf.default file from the cf.data file)
 *
 *      The output files are as follows:
 *      cf_parser.cci - this file contains, default_all() which
 *            initializes variables with the default
 *            values, parse_line() that parses line from
 *            squid.conf.default, dump_config that dumps the
 *            current the values of the variables.
 *      squid.conf.default - default configuration file given to the server
 *           administrator.
 *****************************************************************************/

/*
 * hack around a bug in intel's c++ compiler's libraries which do not
 * correctly support 64-bit iostreams
 */
#if defined(__INTEL_COMPILER) && defined(_FILE_OFFSET_BITS) && \
_FILE_OFFSET_BITS==64
#undef _FILE_OFFSET_BITS
#endif

#include <cassert>
#include <cctype>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <list>
#include <stack>

#include "cf_gen_defines.cci"

#define MAX_LINE    1024    /* longest configuration line */
#define _PATH_PARSER        "cf_parser.cci"
#define _PATH_SQUID_CONF    "squid.conf.documented"
#define _PATH_SQUID_CONF_SHORT  "squid.conf.default"
#define _PATH_CF_DEPEND     "cf.data.depend"

enum State {
    sSTART,
    s1,
    sDOC,
    sCFGLINES,
    sEXIT
};

typedef std::list<std::string> LineList;
typedef std::list<std::string> TypeDepList;
typedef std::list<std::string> EntryAliasList;

class DefaultValues
{
public:
    /// Default config lines to be defined before parsing the config files.
    LineList preset;

    /// Default config lines to parse if the directive has no prior settings.
    /// This is mutually exclusive with preset values.
    /// An error will be printed during build if they clash.
    LineList if_none;

    /// Default config lines to parse and add to any prior settings.
    LineList postscriptum;

    /// Text description to use in documentation for the default.
    /// If unset the preset or if-none values will be displayed.
    LineList docs;
};

class Entry
{
public:
    explicit Entry(const char *str) : name(str) {}

    std::string name;
    EntryAliasList alias;
    std::string type;
    std::string loc;
    DefaultValues defaults;
    std::string comment;
    std::string ifdef;
    LineList doc;
    LineList cfgLines; ///< between CONFIG_START and CONFIG_END
    int array_flag = 0; ///< TYPE is a raw array[] declaration

    void genParse(std::ostream &fout) const;

private:
    void genParseAlias(const std::string &, std::ostream &) const;
};

typedef std::list<class Entry> EntryList;

class Type
{
public:
    Type(const char *str) : name(str) {}

    std::string name;
    TypeDepList depend;
};

typedef std::list<class Type> TypeList;

static const char WS[] = " \t\n";
static int gen_default(const EntryList &, std::ostream &);
static void gen_parse(const EntryList &, std::ostream &);
static void gen_dump(const EntryList &, std::ostream&);
static void gen_free(const EntryList &, std::ostream&);
static void gen_conf(const EntryList &, std::ostream&, bool verbose_output);
static void gen_default_if_none(const EntryList &, std::ostream&);
static void gen_default_postscriptum(const EntryList &, std::ostream&);
static bool isDefined(const std::string &name);
static const char *available_if(const std::string &name);
static const char *gen_quote_escape(const std::string &var);

static void
checkDepend(const std::string &directive, const char *name, const TypeList &types, const EntryList &entries)
{
    for (const auto &t : types) {
        if (t.name.compare(name) != 0)
            continue;
        for (const auto &dep : t.depend) {
            EntryList::const_iterator entry = entries.begin();
            for (; entry != entries.end(); ++entry) {
                if (entry->name.compare(dep) == 0)
                    break;
            }
            if (entry == entries.end()) {
                std::cerr << "ERROR: '" << directive << "' (" << name << ") depends on '" << dep << "'\n";
                exit(EXIT_FAILURE);
            }
        }
        return;
    }
    std::cerr << "ERROR: Dependencies for cf.data type '" << name << "' used in ' " << directive << "' not defined\n" ;
    exit(EXIT_FAILURE);
}

static void
usage(const char *program_name)
{
    std::cerr << "Usage: " << program_name << " cf.data cf.data.depend\n";
    exit(EXIT_FAILURE);
}

static void
errorMsg(const char *filename, int line, const char *detail)
{
    std::cerr << "Error in '" << filename << "' on line " << line <<
              "--> " << detail << std::endl;
}

int
main(int argc, char *argv[])
{
    char *input_filename;
    const char *output_filename = _PATH_PARSER;
    const char *conf_filename = _PATH_SQUID_CONF;
    const char *conf_filename_short = _PATH_SQUID_CONF_SHORT;
    const char *type_depend;
    int linenum = 0;
    EntryList entries;
    TypeList types;
    enum State state;
    int rc = 0;
    char *ptr = nullptr;
    char buff[MAX_LINE];
    std::ifstream fp;
    std::stack<std::string> IFDEFS;

    if (argc != 3)
        usage(argv[0]);

    input_filename = argv[1];
    type_depend = argv[2];

    /*-------------------------------------------------------------------*
     * Parse type dependencies
     *-------------------------------------------------------------------*/
    fp.open(type_depend, std::ifstream::in);
    if (fp.fail()) {
        std::cerr << "Error while opening type dependencies file '" <<
                  type_depend << "': " << strerror(errno) << std::endl;
        exit(EXIT_FAILURE);
    }

    while (fp.good()) {
        fp.getline(buff,MAX_LINE);
        const char *type = strtok(buff, WS);
        const char *dep;
        if (!type || type[0] == '#')
            continue;
        Type t(type);
        while ((dep = strtok(nullptr, WS)) != nullptr) {
            t.depend.push_front(dep);
        }
        types.push_front(t);
    }
    fp.close();
    fp.clear(); // BSD does not reset flags in close().

    /*-------------------------------------------------------------------*
     * Parse input file
     *-------------------------------------------------------------------*/

    /* Open input file */
    fp.open(input_filename, std::ifstream::in);
    if (fp.fail()) {
        std::cerr << "Error while opening input file '" <<
                  input_filename << "': " << strerror(errno) << std::endl;
        exit(EXIT_FAILURE);
    }

    state = sSTART;

    while (fp.getline(buff,MAX_LINE), fp.good() && state != sEXIT) {
        char *t;

        ++linenum;

        if ((t = strchr(buff, '\n')))
            *t = '\0';

        if (strncmp(buff, "IF ", 3) == 0) {
            if ((ptr = strtok(buff + 3, WS)) == nullptr) {
                errorMsg(input_filename, linenum, "Missing IF parameter");
                exit(EXIT_FAILURE);
            }
            IFDEFS.push(ptr);
            continue;
        } else if (strcmp(buff, "ENDIF") == 0) {
            if (IFDEFS.size() == 0) {
                errorMsg(input_filename, linenum, "ENDIF without IF first");
                exit(EXIT_FAILURE);
            }
            IFDEFS.pop();
        } else if (!IFDEFS.size() || isDefined(IFDEFS.top()))
            switch (state) {

            case sSTART:

                if ((strlen(buff) == 0) || (!strncmp(buff, "#", 1))) {
                    /* ignore empty and comment lines */
                    (void) 0;
                } else if (!strncmp(buff, "NAME:", 5)) {
                    char *name, *aliasname;

                    if ((name = strtok(buff + 5, WS)) == nullptr) {
                        errorMsg(input_filename, linenum, buff);
                        exit(EXIT_FAILURE);
                    }

                    auto &newEntry = entries.emplace_back(name);

                    while ((aliasname = strtok(nullptr, WS)) != nullptr)
                        newEntry.alias.push_front(aliasname);

                    state = s1;
                } else if (!strcmp(buff, "EOF")) {
                    state = sEXIT;
                } else if (!strcmp(buff, "COMMENT_START")) {
                    auto &newEntry = entries.emplace_back("comment");
                    newEntry.loc = "none";
                    state = sDOC;
                } else {
                    errorMsg(input_filename, linenum, buff);
                    exit(EXIT_FAILURE);
                }

                break;

            case s1: {
                Entry &curr = entries.back();

                if ((strlen(buff) == 0) || (!strncmp(buff, "#", 1))) {
                    /* ignore empty and comment lines */
                    (void) 0;
                } else if (!strncmp(buff, "COMMENT:", 8)) {
                    ptr = buff + 8;

                    while (isspace((unsigned char)*ptr))
                        ++ptr;

                    curr.comment = ptr;
                } else if (!strncmp(buff, "DEFAULT:", 8)) {
                    ptr = buff + 8;

                    while (isspace((unsigned char)*ptr))
                        ++ptr;

                    curr.defaults.preset.push_back(ptr);
                } else if (!strncmp(buff, "DEFAULT_IF_NONE:", 16)) {
                    ptr = buff + 16;

                    while (isspace((unsigned char)*ptr))
                        ++ptr;

                    curr.defaults.if_none.push_back(ptr);
                } else if (!strncmp(buff, "POSTSCRIPTUM:", 13)) {
                    ptr = buff + 13;

                    while (isspace((unsigned char)*ptr))
                        ++ptr;

                    curr.defaults.postscriptum.push_back(ptr);
                } else if (!strncmp(buff, "DEFAULT_DOC:", 12)) {
                    ptr = buff + 12;

                    while (isspace((unsigned char)*ptr))
                        ++ptr;

                    curr.defaults.docs.push_back(ptr);
                } else if (!strncmp(buff, "LOC:", 4)) {
                    if ((ptr = strtok(buff + 4, WS)) == nullptr) {
                        errorMsg(input_filename, linenum, buff);
                        exit(EXIT_FAILURE);
                    }

                    curr.loc = ptr;
                } else if (!strncmp(buff, "TYPE:", 5)) {
                    if ((ptr = strtok(buff + 5, WS)) == nullptr) {
                        errorMsg(input_filename, linenum, buff);
                        exit(EXIT_FAILURE);
                    }

                    /* hack to support arrays, rather than pointers */
                    if (strcmp(ptr + strlen(ptr) - 2, "[]") == 0) {
                        curr.array_flag = 1;
                        *(ptr + strlen(ptr) - 2) = '\0';
                    }

                    checkDepend(curr.name, ptr, types, entries);
                    curr.type = ptr;
                } else if (!strncmp(buff, "IFDEF:", 6)) {
                    if ((ptr = strtok(buff + 6, WS)) == nullptr) {
                        errorMsg(input_filename, linenum, buff);
                        exit(EXIT_FAILURE);
                    }

                    curr.ifdef = ptr;
                } else if (!strcmp(buff, "DOC_START")) {
                    state = sDOC;
                } else if (!strcmp(buff, "DOC_NONE")) {
                    state = sSTART;
                } else {
                    errorMsg(input_filename, linenum, buff);
                    exit(EXIT_FAILURE);
                }
            }
            break;

            case sDOC:
                if (!strcmp(buff, "DOC_END") || !strcmp(buff, "COMMENT_END")) {
                    state = sSTART;
                } else if (strcmp(buff, "CONFIG_START") == 0) {
                    state = sCFGLINES;
                } else {
                    entries.back().doc.push_back(buff);
                }
                break;

            case sCFGLINES:
                if (strcmp(buff, "CONFIG_END") == 0) {
                    state = sDOC;
                } else {
                    entries.back().cfgLines.push_back(buff);
                }
                break;
            case sEXIT:
                assert(0);      /* should never get here */
                break;
            }

    }

    if (state != sEXIT) {
        errorMsg(input_filename, linenum, "Error: unexpected EOF");
        exit(EXIT_FAILURE);
    }

    fp.close();

    /*-------------------------------------------------------------------*
     * Generate default_all()
     * Generate parse_line()
     * Generate dump_config()
     * Generate free_all()
     * Generate example squid.conf.default file
     *-------------------------------------------------------------------*/

    /* Open output x.c file */

    std::ofstream fout(output_filename,std::ostream::out);
    if (!fout.good()) {
        std::cerr << "Error while opening output .c file '" <<
                  output_filename << "': " << strerror(errno) << std::endl;
        exit(EXIT_FAILURE);
    }

    fout <<  "/*\n" <<
         " * Generated automatically from " << input_filename << " by " <<
         argv[0] << "\n"
         " *\n"
         " * Abstract: This file contains routines used to configure the\n"
         " *           variables in the squid server.\n"
         " */\n"
         "\n";

    rc = gen_default(entries, fout);

    gen_default_if_none(entries, fout);

    gen_default_postscriptum(entries, fout);

    gen_parse(entries, fout);

    gen_dump(entries, fout);

    gen_free(entries, fout);

    fout.close();

    /* Open output x.conf file */
    fout.open(conf_filename,std::ostream::out);
    if (!fout.good()) {
        std::cerr << "Error while opening output conf file '" <<
                  output_filename << "': " << strerror(errno) << std::endl;
        exit(EXIT_FAILURE);
    }

    gen_conf(entries, fout, 1);

    fout.close();

    fout.open(conf_filename_short,std::ostream::out);
    if (!fout.good()) {
        std::cerr << "Error while opening output short conf file '" <<
                  output_filename << "': " << strerror(errno) << std::endl;
        exit(EXIT_FAILURE);
    }
    gen_conf(entries, fout, 0);
    fout.close();

    return (rc);
}

static int
gen_default(const EntryList &head, std::ostream &fout)
{
    int rc = 0;
    fout << "static void" << std::endl <<
         "default_line(const char *s)" << std::endl <<
         "{" << std::endl <<
         "    char *tmp_line = xstrdup(s);" << std::endl <<
         "    int len = strlen(tmp_line);" << std::endl <<
         "    ProcessMacros(tmp_line, len);" << std::endl <<
         "    xstrncpy(config_input_line, tmp_line, sizeof(config_input_line));" << std::endl <<
         "    config_lineno++;" << std::endl <<
         "    parse_line(tmp_line);" << std::endl <<
         "    xfree(tmp_line);" << std::endl <<
         "}" << std::endl << std::endl;
    fout << "static void" << std::endl <<
         "default_all(void)" << std::endl <<
         "{" << std::endl <<
         "    cfg_filename = \"Default Configuration\";" << std::endl <<
         "    config_lineno = 0;" << std::endl;

    for (const auto &entry : head) {
        assert(entry.name.size());

        if (!entry.name.compare("comment"))
            continue;

        if (!entry.type.compare("obsolete"))
            continue;

        if (!entry.loc.size()) {
            std::cerr << "NO LOCATION FOR " << entry.name << std::endl;
            rc |= 1;
            continue;
        }

        if (!entry.defaults.preset.size() && entry.defaults.if_none.empty()) {
            std::cerr << "NO DEFAULT FOR " << entry.name << std::endl;
            rc |= 1;
            continue;
        }

        if (!entry.defaults.preset.size() || entry.defaults.preset.front().compare("none") == 0) {
            fout << "    // No default for " << entry.name << std::endl;
        } else {
            if (entry.ifdef.size())
                fout << "#if " << entry.ifdef << std::endl;

            for (const auto &l : entry.defaults.preset)
                fout << "    default_line(\"" << entry.name << " " << gen_quote_escape(l) << "\");" << std::endl;

            if (entry.ifdef.size())
                fout << "#endif" << std::endl;
        }
    }

    fout << "    cfg_filename = nullptr;" << std::endl <<
         "}" << std::endl << std::endl;
    return rc;
}

static void
gen_default_if_none(const EntryList &head, std::ostream &fout)
{
    fout << "static void" << std::endl <<
         "defaults_if_none(void)" << std::endl <<
         "{" << std::endl <<
         "    cfg_filename = \"Default Configuration (if absent)\";" << std::endl <<
         "    config_lineno = 0;" << std::endl;

    for (const auto &entry : head) {
        assert(entry.name.size());

        if (!entry.loc.size())
            continue;

        if (entry.defaults.if_none.empty())
            continue;

        if (!entry.defaults.preset.empty()) {
            std::cerr << "ERROR: " << entry.name << " has preset defaults. DEFAULT_IF_NONE cannot be true." << std::endl;
            exit(EXIT_FAILURE);
        }

        if (entry.ifdef.size())
            fout << "#if " << entry.ifdef << std::endl;

        fout << "    if (check_null_" << entry.type << "(" << entry.loc << ")) {" << std::endl;
        for (const auto &l : entry.defaults.if_none)
            fout << "        default_line(\"" << entry.name << " " << gen_quote_escape(l) <<"\");" << std::endl;
        fout << "    }" << std::endl;

        if (entry.ifdef.size())
            fout << "#endif" << std::endl;
    }

    fout << "    cfg_filename = nullptr;" << std::endl <<
         "}" << std::endl << std::endl;
}

/// append configuration options specified by POSTSCRIPTUM lines
static void
gen_default_postscriptum(const EntryList &head, std::ostream &fout)
{
    fout << "static void" << std::endl <<
         "defaults_postscriptum(void)" << std::endl <<
         "{" << std::endl <<
         "    cfg_filename = \"Default Configuration (postscriptum)\";" << std::endl <<
         "    config_lineno = 0;" << std::endl;

    for (const auto &entry : head) {
        assert(entry.name.size());

        if (!entry.loc.size())
            continue;

        if (entry.defaults.postscriptum.empty())
            continue;

        if (entry.ifdef.size())
            fout << "#if " << entry.ifdef << std::endl;

        for (const auto &l : entry.defaults.postscriptum)
            fout << "    default_line(\"" << entry.name << " " << l <<"\");" << std::endl;

        if (entry.ifdef.size())
            fout << "#endif" << std::endl;
    }

    fout << "    cfg_filename = nullptr;" << std::endl <<
         "}" << std::endl << std::endl;
}

void
Entry::genParseAlias(const std::string &aName, std::ostream &fout) const
{
    fout << "    if (!strcmp(token, \"" << aName << "\")) {" << std::endl;
    if (ifdef.size())
        fout << "#if " << ifdef << std::endl;
    fout << "        cfg_directive = \"" << aName << "\";" << std::endl;
    fout << "        ";
    if (type.compare("obsolete") == 0) {
        fout << "debugs(0, DBG_CRITICAL, \"ERROR: Directive '" << aName << "' is obsolete.\");\n";
        for (const auto &l : doc) {
            // offset line to strip initial whitespace tab byte
            fout << "        debugs(0, DBG_PARSE_NOTE(DBG_IMPORTANT), \"" << aName << " : " << &l[1] << "\");" << std::endl;
        }
        fout << "        parse_obsolete(token);";
    } else if (!loc.size() || loc.compare("none") == 0) {
        fout << "parse_" << type << "();";
    } else if (type.find("::") != std::string::npos) {
        fout << "ParseDirective<" << type << ">(" << loc << ", LegacyParser);";
    } else {
        fout << "parse_" << type << "(&" << loc << (array_flag ? "[0]" : "") << ");";
    }
    fout << std::endl;
    fout << "        cfg_directive = nullptr;" << std::endl;
    if (ifdef.size()) {
        fout <<
             "#else" << std::endl <<
             "    debugs(0, DBG_PARSE_NOTE(DBG_IMPORTANT), \"ERROR: '" << name << "' requires " << available_if(ifdef) << "\");" << std::endl <<
             "#endif" << std::endl;
    }
    fout << "        return 1;" << std::endl;
    fout << "    };" << std::endl;
}

void
Entry::genParse(std::ostream &fout) const
{
    if (name.compare("comment") == 0)
        return;

    // Once for the current directive name
    genParseAlias(name, fout);

    // All accepted aliases
    for (const auto &a : alias) {
        genParseAlias(a, fout);
    }
}

static void
gen_parse(const EntryList &head, std::ostream &fout)
{
    fout <<
         "static int\n"
         "parse_line(char *buff)\n"
         "{\n"
         "\tchar\t*token;\n"
         "\tif ((token = strtok(buff, w_space)) == NULL) \n"
         "\t\treturn 1;\t/* ignore empty lines */\n"
         "\tConfigParser::SetCfgLine(strtok(nullptr, \"\"));\n";

    for (const auto &e : head)
        e.genParse(fout);

    fout << "\treturn 0; /* failure */\n"
         "}\n\n";

}

static void
gen_dump(const EntryList &head, std::ostream &fout)
{
    fout <<
         "static void" << std::endl <<
         "dump_config(StoreEntry *entry)" << std::endl <<
         "{" << std::endl <<
         "    debugs(5, 4, MYNAME);" << std::endl;

    for (const auto &e : head) {

        if (!e.loc.size() || e.loc.compare("none") == 0)
            continue;

        if (e.name.compare("comment") == 0)
            continue;

        if (e.ifdef.size())
            fout << "#if " << e.ifdef << std::endl;

        if (e.type.find("::") != std::string::npos)
            fout << "    DumpDirective<" << e.type << ">(" << e.loc << ", entry, \"" << e.name << "\");\n";
        else
            fout << "    dump_" << e.type << "(entry, \"" << e.name << "\", " << e.loc << ");" << std::endl;

        if (e.ifdef.size())
            fout << "#endif" << std::endl;
    }

    fout << "}" << std::endl << std::endl;
}

static void
gen_free(const EntryList &head, std::ostream &fout)
{
    fout <<
         "static void" << std::endl <<
         "free_all(void)" << std::endl <<
         "{" << std::endl <<
         "    debugs(5, 4, MYNAME);" << std::endl;

    for (const auto &e : head) {
        if (!e.loc.size() || e.loc.compare("none") == 0)
            continue;

        if (e.name.compare("comment") == 0)
            continue;

        if (e.ifdef.size())
            fout << "#if " << e.ifdef << std::endl;

        if (e.type.find("::") != std::string::npos)
            fout << "    FreeDirective<" << e.type << ">(" << e.loc << ");\n";
        else
            fout << "    free_" << e.type << "(&" << e.loc << (e.array_flag ? "[0]" : "") << ");" << std::endl;

        if (e.ifdef.size())
            fout << "#endif" << std::endl;
    }

    fout << "}" << std::endl << std::endl;
}

static bool
isDefined(const std::string &name)
{
    if (!name.size())
        return true;

    for (int i = 0; defines[i].name; ++i) {
        if (name.compare(defines[i].name) == 0)
            return defines[i].defined;
    }

    return false;
}

static const char *
available_if(const std::string &name)
{
    assert(name.size());

    for (int i = 0; defines[i].name; ++i) {
        if (name.compare(defines[i].name) == 0)
            return defines[i].enable;
    }

    return name.c_str();
}

static void
gen_conf(const EntryList &head, std::ostream &fout, bool verbose_output)
{
    for (const auto &entry : head) {
        char buf[8192];
        LineList def;
        int enabled = 1;

        // Display TAG: line
        if (!entry.name.compare("comment"))
            (void) 0;
        else if (!entry.name.compare("obsolete"))
            (void) 0;
        else if (verbose_output) {
            fout << "#  TAG: " << entry.name;

            if (entry.comment.size())
                fout << "\t" << entry.comment;

            fout << std::endl;
        }

        // Display --enable/--disable disclaimer
        if (!isDefined(entry.ifdef)) {
            if (verbose_output) {
                fout << "# Note: This option is only available if Squid is rebuilt with the" << std::endl <<
                     "#       " << available_if(entry.ifdef) << std::endl <<
                     "#" << std::endl;
            }
            enabled = 0;
        }

        // Display DOC_START section
        if (verbose_output && entry.doc.size()) {
            for (const auto &line : entry.doc) {
                fout << "#" << line << std::endl;
            }
        }

        if (entry.defaults.docs.size()) {
            // Display the DEFAULT_DOC line(s)
            def = entry.defaults.docs;
        } else {
            if (entry.defaults.preset.size() && entry.defaults.preset.front().compare("none") != 0) {
                // Display DEFAULT: line(s)
                for (const auto &l : entry.defaults.preset) {
                    snprintf(buf, sizeof(buf), "%s %s", entry.name.c_str(), l.c_str());
                    def.push_back(buf);
                }
            } else if (entry.defaults.if_none.size()) {
                // Display DEFAULT_IF_NONE: line(s)
                for (const auto &l : entry.defaults.if_none) {
                    snprintf(buf, sizeof(buf), "%s %s", entry.name.c_str(), l.c_str());
                    def.push_back(buf);
                }
            }
        }

        // Display "none" if no default is set or comments to display
        if (def.empty() && entry.cfgLines.empty() && entry.name.compare("comment") != 0)
            def.push_back("none");

        if (verbose_output && def.size()) {
            fout << "#Default:\n";
            while (def.size()) {
                fout << "# " << def.front() << std::endl;
                def.pop_front();
            }
            if (entry.doc.empty() && entry.cfgLines.empty())
                fout << std::endl;
        }

        if (verbose_output && entry.cfgLines.size())
            fout << "#" << std::endl;

        if (enabled || verbose_output) {
            for (const auto &line : entry.cfgLines) {
                if (!enabled && line.at(0) != '#')
                    fout << "#";
                fout << line << std::endl;
            }
        }

        if (verbose_output && entry.doc.size()) {
            fout << std::endl;
        }
    }
}

static const char *
gen_quote_escape(const std::string &var)
{
    static std::string esc;
    esc.clear();

    for (const auto c : var) {
        switch (c) {
        case '"':
        case '\\':
            esc += '\\';
            [[fallthrough]];
        default:
            esc += c;
        }
    }

    return esc.c_str();
}

