[BACK]Return to schema.c CVS log [TXT][DIR] Up to [local] / src / usr.sbin / ldapd

File: [local] / src / usr.sbin / ldapd / schema.c (download)

Revision 1.20, Wed Oct 12 11:57:40 2022 UTC (19 months, 2 weeks ago) by jsg
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, OPENBSD_7_4_BASE, OPENBSD_7_4, OPENBSD_7_3_BASE, OPENBSD_7_3, HEAD
Changes since 1.19: +3 -1 lines

avoid use after free in error paths
ok miod@ martijn@

/*	$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;
}