root/usr.sbin/ldapd/schema.c
/*      $OpenBSD: schema.c,v 1.20 2022/10/12 11:57:40 jsg Exp $ */

/*
 * Copyright (c) 2010 Martin Hedenfalk <martinh@openbsd.org>
 *
 * 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 <sys/types.h>

#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>

#include "ldapd.h"
#include "log.h"

#define ERROR   -1
#define STRING   1

static int
attr_oid_cmp(struct attr_type *a, struct attr_type *b)
{
        return strcasecmp(a->oid, b->oid);
}

static int
obj_oid_cmp(struct object *a, struct object *b)
{
        return strcasecmp(a->oid, b->oid);
}

static int
oidname_cmp(struct oidname *a, struct oidname *b)
{
        return strcasecmp(a->on_name, b->on_name);
}

static int
symoid_cmp(struct symoid *a, struct symoid *b)
{
        return strcasecmp(a->name, b->name);
}

RB_GENERATE(attr_type_tree, attr_type, link, attr_oid_cmp);
RB_GENERATE(object_tree, object, link, obj_oid_cmp);
RB_GENERATE(oidname_tree, oidname, link, oidname_cmp);
RB_GENERATE(symoid_tree, symoid, link, symoid_cmp);

static struct attr_list *push_attr(struct attr_list *alist, struct attr_type *a);
static struct obj_list  *push_obj(struct obj_list *olist, struct object *obj);
static struct name_list *push_name(struct name_list *nl, char *name);
int                      is_oidstr(const char *oidstr);

struct attr_type *
lookup_attribute_by_name(struct schema *schema, char *name)
{
        struct oidname          *on, find;

        find.on_name = name;
        on = RB_FIND(oidname_tree, &schema->attr_names, &find);

        if (on)
                return on->on_attr_type;
        return NULL;
}

struct attr_type *
lookup_attribute_by_oid(struct schema *schema, char *oid)
{
        struct attr_type         find;

        find.oid = oid;
        return RB_FIND(attr_type_tree, &schema->attr_types, &find);
}

struct attr_type *
lookup_attribute(struct schema *schema, char *oid_or_name)
{
        if (is_oidstr(oid_or_name))
                return lookup_attribute_by_oid(schema, oid_or_name);
        return lookup_attribute_by_name(schema, oid_or_name);
}

struct object *
lookup_object_by_oid(struct schema *schema, char *oid)
{
        struct object    find;

        find.oid = oid;
        return RB_FIND(object_tree, &schema->objects, &find);
}

struct object *
lookup_object_by_name(struct schema *schema, char *name)
{
        struct oidname          *on, find;

        find.on_name = name;
        on = RB_FIND(oidname_tree, &schema->object_names, &find);

        if (on)
                return on->on_object;
        return NULL;
}

struct object *
lookup_object(struct schema *schema, char *oid_or_name)
{
        if (is_oidstr(oid_or_name))
                return lookup_object_by_oid(schema, oid_or_name);
        return lookup_object_by_name(schema, oid_or_name);
}

/*
 * Looks up a symbolic OID, optionally with a suffix OID, so if
 *   SYMBOL = 1.2.3.4
 * then
 *   SYMBOL:5.6 = 1.2.3.4.5.6
 *
 * Returned string must be freed by the caller.
 * Modifies the name argument.
 */
char *
lookup_symbolic_oid(struct schema *schema, char *name)
{
        struct symoid   *symoid, find;
        char            *colon, *oid;
        size_t           sz;

        colon = strchr(name, ':');
        if (colon != NULL) {
                if (!is_oidstr(colon + 1)) {
                        log_warnx("invalid OID after colon: %s", colon + 1);
                        return NULL;
                }
                *colon = '\0';
        }

        find.name = name;
        symoid = RB_FIND(symoid_tree, &schema->symbolic_oids, &find);
        if (symoid == NULL)
                return NULL;

        if (colon == NULL)
                return strdup(symoid->oid);

        /* Expand SYMBOL:OID. */
        sz = strlen(symoid->oid) + 1 + strlen(colon + 1) + 1;
        if ((oid = malloc(sz)) == NULL) {
                log_warnx("malloc");
                return NULL;
        }

        strlcpy(oid, symoid->oid, sz);
        strlcat(oid, ".", sz);
        strlcat(oid, colon + 1, sz);

        return oid;
}

/*
 * Push a symbol-OID pair on the tree. Name and OID must be valid pointers
 * during the lifetime of the tree.
 */
static struct symoid *
push_symbolic_oid(struct schema *schema, char *name, char *oid)
{
        struct symoid   *symoid, find;

        find.name = name;
        symoid = RB_FIND(symoid_tree, &schema->symbolic_oids, &find);

        if (symoid == NULL) {
                symoid = calloc(1, sizeof(*symoid));
                if (symoid == NULL) {
                        log_warnx("calloc");
                        return NULL;
                }

                symoid->name = name;
                RB_INSERT(symoid_tree, &schema->symbolic_oids, symoid);
        }

        free(symoid->oid);
        symoid->oid = oid;

        return symoid;
}

static struct attr_list *
push_attr(struct attr_list *alist, struct attr_type *a)
{
        struct attr_ptr         *aptr;

        if (alist == NULL) {
                if ((alist = calloc(1, sizeof(*alist))) == NULL) {
                        log_warn("calloc");
                        return NULL;
                }
                SLIST_INIT(alist);
        }

        if ((aptr = calloc(1, sizeof(*aptr))) == NULL) {
                log_warn("calloc");
                free(alist);
                return NULL;
        }
        aptr->attr_type = a;
        SLIST_INSERT_HEAD(alist, aptr, next);

        return alist;
}

static struct obj_list *
push_obj(struct obj_list *olist, struct object *obj)
{
        struct obj_ptr          *optr;

        if (olist == NULL) {
                if ((olist = calloc(1, sizeof(*olist))) == NULL) {
                        log_warn("calloc");
                        return NULL;
                }
                SLIST_INIT(olist);
        }

        if ((optr = calloc(1, sizeof(*optr))) == NULL) {
                log_warn("calloc");
                free(olist);
                return NULL;
        }
        optr->object = obj;
        SLIST_INSERT_HEAD(olist, optr, next);

        return olist;
}

int
is_oidstr(const char *oidstr)
{
        struct ber_oid   oid;
        return (ober_string2oid(oidstr, &oid) == 0);
}

static struct name_list *
push_name(struct name_list *nl, char *name)
{
        struct name     *n;

        if (nl == NULL) {
                if ((nl = calloc(1, sizeof(*nl))) == NULL) {
                        log_warn("calloc");
                        return NULL;
                }
                SLIST_INIT(nl);
        }
        if ((n = calloc(1, sizeof(*n))) == NULL) {
                log_warn("calloc");
                free(nl);
                return NULL;
        }
        n->name = name;
        SLIST_INSERT_HEAD(nl, n, next);

        return nl;
}

static int
schema_getc(struct schema *schema, int quotec)
{
        int             c, next;

        if (schema->pushback_index)
                return (schema->pushback_buffer[--schema->pushback_index]);

        if (quotec) {
                if ((c = getc(schema->fp)) == EOF) {
                        log_warnx("reached end of file while parsing "
                            "quoted string");
                        return EOF;
                }
                return (c);
        }

        while ((c = getc(schema->fp)) == '\\') {
                next = getc(schema->fp);
                if (next != '\n') {
                        c = next;
                        break;
                }
                schema->lineno++;
        }

        return (c);
}

static int
schema_ungetc(struct schema *schema, int c)
{
        if (c == EOF)
                return EOF;

        if (schema->pushback_index < SCHEMA_MAXPUSHBACK-1)
                return (schema->pushback_buffer[schema->pushback_index++] = c);
        else
                return (EOF);
}

static int
findeol(struct schema *schema)
{
        int     c;

        /* skip to either EOF or the first real EOL */
        while (1) {
                if (schema->pushback_index)
                        c = schema->pushback_buffer[--schema->pushback_index];
                else
                        c = schema_getc(schema, 0);
                if (c == '\n') {
                        schema->lineno++;
                        break;
                }
                if (c == EOF)
                        break;
        }
        return (ERROR);
}

static int
schema_lex(struct schema *schema, char **kw)
{
        char     buf[8096];
        char    *p;
        int      quotec, next, c;

        if (kw)
                *kw = NULL;

top:
        p = buf;
        while ((c = schema_getc(schema, 0)) == ' ' || c == '\t')
                ; /* nothing */

        if (c == '#')
                while ((c = schema_getc(schema, 0)) != '\n' && c != EOF)
                        ; /* nothing */

        switch (c) {
        case '\'':
        case '"':
                quotec = c;
                while (1) {
                        if ((c = schema_getc(schema, quotec)) == EOF)
                                return (0);
                        if (c == '\n') {
                                schema->lineno++;
                                continue;
                        } else if (c == '\\') {
                                if ((next = schema_getc(schema, quotec)) == EOF)
                                        return (0);
                                if (next == quotec || c == ' ' || c == '\t')
                                        c = next;
                                else if (next == '\n')
                                        continue;
                                else
                                        schema_ungetc(schema, next);
                        } else if (c == quotec) {
                                *p = '\0';
                                break;
                        }
                        if (p + 1 >= buf + sizeof(buf) - 1) {
                                log_warnx("string too long");
                                return (findeol(schema));
                        }
                        *p++ = (char)c;
                }
                if (kw != NULL && (*kw = strdup(buf)) == NULL)
                        fatal("schema_lex: strdup");
                return (STRING);
        }

#define allowed_in_string(x) \
        (isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
        x != '{' && x != '}' && x != '<' && x != '>' && \
        x != '!' && x != '=' && x != '/' && x != '#' && \
        x != ','))

        if (isalnum(c) || c == ':' || c == '_' || c == '*') {
                do {
                        *p++ = c;
                        if ((size_t)(p-buf) >= sizeof(buf)) {
                                log_warnx("string too long");
                                return (findeol(schema));
                        }
                } while ((c = schema_getc(schema, 0)) != EOF && (allowed_in_string(c)));
                schema_ungetc(schema, c);
                *p = '\0';
                if (kw != NULL && (*kw = strdup(buf)) == NULL)
                        fatal("schema_lex: strdup");
                return STRING;
        }
        if (c == '\n') {
                schema->lineno++;
                goto top;
        }
        if (c == EOF)
                return (0);
        return (c);
}

struct schema *
schema_new(void)
{
        struct schema   *schema;

        if ((schema = calloc(1, sizeof(*schema))) == NULL)
                return NULL;

        RB_INIT(&schema->attr_types);
        RB_INIT(&schema->attr_names);
        RB_INIT(&schema->objects);
        RB_INIT(&schema->object_names);
        RB_INIT(&schema->symbolic_oids);

        return schema;
}

static void
schema_err(struct schema *schema, const char *fmt, ...)
{
        va_list          ap;
        char            *msg;

        va_start(ap, fmt);
        if (vasprintf(&msg, fmt, ap) == -1)
                fatal("vasprintf");
        va_end(ap);
        logit(LOG_CRIT, "%s:%d: %s", schema->filename, schema->lineno, msg);
        free(msg);

        schema->error++;
}

static int
schema_link_attr_name(struct schema *schema, const char *name, struct attr_type *attr)
{
        struct oidname          *oidname, *prev;

        if ((oidname = calloc(1, sizeof(*oidname))) == NULL) {
                log_warn("calloc");
                return -1;
        }

        oidname->on_name = name;
        oidname->on_attr_type = attr;
        prev = RB_INSERT(oidname_tree, &schema->attr_names, oidname);
        if (prev != NULL) {
                schema_err(schema, "attribute type name '%s'"
                    " already defined for oid %s",
                    name, prev->on_attr_type->oid);
                free(oidname);
                return -1;
        }

        return 0;
}

static int
schema_link_attr_names(struct schema *schema, struct attr_type *attr)
{
        struct name     *name;

        SLIST_FOREACH(name, attr->names, next) {
                if (schema_link_attr_name(schema, name->name, attr) != 0)
                        return -1;
        }
        return 0;
}

static int
schema_link_obj_name(struct schema *schema, const char *name, struct object *obj)
{
        struct oidname          *oidname, *prev;

        if ((oidname = calloc(1, sizeof(*oidname))) == NULL) {
                log_warn("calloc");
                return -1;
        }

        oidname->on_name = name;
        oidname->on_object = obj;
        prev = RB_INSERT(oidname_tree, &schema->object_names, oidname);
        if (prev != NULL) {
                schema_err(schema, "object class name '%s'"
                    " already defined for oid %s",
                    name, prev->on_object->oid);
                free(oidname);
                return -1;
        }

        return 0;
}

static int
schema_link_obj_names(struct schema *schema, struct object *obj)
{
        struct name     *name;

        SLIST_FOREACH(name, obj->names, next) {
                if (schema_link_obj_name(schema, name->name, obj) != 0)
                        return -1;
        }
        return 0;
}

static struct name_list *
schema_parse_names(struct schema *schema)
{
        struct name_list        *nlist = NULL;
        char                    *kw;
        int                      token;

        token = schema_lex(schema, &kw);
        if (token == STRING)
                return push_name(NULL, kw);

        if (token != '(')
                goto fail;

        for (;;) {
                token = schema_lex(schema, &kw);
                if (token == ')')
                        break;
                if (token != STRING)
                        goto fail;
                nlist = push_name(nlist, kw);
        }

        return nlist;

fail:
        free(kw);
        /* FIXME: leaks nlist here */
        return NULL;
}

static void
schema_free_name_list(struct name_list *nlist)
{
        struct name     *name;

        while ((name = SLIST_FIRST(nlist)) != NULL) {
                SLIST_REMOVE_HEAD(nlist, next);
                free(name->name);
                free(name);
        }
        free(nlist);
}

static struct attr_list *
schema_parse_attrlist(struct schema *schema)
{
        struct attr_list        *alist = NULL;
        struct attr_type        *attr;
        char                    *kw;
        int                      token, want_dollar = 0;

        token = schema_lex(schema, &kw);
        if (token == STRING) {
                if ((attr = lookup_attribute(schema, kw)) == NULL) {
                        schema_err(schema, "undeclared attribute type '%s'", kw);
                        goto fail;
                }
                free(kw);
                return push_attr(NULL, attr);
        }

        if (token != '(')
                goto fail;

        for (;;) {
                token = schema_lex(schema, &kw);
                if (token == ')')
                        break;
                if (token == '$') {
                        if (!want_dollar)
                                goto fail;
                        want_dollar = 0;
                        continue;
                }
                if (token != STRING)
                        goto fail;
                if ((attr = lookup_attribute(schema, kw)) == NULL) {
                        schema_err(schema, "%s: no such attribute", kw);
                        goto fail;
                }
                alist = push_attr(alist, attr);
                free(kw);
                want_dollar = 1;
        }

        return alist;

fail:
        free(kw);
        /* FIXME: leaks alist here */
        return NULL;
}

static struct obj_list *
schema_parse_objlist(struct schema *schema)
{
        struct obj_list *olist = NULL;
        struct object   *obj;
        char            *kw;
        int              token, want_dollar = 0;

        token = schema_lex(schema, &kw);
        if (token == STRING) {
                if ((obj = lookup_object(schema, kw)) == NULL) {
                        schema_err(schema, "undeclared object class '%s'", kw);
                        goto fail;
                }
                free(kw);
                return push_obj(NULL, obj);
        }

        if (token != '(')
                goto fail;

        for (;;) {
                token = schema_lex(schema, &kw);
                if (token == ')')
                        break;
                if (token == '$') {
                        if (!want_dollar)
                                goto fail;
                        want_dollar = 0;
                        continue;
                }
                if (token != STRING)
                        goto fail;
                if ((obj = lookup_object(schema, kw)) == NULL)
                        goto fail;
                olist = push_obj(olist, obj);
                want_dollar = 1;
        }

        return olist;

fail:
        free(kw);
        /* FIXME: leaks olist here */
        return NULL;
}

static int
schema_validate_match_rule(struct schema *schema, struct attr_type *at,
    const struct match_rule *mrule, enum match_rule_type type)
{
        int i;

        if (mrule == NULL)
                return 0;

        if ((mrule->type & type) != type) {
                schema_err(schema, "%s: bad matching rule '%s'",
                    ATTR_NAME(at), mrule->name);
                return -1;
        }

        /* Is this matching rule compatible with the attribute syntax? */
        if (strcmp(mrule->syntax_oid, at->syntax->oid) == 0)
                return 0;

        /* Check any alternative syntaxes for compatibility. */
        for (i = 0; mrule->alt_syntax_oids && mrule->alt_syntax_oids[i]; i++)
                if (strcmp(mrule->alt_syntax_oids[i], at->syntax->oid) == 0)
                        return 0;

        schema_err(schema, "%s: inappropriate matching rule '%s' for syntax [%s]",
            ATTR_NAME(at), mrule->name, at->syntax->oid);
        return -1;
}

static int
schema_parse_attributetype(struct schema *schema)
{
        struct attr_type        *attr = NULL, *prev, *sup;
        struct name_list        *xnames;
        char                    *kw = NULL, *arg = NULL;
        int                      token, ret = 0, c;

        if (schema_lex(schema, NULL) != '(')
                goto fail;

        if (schema_lex(schema, &kw) != STRING)
                goto fail;

        if ((attr = calloc(1, sizeof(*attr))) == NULL) {
                log_warn("calloc");
                goto fail;
        }
        attr->usage = USAGE_USER_APP;

        if (is_oidstr(kw))
                attr->oid = kw;
        else {
                attr->oid = lookup_symbolic_oid(schema, kw);
                if (attr->oid == NULL)
                        goto fail;
                free(kw);
        }
        kw = NULL;

        prev = RB_INSERT(attr_type_tree, &schema->attr_types, attr);
        if (prev != NULL) {
                schema_err(schema, "attribute type %s already defined", attr->oid);
                goto fail;
        }

        while (ret == 0) {
                token = schema_lex(schema, &kw);
                if (token == ')')
                        break;
                else if (token != STRING)
                        goto fail;
                if (strcasecmp(kw, "NAME") == 0) {
                        attr->names = schema_parse_names(schema);
                        if (attr->names == NULL)
                                goto fail;
                        schema_link_attr_names(schema, attr);
                } else if (strcasecmp(kw, "DESC") == 0) {
                        if (schema_lex(schema, &attr->desc) != STRING)
                                goto fail;
                } else if (strcasecmp(kw, "OBSOLETE") == 0) {
                        attr->obsolete = 1;
                } else if (strcasecmp(kw, "SUP") == 0) {
                        if (schema_lex(schema, &arg) != STRING)
                                goto fail;
                        if ((attr->sup = lookup_attribute(schema, arg)) == NULL) {
                                schema_err(schema, "%s: no such attribute", arg);
                                goto fail;
                        }
                        free(arg);
                } else if (strcasecmp(kw, "EQUALITY") == 0) {
                        if (schema_lex(schema, &arg) != STRING)
                                goto fail;
                        if ((attr->equality = match_rule_lookup(arg)) == NULL) {
                                schema_err(schema, "%s: unknown matching rule",
                                    arg);
                                goto fail;
                        }
                        free(arg);
                } else if (strcasecmp(kw, "ORDERING") == 0) {
                        if (schema_lex(schema, &arg) != STRING)
                                goto fail;
                        if ((attr->ordering = match_rule_lookup(arg)) == NULL) {
                                schema_err(schema, "%s: unknown matching rule",
                                    arg);
                                goto fail;
                        }
                        free(arg);
                } else if (strcasecmp(kw, "SUBSTR") == 0) {
                        if (schema_lex(schema, &arg) != STRING)
                                goto fail;
                        if ((attr->substr = match_rule_lookup(arg)) == NULL) {
                                schema_err(schema, "%s: unknown matching rule",
                                    arg);
                                goto fail;
                        }
                        free(arg);
                } else if (strcasecmp(kw, "SYNTAX") == 0) {
                        if (schema_lex(schema, &arg) != STRING ||
                            !is_oidstr(arg))
                                goto fail;

                        if ((attr->syntax = syntax_lookup(arg)) == NULL) {
                                schema_err(schema, "syntax not supported: %s",
                                    arg);
                                goto fail;
                        }

                        if ((c = schema_getc(schema, 0)) == '{') {
                                if (schema_lex(schema, NULL) != STRING ||
                                    schema_lex(schema, NULL) != '}')
                                        goto fail;
                        } else
                                schema_ungetc(schema, c);
                        free(arg);
                } else if (strcasecmp(kw, "SINGLE-VALUE") == 0) {
                        attr->single = 1;
                } else if (strcasecmp(kw, "COLLECTIVE") == 0) {
                        attr->collective = 1;
                } else if (strcasecmp(kw, "NO-USER-MODIFICATION") == 0) {
                        attr->immutable = 1;
                } else if (strcasecmp(kw, "USAGE") == 0) {
                        if (schema_lex(schema, &arg) != STRING)
                                goto fail;
                        if (strcasecmp(arg, "dSAOperation") == 0)
                                attr->usage = USAGE_DSA_OP;
                        else if (strcasecmp(arg, "directoryOperation") == 0)
                                attr->usage = USAGE_DIR_OP;
                        else if (strcasecmp(arg, "distributedOperation") == 0)
                                attr->usage = USAGE_DIST_OP;
                        else if (strcasecmp(arg, "userApplications") == 0)
                                attr->usage = USAGE_USER_APP;
                        else {
                                schema_err(schema, "invalid usage '%s'", arg);
                                goto fail;
                        }
                        free(arg);
                } else if (strncmp(kw, "X-", 2) == 0) {
                        /* unknown extension, eat argument(s) */
                        xnames = schema_parse_names(schema);
                        if (xnames == NULL)
                                goto fail;
                        schema_free_name_list(xnames);
                } else {
                        schema_err(schema, "syntax error at token '%s'", kw);
                        goto fail;
                }
                free(kw);
                kw = NULL;
        }

        /* Check that a syntax is defined, either directly or
         * indirectly via a superior attribute type.
         */
        sup = attr->sup;
        while (attr->syntax == NULL && sup != NULL) {
                attr->syntax = sup->syntax;
                sup = sup->sup;
        } 
        if (attr->syntax == NULL) {
                schema_err(schema, "%s: no syntax defined", ATTR_NAME(attr));
                goto fail;
        }

        /* If the attribute type doesn't explicitly define equality, check
         * if any superior attribute type does.
         */
        sup = attr->sup;
        while (attr->equality == NULL && sup != NULL) {
                attr->equality = sup->equality;
                sup = sup->sup;
        } 
        /* Same thing with ordering matching rule. */
        sup = attr->sup;
        while (attr->ordering == NULL && sup != NULL) {
                attr->ordering = sup->ordering;
                sup = sup->sup;
        } 
        /* ...and substring matching rule. */
        sup = attr->sup;
        while (attr->substr == NULL && sup != NULL) {
                attr->substr = sup->substr;
                sup = sup->sup;
        } 

        if (schema_validate_match_rule(schema, attr, attr->equality, MATCH_EQUALITY) != 0 ||
            schema_validate_match_rule(schema, attr, attr->ordering, MATCH_ORDERING) != 0 ||
            schema_validate_match_rule(schema, attr, attr->substr, MATCH_SUBSTR) != 0)
                goto fail;

        return 0;

fail:
        free(kw);
        if (attr != NULL) {
                if (attr->oid != NULL) {
                        RB_REMOVE(attr_type_tree, &schema->attr_types, attr);
                        free(attr->oid);
                }
                free(attr->desc);
                free(attr);
        }
        return -1;
}

static int
schema_parse_objectclass(struct schema *schema)
{
        struct object           *obj = NULL, *prev;
        struct obj_ptr          *optr;
        struct name_list        *xnames;
        char                    *kw = NULL;
        int                      token, ret = 0;

        if (schema_lex(schema, NULL) != '(')
                goto fail;

        if (schema_lex(schema, &kw) != STRING)
                goto fail;

        if ((obj = calloc(1, sizeof(*obj))) == NULL) {
                log_warn("calloc");
                goto fail;
        }
        obj->kind = KIND_STRUCTURAL;

        if (is_oidstr(kw))
                obj->oid = kw;
        else {
                obj->oid = lookup_symbolic_oid(schema, kw);
                if (obj->oid == NULL)
                        goto fail;
                free(kw);
        }
        kw = NULL;

        prev = RB_INSERT(object_tree, &schema->objects, obj);
        if (prev != NULL) {
                schema_err(schema, "object class %s already defined", obj->oid);
                goto fail;
        }

        while (ret == 0) {
                token = schema_lex(schema, &kw);
                if (token == ')')
                        break;
                else if (token != STRING)
                        goto fail;
                if (strcasecmp(kw, "NAME") == 0) {
                        obj->names = schema_parse_names(schema);
                        if (obj->names == NULL)
                                goto fail;
                        schema_link_obj_names(schema, obj);
                } else if (strcasecmp(kw, "DESC") == 0) {
                        if (schema_lex(schema, &obj->desc) != STRING)
                                goto fail;
                } else if (strcasecmp(kw, "OBSOLETE") == 0) {
                        obj->obsolete = 1;
                } else if (strcasecmp(kw, "SUP") == 0) {
                        obj->sup = schema_parse_objlist(schema);
                        if (obj->sup == NULL)
                                goto fail;
                } else if (strcasecmp(kw, "ABSTRACT") == 0) {
                        obj->kind = KIND_ABSTRACT;
                } else if (strcasecmp(kw, "STRUCTURAL") == 0) {
                        obj->kind = KIND_STRUCTURAL;
                } else if (strcasecmp(kw, "AUXILIARY") == 0) {
                        obj->kind = KIND_AUXILIARY;
                } else if (strcasecmp(kw, "MUST") == 0) {
                        obj->must = schema_parse_attrlist(schema);
                        if (obj->must == NULL)
                                goto fail;
                } else if (strcasecmp(kw, "MAY") == 0) {
                        obj->may = schema_parse_attrlist(schema);
                        if (obj->may == NULL)
                                goto fail;
                } else if (strncasecmp(kw, "X-", 2) == 0) {
                        /* unknown extension, eat argument(s) */
                        xnames = schema_parse_names(schema);
                        if (xnames == NULL)
                                goto fail;
                        schema_free_name_list(xnames);
                } else {
                        schema_err(schema, "syntax error at token '%s'", kw);
                        goto fail;
                }
                free(kw);
                kw = NULL;
        }

        /* Verify the subclassing is allowed.
         *
         * Structural object classes cannot subclass auxiliary object classes.
         * Auxiliary object classes cannot subclass structural object classes.
         * Abstract object classes cannot derive from structural or auxiliary
         *   object classes.
         */
        if (obj->sup != NULL) {
                SLIST_FOREACH(optr, obj->sup, next) {
                        if (obj->kind == KIND_STRUCTURAL &&
                            optr->object->kind == KIND_AUXILIARY) {
                                log_warnx("structural object class '%s' cannot"
                                    " subclass auxiliary object class '%s'",
                                    OBJ_NAME(obj), OBJ_NAME(optr->object));
                                goto fail;
                        }

                        if (obj->kind == KIND_AUXILIARY &&
                            optr->object->kind == KIND_STRUCTURAL) {
                                log_warnx("auxiliary object class '%s' cannot"
                                    " subclass structural object class '%s'",
                                    OBJ_NAME(obj), OBJ_NAME(optr->object));
                                goto fail;
                        }

                        if (obj->kind == KIND_ABSTRACT &&
                            optr->object->kind != KIND_ABSTRACT) {
                                log_warnx("abstract object class '%s' cannot"
                                    " subclass non-abstract object class '%s'",
                                    OBJ_NAME(obj), OBJ_NAME(optr->object));
                                goto fail;
                        }
                }
        }

        return 0;

fail:
        free(kw);
        if (obj != NULL) {
                if (obj->oid != NULL) {
                        RB_REMOVE(object_tree, &schema->objects, obj);
                        free(obj->oid);
                }
                free(obj->desc);
                free(obj);
        }
        return -1;
}

static int
schema_parse_objectidentifier(struct schema *schema)
{
        char            *symname = NULL, *symoid = NULL;
        char            *oid = NULL;

        if (schema_lex(schema, &symname) != STRING)
                goto fail;
        if (schema_lex(schema, &symoid) != STRING)
                goto fail;

        if (is_oidstr(symoid)) {
                oid = symoid;
                symoid = NULL;
        } else if ((oid = lookup_symbolic_oid(schema, symoid)) == NULL)
                goto fail;

        if (push_symbolic_oid(schema, symname, oid) == NULL)
                goto fail;

        free(symoid);
        return 0;

fail:
        free(symname);
        free(symoid);
        free(oid);
        return -1;
}

int
schema_parse(struct schema *schema, const char *filename)
{
        char    *kw;
        int      token, ret = 0;

        log_debug("parsing schema file '%s'", filename);

        if ((schema->fp = fopen(filename, "r")) == NULL) {
                log_warn("%s", filename);
                return -1;
        }
        schema->filename = filename;
        schema->lineno = 1;

        while (ret == 0) {
                token = schema_lex(schema, &kw);
                if (token == STRING) {
                        if (strcasecmp(kw, "attributetype") == 0)
                                ret = schema_parse_attributetype(schema);
                        else if (strcasecmp(kw, "objectclass") == 0)
                                ret = schema_parse_objectclass(schema);
                        else if (strcasecmp(kw, "objectidentifier") == 0)
                                ret = schema_parse_objectidentifier(schema);
                        else {
                                schema_err(schema, "syntax error at '%s'", kw);
                                ret = -1;
                        }
                        if (ret == -1 && schema->error == 0)
                                schema_err(schema, "syntax error");
                        free(kw);
                } else if (token == 0) {        /* EOF */
                        break;
                } else {
                        schema_err(schema, "syntax error");
                        ret = -1;
                }
        }

        fclose(schema->fp);
        schema->fp = NULL;
        schema->filename = NULL;

        return ret;
}

static int
schema_dump_names(const char *desc, struct name_list *nlist,
    char *buf, size_t size)
{
        struct name     *name;

        if (nlist == NULL || SLIST_EMPTY(nlist))
                return 0;

        if (strlcat(buf, " ", size) >= size ||
            strlcat(buf, desc, size) >= size)
                return -1;

        name = SLIST_FIRST(nlist);
        if (SLIST_NEXT(name, next) == NULL) {
                /* single name, no parenthesis */
                if (strlcat(buf, " '", size) >= size ||
                    strlcat(buf, name->name, size) >= size ||
                    strlcat(buf, "'", size) >= size)
                        return -1;
        } else {
                if (strlcat(buf, " ( ", size) >= size)
                        return -1;
                SLIST_FOREACH(name, nlist, next)
                        if (strlcat(buf, "'", size) >= size ||
                            strlcat(buf, name->name, size) >= size ||
                            strlcat(buf, "' ", size) >= size)
                                return -1;
                if (strlcat(buf, ")", size) >= size)
                        return -1;
        }

        return 0;
}

static int
schema_dump_attrlist(const char *desc, struct attr_list *alist,
    char *buf, size_t size)
{
        struct attr_ptr         *aptr;

        if (alist == NULL || SLIST_EMPTY(alist))
                return 0;

        if (strlcat(buf, " ", size) >= size ||
            strlcat(buf, desc, size) >= size)
                return -1;

        aptr = SLIST_FIRST(alist);
        if (SLIST_NEXT(aptr, next) == NULL) {
                /* single attribute, no parenthesis */
                if (strlcat(buf, " ", size) >= size ||
                    strlcat(buf, ATTR_NAME(aptr->attr_type), size) >= size)
                        return -1;
        } else {
                if (strlcat(buf, " ( ", size) >= size)
                        return -1;
                SLIST_FOREACH(aptr, alist, next) {
                        if (strlcat(buf, ATTR_NAME(aptr->attr_type),
                            size) >= size ||
                            strlcat(buf, " ", size) >= size)
                                return -1;
                        if (SLIST_NEXT(aptr, next) != NULL &&
                            strlcat(buf, "$ ", size) >= size)
                                return -1;
                }
                if (strlcat(buf, ")", size) >= size)
                        return -1;
        }

        return 0;
}

static int
schema_dump_objlist(const char *desc, struct obj_list *olist,
    char *buf, size_t size)
{
        struct obj_ptr          *optr;

        if (olist == NULL || SLIST_EMPTY(olist))
                return 0;

        if (strlcat(buf, " ", size) >= size ||
            strlcat(buf, desc, size) >= size)
                return -1;

        optr = SLIST_FIRST(olist);
        if (SLIST_NEXT(optr, next) == NULL) {
                /* single attribute, no parenthesis */
                if (strlcat(buf, " ", size) >= size ||
                    strlcat(buf, OBJ_NAME(optr->object), size) >= size)
                        return -1;
        } else {
                if (strlcat(buf, " ( ", size) >= size)
                        return -1;
                SLIST_FOREACH(optr, olist, next) {
                        if (strlcat(buf, OBJ_NAME(optr->object), size) >= size ||
                            strlcat(buf, " ", size) >= size)
                                return -1;
                        if (SLIST_NEXT(optr, next) != NULL &&
                            strlcat(buf, "$ ", size) >= size)
                                return -1;
                }
                if (strlcat(buf, ")", size) >= size)
                        return -1;
        }

        return 0;
}

int
schema_dump_object(struct object *obj, char *buf, size_t size)
{
        if (strlcpy(buf, "( ", size) >= size ||
            strlcat(buf, obj->oid, size) >= size)
                return -1;

        if (schema_dump_names("NAME", obj->names, buf, size) != 0)
                return -1;

        if (obj->desc != NULL)
                if (strlcat(buf, " DESC '", size) >= size ||
                    strlcat(buf, obj->desc, size) >= size ||
                    strlcat(buf, "'", size) >= size)
                        return -1;

        switch (obj->kind) {
        case KIND_STRUCTURAL:
                if (strlcat(buf, " STRUCTURAL", size) >= size)
                        return -1;
                break;
        case KIND_ABSTRACT:
                if (strlcat(buf, " ABSTRACT", size) >= size)
                        return -1;
                break;
        case KIND_AUXILIARY:
                if (strlcat(buf, " AUXILIARY", size) >= size)
                        return -1;
                break;
        }

        if (schema_dump_objlist("SUP", obj->sup, buf, size) != 0)
                return -1;

        if (obj->obsolete && strlcat(buf, " OBSOLETE", size) >= size)
                return -1;

        if (schema_dump_attrlist("MUST", obj->must, buf, size) != 0)
                return -1;

        if (schema_dump_attrlist("MAY", obj->may, buf, size) != 0)
                return -1;

        if (strlcat(buf, " )", size) >= size)
                return -1;

        return 0;
}

int
schema_dump_attribute(struct attr_type *at, char *buf, size_t size)
{
        if (strlcpy(buf, "( ", size) >= size ||
            strlcat(buf, at->oid, size) >= size)
                return -1;

        if (schema_dump_names("NAME", at->names, buf, size) != 0)
                return -1;

        if (at->desc != NULL)
                if (strlcat(buf, " DESC '", size) >= size ||
                    strlcat(buf, at->desc, size) >= size ||
                    strlcat(buf, "'", size) >= size)
                        return -1;

        if (at->obsolete && strlcat(buf, " OBSOLETE", size) >= size)
                return -1;

        if (at->sup != NULL)
                if (strlcat(buf, " SUP ", size) >= size ||
                    strlcat(buf, ATTR_NAME(at->sup), size) >= size)
                        return -1;

        if (at->equality != NULL)
                if (strlcat(buf, " EQUALITY ", size) >= size ||
                    strlcat(buf, at->equality->name, size) >= size)
                        return -1;

        if (at->ordering != NULL)
                if (strlcat(buf, " ORDERING ", size) >= size ||
                    strlcat(buf, at->ordering->name, size) >= size)
                        return -1;

        if (at->substr != NULL)
                if (strlcat(buf, " SUBSTR ", size) >= size ||
                    strlcat(buf, at->substr->name, size) >= size)
                        return -1;

        if (at->syntax != NULL)
                if (strlcat(buf, " SYNTAX ", size) >= size ||
                    strlcat(buf, at->syntax->oid, size) >= size)
                        return -1;

        if (at->single && strlcat(buf, " SINGLE-VALUE", size) >= size)
                return -1;

        if (at->collective && strlcat(buf, " COLLECTIVE", size) >= size)
                return -1;

        if (at->immutable && strlcat(buf, " NO-USER-MODIFICATION", size) >= size)
                return -1;

        switch (at->usage) {
        case USAGE_USER_APP:
                /* User application usage is the default. */
                break;
        case USAGE_DIR_OP:
                if (strlcat(buf, " USAGE directoryOperation", size) >= size)
                        return -1;
                break;
        case USAGE_DIST_OP:
                if (strlcat(buf, " USAGE distributedOperation", size) >= size)
                        return -1;
                break;
        case USAGE_DSA_OP:
                if (strlcat(buf, " USAGE dSAOperation", size) >= size)
                        return -1;
                break;
        }

        if (strlcat(buf, " )", size) >= size)
                return -1;

        return 0;
}

int
schema_dump_match_rule(struct match_rule *mr, char *buf, size_t size)
{
        if (strlcpy(buf, "( ", size) >= size ||
            strlcat(buf, mr->oid, size) >= size ||
            strlcat(buf, " NAME '", size) >= size ||
            strlcat(buf, mr->name, size) >= size ||
            strlcat(buf, "' SYNTAX ", size) >= size ||
            strlcat(buf, mr->syntax_oid, size) >= size ||
            strlcat(buf, " )", size) >= size)
                return -1;

        return 0;
}