/*
 * D-Bus XML Config Parser
 */

#include <c-list.h>
#include <c-stdaux.h>
#include <expat.h>
#include <stdlib.h>
#include "dbus/protocol.h"
#include "launch/config.h"
#include "launch/nss-cache.h"
#include "util/common.h"
#include "util/dirwatch.h"
#include "util/error.h"
#include "util/selinux.h"
#include "util/string.h"

static_assert(__builtin_types_compatible_p(XML_Char, char),
              "Missing UTF-8 support in expat");

/* Print an error message to stderr including file+line information */
#define CONFIG_ERR(_state, _intro, _outro, ...)                                 \
        fprintf(stderr,                                                         \
                _intro " in %s +%lu" _outro "\n",                               \
                (_state)->file->path,                                           \
                XML_GetCurrentLineNumber(                                       \
                        c_container_of((_state), ConfigParser, state)->xml),    \
                ## __VA_ARGS__)

/**
 * config_path_new() - XXX
 */
int config_path_new(ConfigPath **filep, ConfigPath *parent, const char *prefix, const char *path) {
        _c_cleanup_(config_path_unrefp) ConfigPath *file = NULL;
        size_t n_path, n_prefix;
        char *t;

        n_path = strlen(path);
        n_prefix = 0;

        /* prepend parent-path if @path is relative */
        if (path[0] != '/') {
                if (prefix) {
                        n_prefix = strlen(prefix);
                } else if (parent) {
                        if (parent->is_dir) {
                                prefix = parent->path;
                                n_prefix = strlen(parent->path);
                        } else {
                                t = strrchr(parent->path, '/');
                                if (t) {
                                        prefix = parent->path;
                                        n_prefix = t - parent->path;
                                }
                        }
                }
        }

        file = calloc(1, sizeof(*file) + n_prefix + 1 + n_path + 1);
        if (!file)
                return error_origin(-ENOMEM);

        *file = (ConfigPath)CONFIG_FILE_NULL(*file);
        file->parent = config_path_ref(parent);

        if (n_prefix) {
                c_memcpy(file->path, prefix, n_prefix);
                file->path[n_prefix] = '/';
                c_memcpy(file->path + n_prefix + 1, path, n_path + 1);
        } else {
                c_memcpy(file->path, path, n_path + 1);
        }

        *filep = file;
        file = NULL;
        return 0;
}

static int config_path_new_dir(ConfigPath **filep, ConfigPath *parent, const char *path) {
        int r;

        r = config_path_new(filep, parent, NULL, path);
        if (!r)
                (*filep)->is_dir = true;

        return r;
}

/**
 * config_path_ref() - XXX
 */
ConfigPath *config_path_ref(ConfigPath *file) {
        if (file)
                ++file->n_refs;
        return file;
}

/**
 * config_path_unref() - XXX
 */
ConfigPath *config_path_unref(ConfigPath *file) {
        ConfigPath *parent;

        if (!file || --file->n_refs)
                return NULL;

        parent = file->parent;
        free(file);

        return config_path_unref(parent);
}

/**
 * config_node_new() - XXX
 */
int config_node_new(ConfigNode **nodep, ConfigNode *parent, unsigned int type) {
        _c_cleanup_(config_node_freep) ConfigNode *node = NULL;

        node = calloc(1, sizeof(*node));
        if (!node)
                return error_origin(-ENOMEM);

        *node = (ConfigNode)CONFIG_NODE_NULL(*node);
        node->parent = parent;
        if (parent)
                ++parent->n_children;
        node->type = type;

        switch (node->type) {
        case CONFIG_NODE_ALLOW:
        case CONFIG_NODE_DENY:
                node->allow_deny.max_fds = UINT64_MAX;
                break;
        }

        node->cdata = strdup("");
        if (!node->cdata)
                return error_origin(-ENOMEM);

        *nodep = node;
        node = NULL;
        return 0;
}

/**
 * config_node_free() - XXX
 */
ConfigNode *config_node_free(ConfigNode *node) {
        if (!node)
                return NULL;

        switch (node->type) {
        case CONFIG_NODE_TYPE:
                free(node->bustype.name);
                break;
        case CONFIG_NODE_INCLUDEDIR:
                config_path_unref(node->includedir.dir);
                break;
        case CONFIG_NODE_INCLUDE:
                config_path_unref(node->include.file);
                break;
        case CONFIG_NODE_SERVICEDIR:
                free(node->servicedir.path);
                break;
        case CONFIG_NODE_ALLOW:
        case CONFIG_NODE_DENY:
                free(node->allow_deny.send_interface);
                free(node->allow_deny.send_member);
                free(node->allow_deny.send_error);
                free(node->allow_deny.send_destination);
                free(node->allow_deny.send_path);
                free(node->allow_deny.recv_interface);
                free(node->allow_deny.recv_member);
                free(node->allow_deny.recv_error);
                free(node->allow_deny.recv_sender);
                free(node->allow_deny.recv_path);
                free(node->allow_deny.own);
                free(node->allow_deny.own_prefix);
                break;
        case CONFIG_NODE_ASSOCIATE:
                free(node->associate.own);
                free(node->associate.context);
                break;
        }

        free(node->cdata);
        config_path_unref(node->path);

        c_assert(!node->n_children);
        if (node->parent)
                --node->parent->n_children;

        c_list_unlink(&node->include_link);
        c_list_unlink(&node->root_link);
        free(node);

        return NULL;
}

/**
 * config_root_new() - XXX
 */
int config_root_new(ConfigRoot **rootp) {
        _c_cleanup_(config_root_freep) ConfigRoot *root = NULL;

        root = calloc(1, sizeof(*root));
        if (!root)
                return error_origin(-ENOMEM);

        *root = (ConfigRoot)CONFIG_ROOT_NULL(*root);

        *rootp = root;
        root = NULL;
        return 0;
}

/**
 * config_root_free() - XXX
 */
ConfigRoot *config_root_free(ConfigRoot *root) {
        ConfigNode *i_node;

        if (!root)
                return NULL;

        while ((i_node = c_list_last_entry(&root->node_list, ConfigNode, root_link)))
                config_node_free(i_node);

        c_assert(c_list_is_empty(&root->node_list));
        c_assert(c_list_is_empty(&root->include_list));

        free(root);

        return NULL;
}

static int config_parser_attrs_include(ConfigState *state, ConfigNode *node, const XML_Char **attrs) {
        const char *k, *v;

        while (*attrs) {
                k = *(attrs++);
                v = *(attrs++);

                if (!strcmp(k, "ignore_missing")) {
                        if (!strcmp(v, "yes"))
                                node->include.ignore_missing = true;
                        else if (!strcmp(v, "no"))
                                node->include.ignore_missing = false;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "if_selinux_enabled")) {
                        if (!strcmp(v, "yes"))
                                node->include.if_selinux_enabled = true;
                        else if (!strcmp(v, "no"))
                                node->include.if_selinux_enabled = false;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "selinux_root_relative")) {
                        if (!strcmp(v, "yes"))
                                node->include.selinux_root_relative = true;
                        else if (!strcmp(v, "no"))
                                node->include.selinux_root_relative = false;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else {
                        CONFIG_ERR(state, "Unknown attribute", ": %s=\"%s\"", k, v);
                }
        }

        return 0;
}

static int config_parser_attrs_policy(ConfigState *state, ConfigNode *node, const XML_Char **attrs) {
        const char *k, *v;
        int r;

        while (*attrs) {
                k = *(attrs++);
                v = *(attrs++);

                if (!strcmp(k, "user")) {
                        if (node->policy.context)
                                CONFIG_ERR(state, "Conflicting attributes", "");

                        r = nss_cache_get_uid(state->nss, &node->policy.id, NULL, v);
                        if (r) {
                                if (r == NSS_CACHE_E_INVALID_NAME) {
                                        CONFIG_ERR(state, "Invalid user-name", ": %s=\"%s\"", k, v);
                                        node->policy.id = (uint32_t)-1;
                                } else {
                                        return error_fold(r);
                                }
                        }

                        node->policy.context = CONFIG_POLICY_USER;
                } else if (!strcmp(k, "group")) {
                        if (node->policy.context)
                                CONFIG_ERR(state, "Conflicting attributes", "");

                        r = nss_cache_get_gid(state->nss, &node->policy.id, v);
                        if (r) {
                                if (r == NSS_CACHE_E_INVALID_NAME) {
                                        CONFIG_ERR(state, "Invalid group-name", ": %s=\"%s\"", k, v);
                                        node->policy.id = (uint32_t)-1;
                                } else {
                                        return error_fold(r);
                                }
                        }

                        node->policy.context = CONFIG_POLICY_GROUP;
                } else if (!strcmp(k, "context")) {
                        if (node->policy.context)
                                CONFIG_ERR(state, "Conflicting attributes", "");

                        if (!strcmp(v, "mandatory"))
                                node->policy.context = CONFIG_POLICY_MANDATORY;
                        else if (!strcmp(v, "default"))
                                node->policy.context = CONFIG_POLICY_DEFAULT;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "at_console")) {
                        if (node->policy.context)
                                CONFIG_ERR(state, "Conflicting attributes", "");

                        if (!strcmp(v, "true"))
                                node->policy.context = CONFIG_POLICY_AT_CONSOLE;
                        else if (!strcmp(v, "false"))
                                node->policy.context = CONFIG_POLICY_NO_CONSOLE;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else {
                        CONFIG_ERR(state, "Unknown attribute", ": %s=\"%s\"", k, v);
                }
        }

        if (!node->policy.context)
                CONFIG_ERR(state, "Missing attribute", "");

        return 0;
}

static int config_parser_attrs_limit(ConfigState *state, ConfigNode *node, const XML_Char **attrs) {
        const char *k, *v;

        while (*attrs) {
                k = *(attrs++);
                v = *(attrs++);

                if (!strcmp(k, "name")) {
                        if (!strcmp(v, "max_incoming_bytes"))
                                node->limit.name = CONFIG_LIMIT_MAX_INCOMING_BYTES;
                        else if (!strcmp(v, "max_incoming_unix_fds"))
                                node->limit.name = CONFIG_LIMIT_MAX_INCOMING_UNIX_FDS;
                        else if (!strcmp(v, "max_outgoing_bytes"))
                                node->limit.name = CONFIG_LIMIT_MAX_OUTGOING_BYTES;
                        else if (!strcmp(v, "max_outgoing_unix_fds"))
                                node->limit.name = CONFIG_LIMIT_MAX_OUTGOING_UNIX_FDS;
                        else if (!strcmp(v, "max_message_size"))
                                node->limit.name = CONFIG_LIMIT_MAX_MESSAGE_SIZE;
                        else if (!strcmp(v, "max_message_unix_fds"))
                                node->limit.name = CONFIG_LIMIT_MAX_MESSAGE_UNIX_FDS;
                        else if (!strcmp(v, "service_start_timeout"))
                                node->limit.name = CONFIG_LIMIT_SERVICE_START_TIMEOUT;
                        else if (!strcmp(v, "auth_timeout"))
                                node->limit.name = CONFIG_LIMIT_AUTH_TIMEOUT;
                        else if (!strcmp(v, "pending_fd_timeout"))
                                node->limit.name = CONFIG_LIMIT_PENDING_FD_TIMEOUT;
                        else if (!strcmp(v, "max_completed_connections"))
                                node->limit.name = CONFIG_LIMIT_MAX_COMPLETED_CONNECTIONS;
                        else if (!strcmp(v, "max_incomplete_connections"))
                                node->limit.name = CONFIG_LIMIT_MAX_INCOMPLETE_CONNECTIONS;
                        else if (!strcmp(v, "max_connections_per_user"))
                                node->limit.name = CONFIG_LIMIT_MAX_CONNECTIONS_PER_USER;
                        else if (!strcmp(v, "max_pending_service_starts"))
                                node->limit.name = CONFIG_LIMIT_MAX_PENDING_SERVICE_STARTS;
                        else if (!strcmp(v, "max_names_per_connection"))
                                node->limit.name = CONFIG_LIMIT_MAX_NAMES_PER_CONNECTION;
                        else if (!strcmp(v, "max_match_rules_per_connection"))
                                node->limit.name = CONFIG_LIMIT_MAX_MATCH_RULES_PER_CONNECTION;
                        else if (!strcmp(v, "max_replies_per_connection"))
                                node->limit.name = CONFIG_LIMIT_MAX_REPLIES_PER_CONNECTION;
                        else if (!strcmp(v, "max_containers_per_user"))
                                node->limit.name = CONFIG_LIMIT_MAX_CONTAINERS_PER_USER;
                        else if (!strcmp(v, "max_containers"))
                                node->limit.name = CONFIG_LIMIT_MAX_CONTAINERS;
                        else if (!strcmp(v, "max_connections_per_container"))
                                node->limit.name = CONFIG_LIMIT_MAX_CONNECTIONS_PER_CONTAINER;
                        else if (!strcmp(v, "max_container_metadata_bytes"))
                                node->limit.name = CONFIG_LIMIT_MAX_CONTAINER_METADATA_BYTES;
                        else if (!strcmp(v, "reply_timeout"))
                                node->limit.name = CONFIG_LIMIT_REPLY_TIMEOUT;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else {
                        CONFIG_ERR(state, "Unknown attribute", ": %s=\"%s\"", k, v);
                }
        }

        if (!node->limit.name)
                CONFIG_ERR(state, "Required attribute 'name' missing", "");

        return 0;
}

static int config_parser_attrs_apparmor(ConfigState *state, ConfigNode *node, const XML_Char **attrs) {
        const char *k, *v;

        while (*attrs) {
                k = *(attrs++);
                v = *(attrs++);

                if (!strcmp(k, "mode")) {
                        if (!strcmp(v, "enabled"))
                                node->apparmor.mode = CONFIG_APPARMOR_ENABLED;
                        else if (!strcmp(v, "disabled"))
                                node->apparmor.mode = CONFIG_APPARMOR_DISABLED;
                        else if (!strcmp(v, "required"))
                                node->apparmor.mode = CONFIG_APPARMOR_REQUIRED;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else {
                        CONFIG_ERR(state, "Unknown attribute", ": %s=\"%s\"", k, v);
                }
        }

        return 0;
}

static int config_parser_attrs_allow_deny(ConfigState *state, ConfigNode *node, const XML_Char **attrs) {
        const char *k, *v;
        char *t;
        int r;

        while (*attrs) {
                k = *(attrs++);
                v = *(attrs++);

                if (!strcmp(k, "send_interface")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.send_interface);
                        node->allow_deny.send_interface = t;
                } else if (!strcmp(k, "send_member")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.send_member);
                        node->allow_deny.send_member = t;
                } else if (!strcmp(k, "send_error")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.send_error);
                        node->allow_deny.send_error = t;
                } else if (!strcmp(k, "send_destination")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.send_destination);
                        node->allow_deny.send_destination = t;
                } else if (!strcmp(k, "send_path")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.send_path);
                        node->allow_deny.send_path = t;
                } else if (!strcmp(k, "send_type")) {
                        if (!strcmp(v, "method_call"))
                                node->allow_deny.send_type = DBUS_MESSAGE_TYPE_METHOD_CALL;
                        else if (!strcmp(v, "method_return"))
                                node->allow_deny.send_type = DBUS_MESSAGE_TYPE_METHOD_RETURN;
                        else if (!strcmp(v, "signal"))
                                node->allow_deny.send_type = DBUS_MESSAGE_TYPE_SIGNAL;
                        else if (!strcmp(v, "error"))
                                node->allow_deny.send_type = DBUS_MESSAGE_TYPE_ERROR;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "send_broadcast")) {
                        if (!strcmp(v, "false"))
                                node->allow_deny.send_broadcast = UTIL_TRISTATE_NO;
                        else if (!strcmp(v, "true"))
                                node->allow_deny.send_broadcast = UTIL_TRISTATE_YES;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "receive_interface")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.recv_interface);
                        node->allow_deny.recv_interface = t;
                } else if (!strcmp(k, "receive_member")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.recv_member);
                        node->allow_deny.recv_member = t;
                } else if (!strcmp(k, "receive_error")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.recv_error);
                        node->allow_deny.recv_error = t;
                } else if (!strcmp(k, "receive_sender")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.recv_sender);
                        node->allow_deny.recv_sender = t;
                } else if (!strcmp(k, "receive_path")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.recv_path);
                        node->allow_deny.recv_path = t;
                } else if (!strcmp(k, "receive_type")) {
                        if (!strcmp(v, "method_call"))
                                node->allow_deny.recv_type = DBUS_MESSAGE_TYPE_METHOD_CALL;
                        else if (!strcmp(v, "method_return"))
                                node->allow_deny.recv_type = DBUS_MESSAGE_TYPE_METHOD_RETURN;
                        else if (!strcmp(v, "signal"))
                                node->allow_deny.recv_type = DBUS_MESSAGE_TYPE_SIGNAL;
                        else if (!strcmp(v, "error"))
                                node->allow_deny.recv_type = DBUS_MESSAGE_TYPE_ERROR;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "own")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.own);
                        node->allow_deny.own = t;
                } else if (!strcmp(k, "own_prefix")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->allow_deny.own_prefix);
                        node->allow_deny.own_prefix = t;
                } else if (!strcmp(k, "min_fds")) {
                        r = util_strtou64(&node->allow_deny.min_fds, v);
                        if (r)
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "max_fds")) {
                        r = util_strtou64(&node->allow_deny.max_fds, v);
                        if (r)
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "user")) {
                        if (!strcmp(v, "*")) {
                                node->allow_deny.uid = (uint32_t)-1;
                                node->allow_deny.user = true;
                        } else {
                                r = nss_cache_get_uid(state->nss, &node->allow_deny.uid, NULL, v);
                                if (r) {
                                        if (r == NSS_CACHE_E_INVALID_NAME) {
                                                CONFIG_ERR(state, "Invalid user-name", ": %s=\"%s\"", k, v);
                                                continue;
                                        }

                                        return error_fold(r);
                                }

                                node->allow_deny.user = true;
                        }
                } else if (!strcmp(k, "group")) {
                        if (!strcmp(v, "*")) {
                                node->allow_deny.gid = (uint32_t)-1;
                                node->allow_deny.group = true;
                        } else {
                                r = nss_cache_get_gid(state->nss, &node->allow_deny.gid, v);
                                if (r) {
                                        if (r == NSS_CACHE_E_INVALID_NAME) {
                                                CONFIG_ERR(state, "Invalid group-name", ": %s=\"%s\"", k, v);
                                                continue;
                                        }

                                        return error_fold(r);
                                }

                                node->allow_deny.group = true;
                        }
                } else if (!strcmp(k, "send_requested_reply")) {
                        if (!strcmp(v, "true"))
                                node->allow_deny.send_requested_reply = UTIL_TRISTATE_YES;
                        else if (!strcmp(v, "false"))
                                node->allow_deny.send_requested_reply = UTIL_TRISTATE_NO;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "receive_requested_reply")) {
                        if (!strcmp(v, "true"))
                                node->allow_deny.recv_requested_reply = UTIL_TRISTATE_YES;
                        else if (!strcmp(v, "false"))
                                node->allow_deny.recv_requested_reply = UTIL_TRISTATE_NO;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "eavesdrop")) {
                        if (!strcmp(v, "true"))
                                node->allow_deny.eavesdrop = UTIL_TRISTATE_YES;
                        else if (!strcmp(v, "false"))
                                node->allow_deny.eavesdrop = UTIL_TRISTATE_NO;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else if (!strcmp(k, "log")) {
                        if (!strcmp(v, "true"))
                                node->allow_deny.log = true;
                        else if (!strcmp(v, "false"))
                                node->allow_deny.log = false;
                        else
                                CONFIG_ERR(state, "Invalid value", ": %s=\"%s\"", k, v);
                } else {
                        CONFIG_ERR(state, "Unknown attribute", ": %s=\"%s\"", k, v);
                }
        }

        return 0;
}

static int config_parser_attrs_associate(ConfigState *state, ConfigNode *node, const XML_Char **attrs) {
        const char *k, *v;
        char *t;

        while (*attrs) {
                k = *(attrs++);
                v = *(attrs++);

                if (!strcmp(k, "own")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->associate.own);
                        node->associate.own = t;
                } else if (!strcmp(k, "context")) {
                        t = strdup(v);
                        if (!t)
                                return error_origin(-ENOMEM);

                        free(node->associate.context);
                        node->associate.context = t;
                } else {
                        CONFIG_ERR(state, "Unknown attribute", ": %s=\"%s\"", k, v);
                }
        }

        if (!node->associate.own)
                CONFIG_ERR(state, "Required attribute 'own' missing", "");
        if (!node->associate.context)
                CONFIG_ERR(state, "Required attribute 'context' missing", "");

        return 0;
}

static int config_parser_attrs_default(ConfigState *state, ConfigNode *node, const XML_Char **attrs) {
        const char *k, *v;

        while (*attrs) {
                k = *(attrs++);
                v = *(attrs++);

                CONFIG_ERR(state, "Unknown attribute", ": %s=\"%s\"", k, v);
        }

        return 0;
}

static void config_parser_begin_fn(void *userdata, const XML_Char *name, const XML_Char **attrs) {
        _c_cleanup_(config_node_freep) ConfigNode *node = NULL;
        ConfigState *state = userdata;
        int r = 0;

        c_assert(state->current);

        /*
         * Whenever we hit a fatal error, we remember it in @state and simply
         * shortcut everything. However, when we just hit unknown tags, we
         * track our nesting depth and ignore anything underneath that tag, but
         * continue parsing once we are back at a known state.
         */
        if (state->error)
                return;
        if (state->n_failed) {
                ++state->n_failed;
                return;
        }

        if (!strcmp(name, "busconfig")) {

                if (state->n_depth)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_BUSCONFIG);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "user")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_USER);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "type")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_TYPE);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "fork")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_FORK);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "syslog")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_SYSLOG);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "keep_umask")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_KEEP_UMASK);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "listen")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_LISTEN);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "pidfile")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_PIDFILE);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "includedir")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_INCLUDEDIR);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "standard_session_servicedirs")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_STANDARD_SESSION_SERVICEDIRS);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "standard_system_servicedirs")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_STANDARD_SYSTEM_SERVICEDIRS);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "servicedir")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_SERVICEDIR);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "servicehelper")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_SERVICEHELPER);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "auth")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_AUTH);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "include")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_INCLUDE);
                if (r)
                        goto failed;

                r = config_parser_attrs_include(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "policy")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_POLICY);
                if (r)
                        goto failed;

                r = config_parser_attrs_policy(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "limit")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_LIMIT);
                if (r)
                        goto failed;

                r = config_parser_attrs_limit(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "selinux")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_SELINUX);
                if (r)
                        goto failed;

                r = config_parser_attrs_default(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "apparmor")) {

                if (state->current->type != CONFIG_NODE_BUSCONFIG)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_APPARMOR);
                if (r)
                        goto failed;

                r = config_parser_attrs_apparmor(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "allow")) {

                if (state->current->type != CONFIG_NODE_POLICY)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_ALLOW);
                if (r)
                        goto failed;

                r = config_parser_attrs_allow_deny(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "deny")) {

                if (state->current->type != CONFIG_NODE_POLICY)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_DENY);
                if (r)
                        goto failed;

                r = config_parser_attrs_allow_deny(state, node, attrs);
                if (r)
                        goto failed;

        } else if (!strcmp(name, "associate")) {

                if (state->current->type != CONFIG_NODE_SELINUX)
                        goto failed;

                r = config_node_new(&node, state->current, CONFIG_NODE_ASSOCIATE);
                if (r)
                        goto failed;

                r = config_parser_attrs_associate(state, node, attrs);
                if (r)
                        goto failed;

        } else {
                CONFIG_ERR(state, "Unknown element", ": %s", name);
                goto failed;
        }

        c_assert(node);
        c_assert(node->parent);

        c_list_link_after(&state->last->root_link, &node->root_link);
        state->current = node;
        state->last = node;
        ++state->n_depth;

        node->path = config_path_ref(state->file);
        node->file = node->path->path;
        node->lineno = XML_GetCurrentLineNumber(c_container_of(state, ConfigParser, state)->xml);

        node = NULL;
        return;

failed:
        if (r)
                state->error = error_trace(r);
        else
                ++state->n_failed;
}

static void config_parser_end_fn(void *userdata, const XML_Char *name) {
        ConfigState *state = userdata;
        int r;

        /*
         * Shortcut on errors. This is orthogonal to the begin-handler. See
         * there for details.
         */
        if (state->error)
                return;
        if (state->n_failed) {
                --state->n_failed;
                return;
        }

        /*
         * Before exiting a node, we verify that we got a valid dataset and
         * that all mandatory data was given.
         */
        switch (state->current->type) {
        case CONFIG_NODE_TYPE:
                state->current->bustype.name = strdup(state->current->cdata);
                if (!state->current->bustype.name) {
                        state->error = error_origin(-ENOMEM);
                        return;
                }

                break;

        case CONFIG_NODE_USER:
                state->current->user.valid = false;
                r = nss_cache_get_uid(state->nss,
                                      &state->current->user.uid,
                                      &state->current->user.gid,
                                      state->current->cdata);
                if (r) {
                        if (r == NSS_CACHE_E_INVALID_NAME) {
                                CONFIG_ERR(state, "Invalid user-name", ": <user>%s</user>", state->current->cdata);
                                break;
                        }

                        state->error = error_fold(r);
                        return;
                } else {
                        state->current->user.valid = true;
                }

                break;

        case CONFIG_NODE_INCLUDEDIR: {
                _c_cleanup_(c_closedirp) DIR *dir = NULL;
                static const char suffix[] = ".conf";
                struct dirent *de;
                size_t n;

                r = config_path_new_dir(&state->current->includedir.dir,
                                        state->file,
                                        state->current->cdata);
                if (r) {
                        state->error = error_trace(r);
                        return;
                }

                dir = opendir(state->current->includedir.dir->path);
                if (!dir) {
                        if (errno == ENOENT || errno == ENOTDIR)
                                break;

                        if (errno == EACCES || errno == EPERM) {
                                CONFIG_ERR(state, "Access denied", ": %s",
                                           state->current->includedir.dir->path);
                        }

                        state->error = error_origin(-errno);
                        return;
                }

                r = dirwatch_add(state->dirwatch, state->current->includedir.dir->path);
                if (r) {
                        state->error = error_fold(r);
                        return;
                }

                for (errno = 0, de = readdir(dir);
                     de;
                     errno = 0, de = readdir(dir)) {
                        _c_cleanup_(config_node_freep) ConfigNode *node = NULL;

                        n = strlen(de->d_name);

                        if (n <= strlen(suffix))
                                continue;
                        if (strcmp(de->d_name + n - strlen(suffix), suffix))
                                continue;

                        r = config_node_new(&node, state->current, CONFIG_NODE_INCLUDE);
                        if (r) {
                                state->error = error_trace(r);
                                return;
                        }

                        r = config_path_new(&node->include.file,
                                            state->current->includedir.dir,
                                            NULL,
                                            de->d_name);
                        if (r) {
                                state->error = error_trace(r);
                                return;
                        }

                        c_list_link_after(&state->last->root_link, &node->root_link);
                        c_list_link_tail(&state->root->include_list, &node->include_link);
                        state->last = node;
                        node = NULL;
                }

                break;
        }

        case CONFIG_NODE_INCLUDE: {
                r = config_path_new(&state->current->include.file,
                                    state->file,
                                    state->current->include.selinux_root_relative ?
                                        bus_selinux_policy_root() :
                                        NULL,
                                    state->current->cdata);
                if (r) {
                        state->error = error_trace(r);
                        return;
                }

                c_list_link_tail(&state->root->include_list, &state->current->include_link);
                break;
        }

        case CONFIG_NODE_SERVICEDIR: {
                state->current->servicedir.path = strdup(state->current->cdata);
                if (!state->current->servicedir.path) {
                        state->error = error_origin(-ENOMEM);
                        return;
                }

                break;
        }

        case CONFIG_NODE_LIMIT:
                r = util_strtou64(&state->current->limit.value, state->current->cdata);
                if (r)
                        CONFIG_ERR(state, "Invalid limit value", ": %s", state->current->cdata);

                break;

        case CONFIG_NODE_LISTEN:
        case CONFIG_NODE_PIDFILE:
        case CONFIG_NODE_SERVICEHELPER:
        case CONFIG_NODE_AUTH:
                /* XXX: Not yet implemented. */
                break;

        case CONFIG_NODE_BUSCONFIG:
        case CONFIG_NODE_FORK:
        case CONFIG_NODE_SYSLOG:
        case CONFIG_NODE_KEEP_UMASK:
        case CONFIG_NODE_STANDARD_SESSION_SERVICEDIRS:
        case CONFIG_NODE_STANDARD_SYSTEM_SERVICEDIRS:
        case CONFIG_NODE_POLICY:
        case CONFIG_NODE_SELINUX:
        case CONFIG_NODE_APPARMOR:
        case CONFIG_NODE_ALLOW:
        case CONFIG_NODE_DENY:
        case CONFIG_NODE_ASSOCIATE:
                /* fallthrough */
        default:
                if (state->current->cdata &&
                    strspn(state->current->cdata, " \r\t\n") < state->current->n_cdata)
                        CONFIG_ERR(state, "Unknown character value", ": <%s>#CDATA</%s>", name, name);

                break;
        }

        /*
         * Verify our state is correct and then traverse one level up the tree.
         * That is, set the parent as the new current node and decrement the
         * depth.
         */
        c_assert(state->n_depth);
        c_assert(state->current);
        c_assert(state->current->parent);

        --state->n_depth;
        state->current = state->current->parent;
}

static void config_parser_blob_fn(void *userdata, const XML_Char *data, int n_data) {
        ConfigState *state = userdata;
        char *t;

        /* Shortcut on errors. Simply bail out and skip the handler. */
        if (state->error || state->n_failed)
                return;

        /* Append @data to existing @cdata */
        t = malloc(state->current->n_cdata + n_data + 1);
        if (!t) {
                state->error = error_origin(-ENOMEM);
                return;
        }

        c_memcpy(t, state->current->cdata, state->current->n_cdata);
        c_memcpy(t + state->current->n_cdata, data, n_data);
        t[state->current->n_cdata + n_data] = 0;
        free(state->current->cdata);
        state->current->cdata = t;
        state->current->n_cdata += n_data;
}

/**
 * config_parser_init() - XXX
 */
void config_parser_init(ConfigParser *parser) {
        *parser = (ConfigParser)CONFIG_PARSER_NULL(*parser);

        parser->xml = XML_ParserCreate(NULL);
}

/**
 * config_parser_deinit() - XXX
 */
void config_parser_deinit(ConfigParser *parser) {
        if (parser->xml)
                XML_ParserFree(parser->xml);

        *parser = (ConfigParser)CONFIG_PARSER_NULL(*parser);
}

static int config_parser_include(ConfigParser *parser, ConfigRoot *root, ConfigNode *node, NSSCache *nss_cache, Dirwatch *dirwatch) {
        _c_cleanup_(c_closep) int fd = -1;
        char buffer[CONFIG_PARSER_BUFFER_MAX];
        ConfigPath *i_file;
        ssize_t len;
        int r;

        c_assert(node->type == CONFIG_NODE_INCLUDE);
        c_assert(node->include.file);

        c_memset(&parser->state, 0, sizeof(parser->state));
        parser->state.nss = nss_cache;
        parser->state.dirwatch = dirwatch;
        parser->state.file = node->include.file;
        parser->state.root = root;
        parser->state.current = node;
        parser->state.last = node;

        /* ignore selinux files if selinux is disabled */
        if (node->include.if_selinux_enabled && !bus_selinux_is_enabled())
                return 0;

        /* ignore recursive inclusions */
        for (i_file = node->include.file->parent; i_file; i_file = i_file->parent) {
                if (!strcmp(node->include.file->path, i_file->path)) {
                        CONFIG_ERR(&parser->state, "Recursive inclusion", "");
                        return 0;
                }
        }

        XML_ParserReset(parser->xml, NULL);
        XML_SetUserData(parser->xml, &parser->state);
        XML_SetElementHandler(parser->xml, config_parser_begin_fn, config_parser_end_fn);
        XML_SetCharacterDataHandler(parser->xml, config_parser_blob_fn);

        r = open(node->include.file->path, O_RDONLY | O_CLOEXEC | O_NOCTTY);
        if (r < 0) {
                if (errno == ENOENT || errno == ENOTDIR) {
                        if (node->include.ignore_missing)
                                return 0;

                        CONFIG_ERR(&parser->state, "Missing configuration file",
                                   ": %s", node->include.file->path);
                        return CONFIG_E_INVALID;
                } else if (errno == EACCES || errno == EPERM) {
                        CONFIG_ERR(&parser->state, "Access denied",
                                   ": %s", node->include.file->path);
                        return CONFIG_E_INVALID;
                }

                return error_origin(-errno);
        }
        fd = r;

        do {
                len = read(fd, buffer, sizeof(buffer));
                if (len < 0)
                        return error_origin(-errno);

                r = XML_Parse(parser->xml, buffer, len, len ? XML_FALSE : XML_TRUE);
                if (r != XML_STATUS_OK) {
                        CONFIG_ERR(&parser->state, "Invalid XML", ": %s",
                                   XML_ErrorString(XML_GetErrorCode(parser->xml)));
                        return CONFIG_E_INVALID;
                }

                if (parser->state.error)
                        return error_trace(parser->state.error);
        } while (len);

        c_assert(!parser->state.n_depth);
        c_assert(!parser->state.n_failed);

        return 0;
}

/**
 * config_parser_read() - XXX
 */
int config_parser_read(ConfigParser *parser, ConfigRoot **rootp, const char *path, NSSCache *nss_cache, Dirwatch *dirwatch) {
        _c_cleanup_(config_root_freep) ConfigRoot *root = NULL;
        _c_cleanup_(config_path_unrefp) ConfigPath *file = NULL;
        ConfigNode *node;
        int r;

        /*
         * First create a fresh root config entry which we will use to link all
         * parsed nodes to. This is what is returned in the end to the caller.
         */
        r = config_root_new(&root);
        if (r)
                return error_trace(r);

        /*
         * Create a fake <include>@path</include> node on the root entry, which
         * serves as starting point.
         */
        r = config_path_new(&file, NULL, NULL, path);
        if (r)
                return error_trace(r);

        r = config_node_new(&node, NULL, CONFIG_NODE_INCLUDE);
        if (r)
                return error_trace(r);

        node->include.file = config_path_ref(file);
        c_list_link_front(&root->node_list, &node->root_link);
        c_list_link_front(&root->include_list, &node->include_link);

        /*
         * Now for as long as we find include-nodes linked on
         * @root->include_list, we call into config_parser_include(). This will
         * fill in all the contents of the include.
         */
        while ((node = c_list_first_entry(&root->include_list, ConfigNode, include_link))) {
                c_list_unlink(&node->include_link);

                r = config_parser_include(parser, root, node, nss_cache, dirwatch);
                if (r)
                        return error_trace(r);
        }

        *rootp = root;
        root = NULL;
        return 0;
}
