/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2021-2024 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>

#include <sudoers.h>
#include <redblack.h>
#include <cvtsudoers.h>
#include <gram.h>

static struct member *
new_member(const char *name, short type)
{
    struct member *m;
    debug_decl(digest_list_equivalent, SUDOERS_DEBUG_PARSER);

    m = calloc(1, sizeof(struct member));
    if (m == NULL)
	goto oom;
    if (name != NULL) {
	m->name = strdup(name);
	if (m->name == NULL)
	    goto oom;
    }
    m->type = type;

    debug_return_ptr(m);
oom:
    sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
    free(m);
    debug_return_ptr(NULL);
}

/*
 * Compare two digest lists.
 * Returns true if they are the same, else false.
 * XXX - should not care about order
 */
static bool
digest_list_equivalent(struct command_digest_list *cdl1,
    struct command_digest_list *cdl2)
{
    struct command_digest *cd1 = TAILQ_FIRST(cdl1);
    struct command_digest *cd2 = TAILQ_FIRST(cdl2);
    debug_decl(digest_list_equivalent, SUDOERS_DEBUG_PARSER);

    while (cd1 != NULL && cd2 != NULL) {
	if (cd1->digest_type != cd2->digest_type)
	    debug_return_bool(false);
	if (strcmp(cd1->digest_str, cd2->digest_str) != 0)
	    debug_return_bool(false);
	cd1 = TAILQ_NEXT(cd1, entries);
	cd2 = TAILQ_NEXT(cd2, entries);
    }

    if (cd1 != NULL || cd2 != NULL)
	debug_return_bool(false);
    debug_return_bool(true);
}

/*
 * Compare two members.
 * Returns true if they are the same, else false.
 */
static bool
member_equivalent(struct member *m1, struct member *m2)
{
    debug_decl(member_equivalent, SUDOERS_DEBUG_PARSER);

    if (m1->type != m2->type || m1->negated != m2->negated)
	debug_return_bool(false);

    if (m1->type == COMMAND) {
	struct sudo_command *c1 = (struct sudo_command *)m1->name;
	struct sudo_command *c2 = (struct sudo_command *)m2->name;
	if (c1->cmnd != NULL && c2->cmnd != NULL) {
	    if (strcmp(c1->cmnd, c2->cmnd) != 0)
		debug_return_bool(false);
	} else if (c1->cmnd != c2->cmnd) {
	    debug_return_bool(false);
	}

	if (c1->args != NULL && c2->args != NULL) {
	    if (strcmp(c1->args, c2->args) != 0)
		debug_return_bool(false);
	} else if (c1->args != c2->args) {
	    debug_return_bool(false);
	}

	if (!digest_list_equivalent(&c1->digests, &c2->digests)) {
	    debug_return_bool(false);
	}
    } else {
	if (m1->name != NULL && m2->name != NULL) {
	    if (strcmp(m1->name, m2->name) != 0)
		debug_return_bool(false);
	} else if (m1->name != m2->name) {
	    debug_return_bool(false);
	}
    }

    debug_return_bool(true);
}

/*
 * Compare two members, m1 and m2.
 * Returns true if m2 overrides m1, else false.
 */
static bool
member_overridden(struct member *m1, struct member *m2, bool check_negated)
{
    debug_decl(member_overridden, SUDOERS_DEBUG_PARSER);

    if (check_negated && m1->negated != m2->negated)
	debug_return_bool(false);

    /* "ALL" always wins (modulo digest). */
    if (m2->type == ALL) {
	if (m2->name != NULL) {
	    struct sudo_command *c1 = (struct sudo_command *)m1->name;
	    struct sudo_command *c2 = (struct sudo_command *)m2->name;
	    debug_return_bool(digest_list_equivalent(&c1->digests, &c2->digests));
	}
	debug_return_bool(true);
    }

    if (m1->type != m2->type)
	debug_return_bool(false);

    if (m1->type == COMMAND) {
	struct sudo_command *c1 = (struct sudo_command *)m1->name;
	struct sudo_command *c2 = (struct sudo_command *)m2->name;
	if (strcmp(c1->cmnd, c2->cmnd) != 0)
	    debug_return_bool(false);

	if (c1->args != NULL && c2->args != NULL) {
	    if (strcmp(c1->args, c2->args) != 0)
		debug_return_bool(false);
	} else if (c1->args != c2->args) {
	    debug_return_bool(false);
	}

	if (!digest_list_equivalent(&c1->digests, &c2->digests)) {
	    debug_return_bool(false);
	}
    } else {
	if (strcmp(m1->name, m2->name) != 0)
	    debug_return_bool(false);
    }

    debug_return_bool(true);
}

/*
 * Given two member lists, ml1 and ml2.
 * Returns true if the every element of ml1 is overridden by ml2, else false.
 */
static bool
member_list_override(struct member_list *ml1, struct member_list *ml2,
    bool check_negated)
{
    struct member *m1, *m2;
    debug_decl(member_list_override, SUDOERS_DEBUG_PARSER);

    /* An empty member_list only overrides another empty list. */
    if (TAILQ_EMPTY(ml2)) {
	debug_return_bool(TAILQ_EMPTY(ml1));
    }

    /* Check whether each element of ml1 is also covered by ml2. */
    TAILQ_FOREACH_REVERSE(m1, ml1, member_list, entries) {
	bool overridden = false;
	TAILQ_FOREACH_REVERSE(m2, ml2, member_list, entries) {
	    if (member_overridden(m1, m2, check_negated)) {
		overridden = true;
		break;
	    }
	}
	if (!overridden)
	    debug_return_bool(false);
    }

    debug_return_bool(true);
}

/*
 * Compare two member lists.
 * Returns true if they are the same, else false.
 * XXX - should not care about order if things are not negated.
 */
static bool
member_list_equivalent(struct member_list *ml1, struct member_list *ml2)
{
    struct member *m1 = TAILQ_FIRST(ml1);
    struct member *m2 = TAILQ_FIRST(ml2);
    debug_decl(member_list_equivalent, SUDOERS_DEBUG_PARSER);

    while (m1 != NULL && m2 != NULL) {
	if (!member_equivalent(m1, m2))
	    debug_return_bool(false);
	m1 = TAILQ_NEXT(m1, entries);
	m2 = TAILQ_NEXT(m2, entries);
    }

    if (m1 != NULL || m2 != NULL)
	debug_return_bool(false);
    debug_return_bool(true);
}

/*
 * Attempt to simplify a host list.
 * If a host list contains all hosts in bound_hosts, replace them with
 * "ALL".  Also prune hosts on either side of "ALL" when possible.
 */
static bool
simplify_host_list(struct member_list *hosts, const char *file, int line,
    int column, struct member_list *bound_hosts)
{
    struct member *m, *n, *next;
    bool logged = false;
    debug_decl(simplify_host_list, SUDOERS_DEBUG_PARSER);

    /*
     * If all sudoers sources have an associated host, replace a
     * list of those hosts with "ALL".
     */
    if (!TAILQ_EMPTY(bound_hosts)) {
	TAILQ_FOREACH_REVERSE(n, bound_hosts, member_list, entries) {
	    TAILQ_FOREACH_REVERSE(m, hosts, member_list, entries) {
		if (m->negated) {
		    /* Don't try to handled negated entries. */
		    m = NULL;
		    break;
		}
		if (m->type == n->type && strcmp(m->name, n->name) == 0) {
		    /* match */
		    break;
		}
	    }
	    if (m == NULL) {
		/* no match */
		break;
	    }
	}
	if (n == NULL) {
	    /* found all hosts */
	    log_warnx(U_("%s:%d:%d: converting host list to ALL"),
		file, line, column);
	    logged = true;

	    TAILQ_FOREACH_REVERSE(n, bound_hosts, member_list, entries) {
		TAILQ_FOREACH_REVERSE_SAFE(m, hosts, member_list, entries, next) {
		    if (m->negated) {
			/* Don't try to handled negated entries. */
			m = NULL;
			break;
		    }
		    if (m->type == n->type && strcmp(m->name, n->name) == 0) {
			/* remove matching host */
			TAILQ_REMOVE(hosts, m, entries);
			free_member(m);
			break;
		    }
		}
	    }
	    m = new_member(NULL, ALL);
	    if (m == NULL)
		debug_return_bool(false);
	    TAILQ_INSERT_TAIL(hosts, m, entries);
	}
    }

    /*
     * A host list that contains ALL with no negated entries past it
     * is equivalent to a list containing just "ALL".
     */
    TAILQ_FOREACH_REVERSE(m, hosts, member_list, entries) {
	if (m->negated) {
	    /* Don't try to handled negated entries. */
	    break;
	}
	if (m->type == ALL) {
	    /* Replace member list with a single ALL entry. */
	    if (!logged) {
		log_warnx(U_("%s:%d:%d: converting host list to ALL"),
		    file, line, column);
	    }
	    TAILQ_REMOVE(hosts, m, entries);
	    free_members(hosts);
	    TAILQ_INSERT_TAIL(hosts, m, entries);
	    break;
	}
    }

    debug_return_bool(true);
}

/*
 * Generate a unique name from old_name that is not used in parse_tree,
 * subsequent parse_trees or merged_tree.
 */
static char *
alias_make_unique(const char *old_name, short type,
    struct sudoers_parse_tree *parse_tree0,
    struct sudoers_parse_tree *merged_tree)
{
    struct sudoers_parse_tree *parse_tree;
    char *cp, *new_name = NULL;
    struct alias *a;
    long long suffix;
    size_t namelen;
    debug_decl(alias_make_unique, SUDOERS_DEBUG_ALIAS);

    /* If old_name already has a suffix, increment it, else start with "_1". */
    suffix = 0;
    namelen = strlen(old_name);
    cp = strrchr(old_name, '_');
    if (cp != NULL && isdigit((unsigned char)cp[1])) {
	suffix = sudo_strtonum(cp + 1, 0, LLONG_MAX, NULL);
	if (suffix != 0) {
	    namelen = (size_t)(cp - old_name);
	}
    }

    for (;;) {
	suffix++;
	free(new_name);
	if (asprintf(&new_name, "%.*s_%lld", (int)namelen, old_name, suffix) == -1) {
	    sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	    debug_return_ptr(NULL);
	}
	/* Make sure new_name is not already in use. */
	a = alias_get(merged_tree, new_name, type);
	if (a != NULL) {
	    alias_put(a);
	    continue;
	}
	parse_tree = parse_tree0;
	while ((parse_tree = TAILQ_NEXT(parse_tree, entries)) != NULL) {
	    a = alias_get(parse_tree, new_name, type);
	    if (a != NULL) {
		alias_put(a);
		break;
	    }
	}
	if (a == NULL) {
	    /* Must be unique. */
	    break;
	}
    }

    debug_return_ptr(new_name);
}

struct alias_rename_closure {
    const char *old_name;
    const char *new_name;
    int type;
};

static int
alias_rename_members(struct sudoers_parse_tree *parse_tree, struct alias *a,
    void *v)
{
    struct alias_rename_closure *closure = v;
    struct member *m;
    debug_decl(alias_rename_members, SUDOERS_DEBUG_ALIAS);

    if (a->type != closure->type)
	debug_return_int(0);

    /* Replace old_name in member list, if present. */
    TAILQ_FOREACH(m, &a->members, entries) {
	if (m->type == ALIAS && strcmp(m->name, closure->old_name) == 0) {
	    char *copy = strdup(closure->new_name);
	    if (copy == NULL) {
		sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
		debug_return_int(-1);
	    }
	    free(m->name);
	    m->name = copy;
	}
    }

    debug_return_int(0);
}

static bool
alias_rename_defaults(const char *old_name, const char *new_name,
    short alias_type, struct defaults_list *defaults)
{
    struct defaults *def, *def_next;
    struct member *m;
    debug_decl(alias_rename_defaults, SUDOERS_DEBUG_ALIAS);

    TAILQ_FOREACH_SAFE(def, defaults, entries, def_next) {
	/* Consecutive Defaults can share the same binding. */
	if (def_next != NULL && def->binding == def_next->binding)
	    continue;

	switch (def->type) {
	case DEFAULTS_USER:
	    if (alias_type != USERALIAS)
		continue;
	    break;
	case DEFAULTS_RUNAS:
	    if (alias_type != RUNASALIAS)
		continue;
	    break;
	case DEFAULTS_HOST:
	    if (alias_type != HOSTALIAS)
		continue;
	    break;
	default:
	    continue;
	}

	/* Rename matching aliases in the binding's member_list. */
	TAILQ_FOREACH(m, &def->binding->members, entries) {
	    if (m->type != ALIAS)
		continue;
	    if (strcmp(m->name, old_name) == 0) {
		char *copy = strdup(new_name);
		if (copy == NULL) {
		    sudo_warnx(U_("%s: %s"), __func__,
			U_("unable to allocate memory"));
		    debug_return_bool(false);
		}
		free(m->name);
		m->name = copy;
	    }
	}
    }

    debug_return_bool(true);
}

static bool
alias_rename_member(const char *old_name, const char *new_name,
    struct member *m)
{
    debug_decl(alias_rename_member, SUDOERS_DEBUG_ALIAS);

    if (m->type == ALIAS && strcmp(m->name, old_name) == 0) {
	char *copy = strdup(new_name);
	if (copy == NULL) {
	    sudo_warnx(U_("%s: %s"), __func__,
		U_("unable to allocate memory"));
	    debug_return_bool(false);
	}
	free(m->name);
	m->name = copy;
    }

    debug_return_bool(true);
}

static bool
alias_rename_member_list(const char *old_name, const char *new_name,
    struct member_list *members)
{
    struct member *m;
    debug_decl(alias_rename_member_list, SUDOERS_DEBUG_ALIAS);

    TAILQ_FOREACH(m, members, entries) {
	if (!alias_rename_member(old_name, new_name, m))
	    debug_return_bool(false);
    }

    debug_return_bool(true);
}

static bool
alias_rename_userspecs(const char *old_name, const char *new_name,
    short alias_type, struct userspec_list *userspecs)
{
    struct privilege *priv;
    struct cmndspec *cs;
    struct userspec *us;
    debug_decl(alias_rename_userspecs, SUDOERS_DEBUG_ALIAS);

    TAILQ_FOREACH(us, userspecs, entries) {
	if (alias_type == USERALIAS) {
	    if (!alias_rename_member_list(old_name, new_name, &us->users)) {
		debug_return_bool(false);
	    }
	}
	TAILQ_FOREACH(priv, &us->privileges, entries) {
	    if (!alias_rename_defaults(old_name, new_name, alias_type, &priv->defaults)) {
		debug_return_bool(false);
	    }
	    if (alias_type == HOSTALIAS) {
		if (!alias_rename_member_list(old_name, new_name, &priv->hostlist)) {
		    debug_return_bool(false);
		}
		continue;
	    }
	    TAILQ_FOREACH(cs, &priv->cmndlist, entries) {
		if (alias_type == CMNDALIAS) {
		    if (!alias_rename_member(old_name, new_name, cs->cmnd)) {
			debug_return_bool(false);
		    }
		    continue;
		}
		if (alias_type == RUNASALIAS) {
		    if (cs->runasuserlist != NULL) {
			if (!alias_rename_member_list(old_name, new_name, cs->runasuserlist)) {
			    debug_return_bool(false);
			}
		    }
		    if (cs->runasgrouplist != NULL) {
			if (!alias_rename_member_list(old_name, new_name, cs->runasgrouplist)) {
			    debug_return_bool(false);
			}
		    }
		}
	    }
	}
    }

    debug_return_bool(true);
}

/*
 * Rename an alias in parse_tree and all the places where it is used.
 * Takes ownership if new_name which must not be freed by the caller.
 */
static bool
alias_rename(const char *old_name, char *new_name, short alias_type,
    struct sudoers_parse_tree *parse_tree)
{
    struct alias_rename_closure closure = { old_name, new_name, alias_type };
    struct alias *a;
    debug_decl(alias_rename, SUDOERS_DEBUG_ALIAS);

    /* Remove under old name and add via new to maintain tree properties. */
    a = alias_remove(parse_tree, old_name, alias_type);
    if (a == NULL) {
	/* Should not happen. */
	sudo_warnx(U_("unable to find alias %s"), old_name);
	free(new_name);
	debug_return_bool(false);
    }
    log_warnx(U_("%s:%d:%d: renaming alias %s to %s"),
	a->file, a->line, a->column, a->name, new_name);
    free(a->name);
    a->name = new_name;
    switch (rbinsert(parse_tree->aliases, a, NULL)) {
    case 0:
	/* success */
	break;
    case 1:
	/* Already present, should not happen. */
	errno = EEXIST;
	sudo_warn(U_("%s: %s"), __func__, a->name);
	alias_free(a);
	debug_return_bool(false);
	break;
    default:
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	alias_free(a);
	debug_return_bool(false);
    }

    /* Rename it in the aliases tree itself (aliases can be nested). */
    if (!alias_apply(parse_tree, alias_rename_members, &closure))
	debug_return_bool(false);

    /* Rename it in the Defaults list. */
    if (!alias_rename_defaults(old_name, new_name, alias_type, &parse_tree->defaults))
	debug_return_bool(false);

    /* Rename it in the userspecs list. */
    if (!alias_rename_userspecs(old_name, new_name, alias_type, &parse_tree->userspecs))
	debug_return_bool(false);

    debug_return_bool(true);
}

static int
alias_resolve_conflicts(struct sudoers_parse_tree *parse_tree0, struct alias *a,
    void *v)
{
    struct sudoers_parse_tree *parse_tree = parse_tree0;
    struct sudoers_parse_tree *merged_tree = v;
    char *new_name;
    int ret;
    debug_decl(alias_resolve_conflicts, SUDOERS_DEBUG_ALIAS);

    /*
     * Check for conflicting alias names in the subsequent sudoers files.
     * Duplicates are removed and conflicting aliases are renamed.
     * We cannot modify the alias tree that we are traversing.
     */
    while ((parse_tree = TAILQ_NEXT(parse_tree, entries)) != NULL) {
	struct alias *b = alias_get(parse_tree, a->name, a->type);
	if (b == NULL)
	    continue;

	/* If alias 'b' is equivalent, remove it. */
	alias_put(b);
	if (member_list_equivalent(&a->members, &b->members)) {
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"removing duplicate alias %s from %p", a->name, parse_tree);
	    b = alias_remove(parse_tree, a->name, a->type);
	    log_warnx(U_("%s:%d:%d: removing duplicate alias %s"),
		b->file, b->line, b->column, b->name);
	    alias_free(b);
	    continue;
	}

	/* Rename alias 'b' to avoid a naming conflict. */
	new_name = alias_make_unique(a->name, a->type, parse_tree, merged_tree);
	if (new_name == NULL)
	    debug_return_int(-1);
	if (!alias_rename(a->name, new_name, a->type, parse_tree))
	    debug_return_int(-1);
    }

    /*
     * The alias will exist in both the original and merged trees.
     * This is not a problem as the caller will delete the old trees
     * (without freeing the data).
     */
    ret = rbinsert(merged_tree->aliases, a, NULL);
    switch (ret) {
    case 0:
	/* success */
	break;
    case 1:
	/* already present, should not happen. */
	errno = EEXIST;
	sudo_warn(U_("%s: %s"), __func__, a->name);
	debug_return_int(-1);
    default:
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	debug_return_int(-1);
    }

    debug_return_int(0);
}

static bool
merge_aliases(struct sudoers_parse_tree_list *parse_trees,
    struct sudoers_parse_tree *merged_tree)
{
    struct sudoers_parse_tree *parse_tree;
    debug_decl(merge_aliases, SUDOERS_DEBUG_ALIAS);

    /*
     * For each parse_tree, check for collisions with alias names
     * in subsequent parse trees.  On collision, add a numbered
     * suffix (e.g. ALIAS_1) to make the name unique and rename
     * any uses of that alias in the affected parse_tree.
     */
    TAILQ_FOREACH(parse_tree, parse_trees, entries) {
	if (parse_tree->aliases == NULL)
	    continue;

	/*
	 * Resolve any conflicts in alias names, renaming aliases as
	 * needed and eliminating duplicates.
	 */
	if (!alias_apply(parse_tree, alias_resolve_conflicts, merged_tree))
	    debug_return_bool(false);

	/*
	 * Destroy the old alias tree without freeing the alias data
	 * which has been copied to merged_tree.
	 */
	rbdestroy(parse_tree->aliases, NULL);
	parse_tree->aliases = NULL;
    }

    debug_return_bool(true);
}

/*
 * Compare two defaults structs but not their actual value.
 * Returns true if they refer to the same Defaults variable and binding.
 * Also sets mergeable if they only differ in the binding.
 */
static bool
defaults_var_matches(struct defaults *d1, struct defaults *d2,
    bool *mergeable)
{
    debug_decl(defaults_var_matches, SUDOERS_DEBUG_DEFAULTS);

    if (strcmp(d1->var, d2->var) != 0)
	debug_return_bool(false);
    if (d1->type != d2->type) {
	if ((d1->type == DEFAULTS && d2->type == DEFAULTS_HOST) ||
	    (d1->type == DEFAULTS_HOST && d2->type == DEFAULTS)) {
	    /* We can merge host and global bindings. */
	    if (mergeable != NULL)
		*mergeable = true;
	}
	debug_return_bool(false);
    }
    if (d1->type != DEFAULTS) {
	if (!member_list_equivalent(&d1->binding->members, &d2->binding->members)) {
	    if (mergeable != NULL)
		*mergeable = true;
	    debug_return_bool(false);
	}
    }

    debug_return_bool(true);
}

/*
 * Compare the values of two defaults structs, which must be of the same type.
 * Returns true if the value and operator match, else false.
 */
static bool
defaults_val_matches(struct defaults *d1, struct defaults *d2)
{
    debug_decl(defaults_val_matches, SUDOERS_DEBUG_DEFAULTS);

    /* XXX - what about list operators? */
    if (d1->op != d2->op)
	debug_return_bool(false);

    /* Either both must be NULL or both non-NULL _and_ matching. */
    if (d1->val != NULL && d2->val != NULL) {
	if (strcmp(d1->val, d2->val) != 0)
	    debug_return_bool(false);
    } else {
	if (d1->val != NULL || d2->val != NULL)
	    debug_return_bool(false);
    }

    debug_return_bool(true);
}

/*
 * Returns true if d1 is equivalent to d2, else false.
 */
static bool
defaults_equivalent(struct defaults *d1, struct defaults *d2)
{
    debug_decl(defaults_equivalent, SUDOERS_DEBUG_DEFAULTS);

    if (!defaults_var_matches(d1, d2, NULL))
	debug_return_bool(false);
    debug_return_bool(defaults_val_matches(d1, d2));
}

/*
 * Returns true if dl1 is equivalent to dl2, else false.
 */
static bool
defaults_list_equivalent(struct defaults_list *dl1, struct defaults_list *dl2)
{
    struct defaults *d1 = TAILQ_FIRST(dl1);
    struct defaults *d2 = TAILQ_FIRST(dl2);
    debug_decl(defaults_list_equivalent, SUDOERS_DEBUG_DEFAULTS);

    while (d1 != NULL && d2 != NULL) {
	if (!defaults_equivalent(d1, d2))
	    debug_return_bool(false);
	d1 = TAILQ_NEXT(d1, entries);
	d2 = TAILQ_NEXT(d2, entries);
    }

    if (d1 != NULL || d2 != NULL)
	debug_return_bool(false);
    debug_return_bool(true);
}

enum cvtsudoers_conflict {
    CONFLICT_NONE,
    CONFLICT_RESOLVED,
    CONFLICT_UNRESOLVED,
    CONFLICT_ERROR
};

/*
 * Check for duplicate and conflicting Defaults entries in later sudoers files.
 * Returns true if we find a conflict or duplicate, else false.
 */
static enum cvtsudoers_conflict
defaults_check_conflict(struct defaults *def,
    struct sudoers_parse_tree *parse_tree0)
{
    struct sudoers_parse_tree *parse_tree = parse_tree0;
    struct defaults *d;
    debug_decl(defaults_check_conflict, SUDOERS_DEBUG_DEFAULTS);

    while ((parse_tree = TAILQ_NEXT(parse_tree, entries)) != NULL) {
	TAILQ_FOREACH_REVERSE(d, &parse_tree->defaults, defaults_list, entries) {
	    bool mergeable = false;

	    /*
	     * We currently only merge host-based Defaults but could do
	     * others as well.  Lists in Defaults entries can be harder
	     * to read, especially command lists.
	     */
	    if (!defaults_var_matches(def, d, &mergeable)) {
		if (!mergeable || (def->type != DEFAULTS && def->type != DEFAULTS_HOST))
		    continue;
	    }
	    if (defaults_val_matches(def, d)) {
		/* Duplicate Defaults entry (may need to merge binding). */
		if (mergeable) {
		    if (d->type != def->type &&
			    (d->type == DEFAULTS || def->type == DEFAULTS)) {
			/*
			 * To be able to merge two Defaults, they both must
			 * have the same binding type.  Convert a global
			 * Defaults to one bound to single "ALL" member.
			 */
			if (d->type == DEFAULTS) {
			    struct member *m = new_member(NULL, ALL);
			    if (m == NULL)
				debug_return_int(CONFLICT_ERROR);
			    TAILQ_INSERT_TAIL(&d->binding->members, m, entries);
			    d->type = def->type;
			}
			if (def->type == DEFAULTS) {
			    struct member *m = new_member(NULL, ALL);
			    if (m == NULL)
				debug_return_int(CONFLICT_ERROR);
			    TAILQ_INSERT_TAIL(&def->binding->members, m, entries);
			    def->type = d->type;
			}
		    }

		    /* Prepend def binding to d (hence double concat). */
		    TAILQ_CONCAT(&def->binding->members, &d->binding->members, entries);
		    TAILQ_CONCAT(&d->binding->members, &def->binding->members, entries);
		}
		debug_return_int(CONFLICT_RESOLVED);
	    }
	    /*
	     * If the value doesn't match but the Defaults name did we don't
	     * consider that a conflict.
	     */
	    if (!mergeable) {
		log_warnx(U_("%s:%d:%d: conflicting Defaults entry \"%s\" host-specific in %s:%d:%d"),
		    def->file, def->line, def->column, def->var,
		    d->file, d->line, d->column);
		debug_return_int(CONFLICT_UNRESOLVED);
	    }
	}
    }

    debug_return_int(CONFLICT_NONE);
}

/*
 * Merge Defaults entries in parse_trees and store the result in
 * merged_tree.  If a hostname was specified with the sudoers source,
 * create a host-specific Defaults entry where possible.
 * Returns true on success, else false.
 */
static bool
merge_defaults(struct sudoers_parse_tree_list *parse_trees,
    struct sudoers_parse_tree *merged_tree, struct member_list *bound_hosts)
{
    struct sudoers_parse_tree *parse_tree;
    struct defaults *def;
    struct member *m;
    debug_decl(merge_defaults, SUDOERS_DEBUG_DEFAULTS);

    TAILQ_FOREACH(parse_tree, parse_trees, entries) {
	/*
	 * If parse_tree has a host name associated with it,
	 * try to make the Defaults setting host-specific.
	 */
	TAILQ_FOREACH(def, &parse_tree->defaults, entries) {
	    if (parse_tree->lhost != NULL && def->type == DEFAULTS) {
		m = new_member(parse_tree->lhost, WORD);
		if (m == NULL)
		    debug_return_bool(false);
		log_warnx(U_("%s:%d:%d: made Defaults \"%s\" specific to host %s"),
		    def->file, def->line, def->column, def->var,
		    parse_tree->lhost);
		TAILQ_INSERT_TAIL(&def->binding->members, m, entries);
		def->type = DEFAULTS_HOST;
	    }
	}
    }

    TAILQ_FOREACH(parse_tree, parse_trees, entries) {
	while ((def = TAILQ_FIRST(&parse_tree->defaults)) != NULL) {
	    /*
	     * Only add Defaults entry if not overridden by subsequent sudoers.
	     */
	    TAILQ_REMOVE(&parse_tree->defaults, def, entries);
	    switch (defaults_check_conflict(def, parse_tree)) {
	    case CONFLICT_NONE:
		if (def->type != DEFAULTS_HOST) {
		    log_warnx(U_("%s:%d:%d: unable to make Defaults \"%s\" host-specific"),
			def->file, def->line, def->column, def->var);
		}
		TAILQ_INSERT_TAIL(&merged_tree->defaults, def, entries);
		break;
	    case CONFLICT_RESOLVED:
		/* Duplicate or merged into a subsequent Defaults setting. */
		free_default(def);
		break;
	    case CONFLICT_UNRESOLVED:
		log_warnx(U_("%s:%d:%d: removing Defaults \"%s\" overridden by subsequent entries"),
		    def->file, def->line, def->column, def->var);
		free_default(def);
		break;
	    default:
		/* warning printed by defaults_check_conflict() */
		free_default(def);
		debug_return_bool(false);
	    }
	}
    }

    /*
     * Simplify host lists in the merged Defaults.
     */
    TAILQ_FOREACH(def, &merged_tree->defaults, entries) {
	/* TODO: handle refcnt != 1 */
	if (def->type == DEFAULTS_HOST && def->binding->refcnt == 1) {
	    if (!simplify_host_list(&def->binding->members, def->file,
		    def->line, def->column, bound_hosts)) {
		debug_return_bool(false);
	    }
	    m = TAILQ_FIRST(&def->binding->members);
	    if (m->type == ALL && !m->negated) {
		if (TAILQ_NEXT(m, entries) == NULL) {
		    /* Convert Defaults@ALL -> Defaults */
		    def->type = DEFAULTS;
		    free_members(&def->binding->members);
		    TAILQ_INIT(&def->binding->members);
		}
	    }
	}
    }

    debug_return_bool(true);
}

/*
 * Returns true if cs1 is equivalent to cs2, else false.
 */
static bool
cmndspec_equivalent(struct cmndspec *cs1, struct cmndspec *cs2, bool check_negated)
{
    debug_decl(cmndspec_equivalent, SUDOERS_DEBUG_PARSER);

    if (cs1->runasuserlist != NULL && cs2->runasuserlist != NULL) {
	if (!member_list_override(cs1->runasuserlist, cs2->runasuserlist, check_negated))
	    debug_return_bool(false);
    } else if (cs1->runasuserlist != cs2->runasuserlist) {
	debug_return_bool(false);
    }
    if (cs1->runasgrouplist != NULL && cs2->runasgrouplist != NULL) {
	if (!member_list_override(cs1->runasgrouplist, cs2->runasgrouplist, check_negated))
	    debug_return_bool(false);
    } else if (cs1->runasgrouplist != cs2->runasgrouplist) {
	debug_return_bool(false);
    }
    if (!member_equivalent(cs1->cmnd, cs2->cmnd))
	debug_return_bool(false);
    if (TAGS_CHANGED(cs1->tags, cs2->tags))
	debug_return_bool(false);
    if (cs1->timeout != cs2->timeout)
	debug_return_bool(false);
    if (cs1->notbefore != cs2->notbefore)
	debug_return_bool(false);
    if (cs1->notafter != cs2->notafter)
	debug_return_bool(false);
    if (cs1->runcwd != NULL && cs2->runcwd != NULL) {
	if (strcmp(cs1->runcwd, cs2->runcwd) != 0)
	    debug_return_bool(false);
    } else if (cs1->runcwd != cs2->runcwd) {
	debug_return_bool(false);
    }
    if (cs1->runchroot != NULL && cs2->runchroot != NULL) {
	if (strcmp(cs1->runchroot, cs2->runchroot) != 0)
	    debug_return_bool(false);
    } else if (cs1->runchroot != cs2->runchroot) {
	debug_return_bool(false);
    }
    if (cs1->role != NULL && cs2->role != NULL) {
	if (strcmp(cs1->role, cs2->role) != 0)
	    debug_return_bool(false);
    } else if (cs1->role != cs2->role) {
	debug_return_bool(false);
    }
    if (cs1->type != NULL && cs2->type != NULL) {
	if (strcmp(cs1->type, cs2->type) != 0)
	    debug_return_bool(false);
    } else if (cs1->type != cs2->type) {
	debug_return_bool(false);
    }
    if (cs1->apparmor_profile != NULL && cs2->apparmor_profile != NULL) {
	if (strcmp(cs1->apparmor_profile, cs2->apparmor_profile) != 0)
	    debug_return_bool(false);
    } else if (cs1->apparmor_profile != cs2->apparmor_profile) {
	debug_return_bool(false);
    }
    if (cs1->privs != NULL && cs2->privs != NULL) {
	if (strcmp(cs1->privs, cs2->privs) != 0)
	    debug_return_bool(false);
    } else if (cs1->privs != cs2->privs) {
	debug_return_bool(false);
    }
    if (cs1->limitprivs != NULL && cs2->limitprivs != NULL) {
	if (strcmp(cs1->limitprivs, cs2->limitprivs) != 0)
	    debug_return_bool(false);
    } else if (cs1->limitprivs != cs2->limitprivs) {
	debug_return_bool(false);
    }

    debug_return_bool(true);
}

/*
 * Returns true if csl1 is equivalent to csl2, else false.
 */
static bool
cmndspec_list_equivalent(struct cmndspec_list *csl1, struct cmndspec_list *csl2,
    bool check_negated)
{
    struct cmndspec *cs1 = TAILQ_FIRST(csl1);
    struct cmndspec *cs2 = TAILQ_FIRST(csl2);
    debug_decl(cmndspec_list_equivalent, SUDOERS_DEBUG_PARSER);

    while (cs1 != NULL && cs2 != NULL) {
	if (!cmndspec_equivalent(cs1, cs2, check_negated))
	    debug_return_bool(false);
	cs1 = TAILQ_NEXT(cs1, entries);
	cs2 = TAILQ_NEXT(cs2, entries);
    }

    if (cs1 != NULL || cs2 != NULL)
	debug_return_bool(false);
    debug_return_bool(true);
}

/*
 * Check whether userspec us1 is overridden by another sudoers file entry.
 * If us1 and another userspec differ only in their host lists, merges
 * the hosts from us1 into that userspec.
 * Returns true if overridden, else false.
 * TODO: merge privs
 */
static enum cvtsudoers_conflict
userspec_overridden(struct userspec *us1,
    struct sudoers_parse_tree *parse_tree, bool check_negated)
{
    struct userspec *us2;
    bool hosts_differ = false;
    debug_decl(userspec_overridden, SUDOERS_DEBUG_PARSER);

    if (TAILQ_EMPTY(&parse_tree->userspecs))
	debug_return_int(CONFLICT_NONE);

    /* Sudoers rules are applied in reverse order (last match wins). */
    TAILQ_FOREACH_REVERSE(us2, &parse_tree->userspecs, userspec_list, entries) {
	struct privilege *priv1, *priv2;

	if (!member_list_override(&us1->users, &us2->users, check_negated))
	    continue;

	/* XXX - order should not matter */
	priv1 = TAILQ_LAST(&us1->privileges, privilege_list);
	priv2 = TAILQ_LAST(&us2->privileges, privilege_list);
	while (priv1 != NULL && priv2 != NULL) {
	    if (!defaults_list_equivalent(&priv1->defaults, &priv2->defaults))
		break;
	    if (!cmndspec_list_equivalent(&priv1->cmndlist, &priv2->cmndlist, check_negated))
		break;

	    if (!member_list_override(&priv1->hostlist, &priv2->hostlist, check_negated))
		hosts_differ = true;

	    priv1 = TAILQ_PREV(priv1, privilege_list, entries);
	    priv2 = TAILQ_PREV(priv2, privilege_list, entries);
	}
	if (priv1 != NULL || priv2 != NULL) {
	    /* mismatch */
	    continue;
	}

	/*
	 * If we have a match of everything except the host list,
	 * merge the differing host lists.
	 */
	if (hosts_differ) {
	    priv1 = TAILQ_LAST(&us1->privileges, privilege_list);
	    priv2 = TAILQ_LAST(&us2->privileges, privilege_list);
	    while (priv1 != NULL && priv2 != NULL) {
		if (!member_list_override(&priv1->hostlist, &priv2->hostlist, check_negated)) {
		    /*
		     * Priv matches but hosts differ, prepend priv1 hostlist
		     * to into priv2 hostlist (hence the double concat).
		     */
		    TAILQ_CONCAT(&priv1->hostlist, &priv2->hostlist, entries);
		    TAILQ_CONCAT(&priv2->hostlist, &priv1->hostlist, entries);
		    log_warnx(U_("%s:%d:%d: merging userspec into %s:%d:%d"),
			us1->file, us1->line, us1->column,
			us2->file, us2->line, us2->column);
		}
		priv1 = TAILQ_PREV(priv1, privilege_list, entries);
		priv2 = TAILQ_PREV(priv2, privilege_list, entries);
	    }
	    debug_return_int(CONFLICT_RESOLVED);
	}
	debug_return_int(CONFLICT_UNRESOLVED);
    }

    debug_return_int(CONFLICT_NONE);
}

/*
 * Check whether userspec us1 is overridden by another sudoers file entry.
 * If us1 and another userspec differ only in their host lists, merges
 * the hosts from us1 into that userspec.
 * Returns true if overridden, else false.
 */
static enum cvtsudoers_conflict
userspec_check_conflict(struct userspec *us1,
    struct sudoers_parse_tree *parse_tree0)
{
    struct sudoers_parse_tree *parse_tree = parse_tree0;
    debug_decl(userspec_check_conflict, SUDOERS_DEBUG_PARSER);

    while ((parse_tree = TAILQ_NEXT(parse_tree, entries)) != NULL) {
	enum cvtsudoers_conflict ret =
	    userspec_overridden(us1, parse_tree, false);
	if (ret != CONFLICT_NONE)
	    debug_return_int(ret);
    }

    debug_return_int(CONFLICT_NONE);
}

/*
 * Merge userspecs in parse_trees and store the result in merged_tree.
 * If a hostname was specified with the sudoers source, make the
 * privilege host-specific where possible.
 * Returns true on success, else false.
 */
static bool
merge_userspecs(struct sudoers_parse_tree_list *parse_trees,
    struct sudoers_parse_tree *merged_tree, struct member_list *bound_hosts)
{
    struct sudoers_parse_tree *parse_tree;
    struct userspec *us;
    struct privilege *priv;
    struct member *m;
    debug_decl(merge_userspecs, SUDOERS_DEBUG_DEFAULTS);

    /*
     * If parse_tree has a host name associated with it,
     * try to make the privilege host-specific.
     */
    TAILQ_FOREACH(parse_tree, parse_trees, entries) {
	if (parse_tree->lhost == NULL)
	    continue;
	TAILQ_FOREACH(us, &parse_tree->userspecs, entries) {
	    TAILQ_FOREACH(priv, &us->privileges, entries) {
		TAILQ_FOREACH(m, &priv->hostlist, entries) {
		    /* We don't alter !ALL in a hostlist (XXX - should we?). */
		    if (m->type == ALL && !m->negated) {
			char *copy = strdup(parse_tree->lhost);
			if (copy == NULL) {
			    sudo_warnx(U_("%s: %s"), __func__,
				U_("unable to allocate memory"));
			    debug_return_bool(false);
			}
			m->type = WORD;
			m->name = copy;
		    }
		}
	    }
	}
    }

    /*
     * Prune out duplicate userspecs after substituting hostname(s).
     * Traverse the list in reverse order--in sudoers last match wins.
     * XXX - do this at the privilege/cmndspec level instead.
     */
    TAILQ_FOREACH(parse_tree, parse_trees, entries) {
	while ((us = TAILQ_LAST(&parse_tree->userspecs, userspec_list)) != NULL) {
	    TAILQ_REMOVE(&parse_tree->userspecs, us, entries);
	    switch (userspec_check_conflict(us, parse_tree)) {
	    case CONFLICT_NONE:
		TAILQ_INSERT_HEAD(&merged_tree->userspecs, us, entries);
		break;
	    case CONFLICT_RESOLVED:
		free_userspec(us);
		break;
	    case CONFLICT_UNRESOLVED:
		log_warnx(U_("%s:%d:%d: removing userspec overridden by subsequent entries"),
		    us->file, us->line, us->column);
		free_userspec(us);
		break;
	    default:
		/* warning printed by defaults_check_conflict() */
		free_userspec(us);
		debug_return_bool(false);
	    }
	}
    }

    /*
     * Simplify member lists in the merged tree.
     * Convert host lists with all hosts listed to "ALL" and
     * collapse other entries around "ALL".
     */
    TAILQ_FOREACH_REVERSE(us, &merged_tree->userspecs, userspec_list, entries) {
	TAILQ_FOREACH_REVERSE(priv, &us->privileges, privilege_list, entries) {
	    /* TODO: simplify other lists? */
	    if (!simplify_host_list(&priv->hostlist, us->file, us->line,
		    us->column, bound_hosts)) {
		debug_return_bool(false);
	    }
	}
    }

    debug_return_bool(true);
}

struct sudoers_parse_tree *
merge_sudoers(struct sudoers_parse_tree_list *parse_trees,
    struct sudoers_parse_tree *merged_tree)
{
    struct member_list bound_hosts = TAILQ_HEAD_INITIALIZER(bound_hosts);
    struct sudoers_parse_tree *parse_tree;
    debug_decl(merge_sudoers, SUDOERS_DEBUG_UTIL);

    /*
     * If all sudoers sources have a host associated with them, we
     * can replace a list of those hosts with "ALL" in Defaults
     * and userspecs.
     */
    TAILQ_FOREACH(parse_tree, parse_trees, entries) {
	if (parse_tree->lhost == NULL)
	    break;
    }
    if (parse_tree == NULL) {
	TAILQ_FOREACH(parse_tree, parse_trees, entries) {
	    struct member *m = new_member(parse_tree->lhost, WORD);
	    if (m == NULL)
		goto bad;
	    TAILQ_INSERT_TAIL(&bound_hosts, m, entries);
	}
    }

    if ((merged_tree->aliases = alloc_aliases()) == NULL) {
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	goto bad;
    }

    if (!merge_aliases(parse_trees, merged_tree))
	goto bad;

    if (!merge_defaults(parse_trees, merged_tree, &bound_hosts))
	goto bad;

    if (!merge_userspecs(parse_trees, merged_tree, &bound_hosts))
	goto bad;

    free_members(&bound_hosts);
    debug_return_ptr(merged_tree);
bad:
    free_members(&bound_hosts);
    debug_return_ptr(NULL);
}
