/* -*- show-trailing-whitespace: t; indent-tabs: t -*-
 * Copyright (c) 2003,2004,2005,2006 David Lichteblau
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
#define _XOPEN_SOURCE
#include <unistd.h>
#include "common.h"

#define fast_g_string_append_c(gstring, c)				\
	do {								\
		if ((gstring)->len + 1 >= (gstring)->allocated_len)	\
			g_string_append_c((gstring), (c));		\
		else {							\
			(gstring)->str[(gstring)->len++] = (c);		\
			(gstring)->str[(gstring)->len] = 0;		\
		}							\
	} while (0)

/*
 * 0: ok
 * -1: parse error
 * -2: line is just "-"
 */
static int
ldif_read_ad(FILE *s, GString *lhs)
{
	int c;

	for (;;) {
		switch ( c = getc_unlocked(s)) {
		case ':':
			if (ferror(s)) syserr();
			return 0;
		case EOF:
			fputs("Error: Unexpected EOF.\n", stderr);
			return -1;
		case '\r':
			if (fgetc(s) != '\n')
				return -1;
			/* fall through */
		case '\n':
			if (lhs->len) {
				if ( (c = fgetc(s)) == ' ')
					/* folded line */
					break;
				ungetc(c, s);
				if (lhs->len == 1 && lhs->str[0] == '-')
					return -2;
			}
			fputs("Error: Unexpected EOL.\n", stderr);
			return -1;
		case 0:
			fputs("Error: Null byte not allowed.\n", stderr);
			return -1;
		default:
			fast_g_string_append_c(lhs, c);
		}
	}
}

static int
ldif_read_encoding(FILE *s)
{
	int c;

	for (;;) {
		switch ( c = getc_unlocked(s)) {
		case ' ':
			break;
		case ':': /* fall through */
		case '<':
			return c;
		case EOF:
			fputs("Error: Unexpected EOF.\n", stderr);
			return -1;
		case '\r':
			if (fgetc(s) != '\n')
				return -1;
			/* fall through */
		case '\n':
			if ( (c = fgetc(s)) == ' ') /* folded line */ break;
			ungetc(c, s);
			return '\n';
		case 0:
			fputs("Error: Null byte not allowed.\n", stderr);
			return -1;
		default:
			ungetc(c, s);
			return 0;
		}
	}
}

static int
ldif_read_safe(FILE *s, GString *data)
{
	int c;

	for (;;)
		switch ( c = getc_unlocked(s)) {
		case '\r':
			if (fgetc(s) != '\n')
				return -1;
			/* fall through */
		case '\n':
			if ( (c = fgetc(s)) == ' ') /* folded line */ break;
			ungetc(c, s);
			if (ferror(s)) syserr();
			return 0;
		case EOF:
			fputs("Error: Unexpected EOF.\n", stderr);
			return -1;
		default:
			fast_g_string_append_c(data, c);
		}
}

static int
ldif_read_from_file(GString *data, char *name)
{
	int fd, n;
	if ( (fd = open(name, O_RDONLY)) == -1) {
		perror("open");
		return -1;
	}
	data->len = 0;
	n = 1024;
	do {
		int olen = data->len;
		g_string_set_size(data, data->len + n);
		if ( (n = read(fd, data->str + olen, n)) == -1) syserr();
		data->len = olen + n;
	} while (n > 0);
	if (close(fd) == -1) syserr();
	return 0;
}

static int
ldif_skip_comment(FILE *s)
{
	int c;

	for (;;)
		switch ( c = fgetc(s)) {
		case EOF:
			fputs("Error: Unexpected EOF.\n", stderr);
			return -1;
		case '\r':
			if (fgetc(s) != '\n')
				return -1;
			/* fall through */
		case '\n':
			if ( (c = fgetc(s)) == ' ') /* folded line */ break;
			ungetc(c, s);
			if (ferror(s)) syserr();
			return 0;
		}
}

/*
 * Read an LDIF line.
 *
 * 0: ok                           if name->len != 0
 * 0: end of file or empty line    if name->len == 0
 * -1: parse error
 * -2: line is just "-"
 */
static int
ldif_read_line1(FILE *s, GString *name, GString *value)
{
	int c;
	char encoding;
	unsigned char *ustr;
	int len;

	g_string_truncate(name, 0);
	g_string_truncate(value, 0);

	/* skip comment lines */
	do {
		c = fgetc(s);
		switch (c) {
		case EOF:
			if (ferror(s)) syserr();
			return 0;
		case '\n':
			return 0;
		case '\r':
			if (fgetc(s) != '\n')
				return -1;
			return 0;
		case '#':
			if (ldif_skip_comment(s) == -1) return -1;
			break;
		default:
			ungetc(c, s);
			c = -1;
		}
	} while (c != -1);

	if ( c = ldif_read_ad(s, name)) return c;
	if ( (encoding = ldif_read_encoding(s)) == -1) return -1;

	switch (encoding) {
	case 0:
		if (ldif_read_safe(s, value) == -1)
			return -1;
		break;
        case '\n':
                break;
	case ':':
		if (ldif_read_safe(s, value) == -1) return -1;
		ustr = (unsigned char *) value->str;;
		if ( (len = read_base64(value->str, ustr, value->len)) == -1) {
			fputs("Error: Invalid Base64 string.\n", stderr);
			return -1;
		}
		g_string_truncate(value, len);
		break;
	case '<':
		if (ldif_read_safe(s, value) == -1) return -1;
		if (strncmp(value->str, "file://", 7)) {
			fputs("Error: Unknown URL scheme.\n", stderr);
			return -1;
		}
		if (ldif_read_from_file(value, value->str + 7) == -1)
			return -1;
		break;
	default:
		abort();
	}
	return 0;
}


/*
 * Read an LDIF line ("-" not allowed).
 *
 * 0: ok                           if name->len != 0
 * 0: end of file or empty line    if name->len == 0
 * -1: parse error
 */
static int
ldif_read_line(FILE *s, GString *name, GString *value)
{
	int rc = ldif_read_line1(s, name, value);
	if (rc == -2) {
		fputs("Error: Unexpected EOL.\n", stderr);
		rc = -1;
	}
	return rc;
}

static char *
ldif_read_rename_body(FILE *s,
		      GString *tmp1, GString *tmp2,
		      char *olddn,
		      int *deleteoldrdn)
{
	char *newrdn;
	char *dn;
	int i;

	if (ldif_read_line(s, tmp1, tmp2) == -1) return 0;
	if (strcmp(tmp1->str, "newrdn")) {
		fputs("Error: Expected 'newrdn'.\n", stderr);
		return 0;
	}
	i = tmp2->len;
	newrdn = xdup(tmp2->str);

	if (ldif_read_line(s, tmp1, tmp2) == -1) {
		free(newrdn);
		return 0;
	}
	if (strcmp(tmp1->str, "deleteoldrdn")) {
		fputs("Error: Expected 'deleteoldrdn'.\n", stderr);
		free(newrdn);
		return 0;
	}
	if (!strcmp(tmp2->str, "0"))
		*deleteoldrdn = 0;
	else if (!strcmp(tmp2->str, "1"))
		*deleteoldrdn = 1;
	else {
		fputs("Error: Expected '0' or '1' for 'deleteoldrdn'.\n",
		      stderr);
		free(newrdn);
		return 0;
	}

	if (ldif_read_line(s, tmp1, tmp2) == -1) return 0;
	if (tmp1->len == 0) {
		char *komma = strchr(olddn, ',');
		if (!komma) {
			/* probably cannot rename an entry directly below
			 * the Root DSE, but let's play along for now */
			return newrdn;
		}
		dn = xalloc(i + strlen(komma) + 1);
		strcpy(dn, newrdn);
		strcpy(dn + i, komma);
		free(newrdn);
		return dn;
	}
	if (strcmp(tmp1->str, "newsuperior")) {
		free(newrdn);
		fputs("Error: Garbage at end of moddn record.\n", stderr);
		return 0;
	}
	if (tmp2->len == 0)
		return newrdn;

	dn = xalloc(i + tmp2->len + 2);
	strcpy(dn, newrdn);
	dn[i] = ',';
	strcpy(dn + i + 1, tmp2->str);
	free(newrdn);
	return dn;
}

static int
ldif_read_nothing(FILE *s, GString *tmp1, GString *tmp2)
{
	if (ldif_read_line(s, tmp1, tmp2) == -1)
		return -1;
	if (tmp1->len) {
		fputs("Error: Garbage at end of record.\n", stderr);
		return -1;
	}
	return 0;
}

static LDAPMod *
ldif_ldapmod4line(char *action, char *ad)
{
	LDAPMod *m;
	int op;

 	if (!strcmp(action, "add"))
		op = LDAP_MOD_ADD;
	else if (!strcmp(action, "delete"))
		op = LDAP_MOD_DELETE;
	else if (!strcmp(action, "replace"))
		op = LDAP_MOD_REPLACE;
	else {
		fputs(action, stderr);
		fputs("Error: Invalid change marker.\n", stderr);
		return 0;
	}

	m = xalloc(sizeof(LDAPMod));
	m->mod_op = op | LDAP_MOD_BVALUES;
	m->mod_type = xdup(ad);
	return m;
}

static LDAPMod **
ldif_read_modify_body(FILE *s, GString *tmp1, GString *tmp2)
{
	LDAPMod **result;
	GPtrArray *mods = g_ptr_array_new();
	GPtrArray *values;
	LDAPMod *m = 0;
	int rc;

	for (;;) {
		switch (ldif_read_line(s, tmp1, tmp2)) {
		case 0:
			break;
		case -1:
			goto error;
		default:
			abort();
		}
		if (tmp1->len == 0)
			break;

		values = g_ptr_array_new();
		if ( !(m = ldif_ldapmod4line(tmp1->str, tmp2->str)))
			goto error;
		g_ptr_array_add(mods, m);

		do {
			switch ( rc = ldif_read_line1(s, tmp1, tmp2)) {
			case 0:
				if (strcmp(tmp1->str, m->mod_type)) {
					fputs("Error: Attribute name mismatch"
					      " in change-modify.",
					      stderr);
					goto error;
				}
				g_ptr_array_add(values, gstring2berval(tmp2));
				break;
			case -2:
				break;
			case -1:
				goto error;
			default:
				abort();
			}
		} while (rc != -2);

		g_ptr_array_add(values, 0);
		m->mod_bvalues = (void *) values->pdata;
		g_ptr_array_free(values, 0);
		values = 0;
	}

	g_ptr_array_add(mods, 0);
	result = (LDAPMod **) mods->pdata;
	g_ptr_array_free(mods, 0);
	return result;

error:
	g_ptr_array_free(mods, 1);
	if (values) {
		int i;
		for (i = 0; i < values->len; i++)
			xfree_berval(values->pdata[i]);
		g_ptr_array_free(values, 0);
	}
	return 0;
}

/*
 * Read the first line of any record at position `offset' in `s'.
 * Set *pos (if pos != 0).
 * Return 0 on success, -1 otherwise.
 * On success:
 *   - pos is the exact starting position.
 *   - Set *key to the key (if key != 0).
 *   - Set *dn to the Distinguished Name (if dn != 0).
 * EOF is not an error and yields *key = 0 (if key != 0).
 *
 * The key is:
 *   "delete" for "changetype: delete"
 *   "modify" for "changetype: modify"
 *   "rename" for "changetype: moddn" and "changetype: modrdn"
 *   "add" for "changetype: add" (also accepted here)
 * otherwise the value of "ldapvi-key: ...", which must appear as
 * the first line in the attrval record.
 */
static int
ldif_read_header(GString *tmp1, GString *tmp2,
		 FILE *s, long offset, char **key, char **dn, long *pos)
{
	char **rdns = 0;
	char *k;
	char *d;
	long pos2;

	if (offset != -1)
		if (fseek(s, offset, SEEK_SET) == -1) syserr();
	do {
		if (pos)
			if ( (*pos = ftell(s)) == -1) syserr();
		if (ldif_read_line(s, tmp1, tmp2) == -1) return -1;
		if (tmp1->len == 0 && feof(s)) {
			if (key) *key = 0;
			return 0;
		}
		if (!strcmp(tmp1->str, "version")) {
			if (strcmp(tmp2->str, "1")) {
				fputs("Error: Invalid file format.\n", stderr);
				return -1;
			}
			tmp1->len = 0;
		}
	} while (!tmp1->len);

	rdns = ldap_explode_dn(tmp2->str, 0);
	if (!rdns) {
		fputs("Error: Invalid distinguished name string.\n", stderr);
		return -1;
	}
	if (dn)
		d = xdup(tmp2->str);

	if ( (pos2 = ftell(s)) == -1) syserr();

	if (ldif_read_line(s, tmp1, tmp2) == -1) {
		if (dn) free(d);
		return -1;
	}
	if (!strcmp(tmp1->str, "ldapvi-key"))
		k = tmp2->str;
	else if (!strcmp(tmp1->str, "changetype")) {
		if (!strcmp(tmp2->str, "modrdn"))
			k = "rename";
		else if (!strcmp(tmp2->str, "moddn"))
			k = "rename";
		else if (!strcmp(tmp2->str, "delete")
			 || !strcmp(tmp2->str, "modify")
			 || !strcmp(tmp2->str, "add"))
			k = tmp2->str;
		else {
			fputs("Error: invalid changetype.\n", stderr);
			if (dn) free(d);
			return -1;
		}
	} else if (!strcmp(tmp1->str, "control")) {
		fputs("Error: Sorry, 'control:' not supported.\n", stderr);
		if (dn) free(d);
		return -1;
	} else {
		k = "add";
		if (fseek(s, pos2, SEEK_SET) == -1) syserr();
	}

	if (key) *key = xdup(k);
	if (dn) *dn = d;
	ldap_value_free(rdns);
	return 0;
}

static int
ldif_read_attrval_body(GString *tmp1, GString *tmp2, FILE *s, tentry *entry)
{
	for (;;) {
		tattribute *attribute;

		if (ldif_read_line(s, tmp1, tmp2) == -1)
			return -1;
		if (!tmp1->len)
			break;
		attribute = entry_find_attribute(entry, tmp1->str, 1);
		attribute_append_value(attribute, tmp2->str, tmp2->len);
	}
	return 0;
}

/*
 * Read an attrval record at position `offset' in `s'.
 * Set *pos (if pos != 0).
 * Return 0 on success, -1 otherwise.
 * On success:
 *   - pos is the exact starting position.
 *   - Set *entry to the parsed entry (if entry != 0).
 *   - Set *key to the key (if key != 0).
 * EOF is not an error and yields *key = 0 (if key != 0).
 */
int
ldif_read_entry(FILE *s, long offset, char **key, tentry **entry, long *pos)
{
	GString *tmp1 = g_string_new("");
	GString *tmp2 = g_string_new("");
	char *dn;
	char *k = 0;
	tentry *e = 0;

	int rc = ldif_read_header(tmp1, tmp2, s, offset, &k, &dn, pos);
	if (rc || !k) goto cleanup;

	e = entry_new(dn);
	rc = ldif_read_attrval_body(tmp1, tmp2, s, e);
	if (!rc) {
		if (entry) {
			*entry = e;
			e = 0;
		}
		if (key) {
			*key = k;
			k = 0;
		}
	}

cleanup:
	if (k) free(k);
	if (e) entry_free(e);
	g_string_free(tmp1, 1);
	g_string_free(tmp2, 1);
	return rc;
}

/*
 * Read the first two lines of any record at position `offset' in `s'.
 *
 * Set *pos (if pos != 0).
 * Return 0 on success, -1 otherwise.
 * On success:
 *   - pos is the exact starting position.
 *   - Set *key to the key (if key != 0).
 */
int
ldif_peek_entry(FILE *s, long offset, char **key, long *pos)
{
	GString *tmp1 = g_string_new("");
	GString *tmp2 = g_string_new("");

	int rc = ldif_read_header(tmp1, tmp2, s, offset, key, 0, pos);
	g_string_free(tmp1, 1);
	g_string_free(tmp2, 1);
	return rc;
}

/*
 * Read a rename record at position `offset' in `s'.
 * Return 0 on success, -1 otherwise.
 * On success:
 *   - Set *dn1 to the old DN.
 *   - Set *dn2 to the new DN.
 *   - Set *deleteoldrdn to 1 or 0.
 */
int
ldif_read_rename(FILE *s, long offset, char **dn1, char **dn2,
		 int *deleteoldrdn)
{
	GString *tmp1 = g_string_new("");
	GString *tmp2 = g_string_new("");
	char *olddn;
	char *newdn;

	int rc = ldif_read_header(tmp1, tmp2, s, offset, 0, &olddn, 0);
	if (rc) {
		g_string_free(tmp1, 1);
		g_string_free(tmp2, 1);
		return rc;
	}

	newdn = ldif_read_rename_body(s, tmp1, tmp2, olddn, deleteoldrdn);
	g_string_free(tmp1, 1);
	g_string_free(tmp2, 1);

	if (!newdn) {
		free(olddn);
		return -1;
	}
	if (dn1) *dn1 = olddn; else free(olddn);
	if (dn2) *dn2 = newdn; else free(newdn);
	return 0;
}

int
ldif_read_delete(FILE *s, long offset, char **dn)
{
	GString *tmp1 = g_string_new("");
	GString *tmp2 = g_string_new("");
	char *str;

	int rc = ldif_read_header(tmp1, tmp2, s, offset, 0, &str, 0);
	if (rc) {
		g_string_free(tmp1, 1);
		g_string_free(tmp2, 1);
		return rc;
	}

	rc = ldif_read_nothing(s, tmp1, tmp2);
	g_string_free(tmp1, 1);
	g_string_free(tmp2, 1);

	if (rc == -1)
		free(str);
	else
		*dn = str;
	return rc;
}

/*
 * Read a modify record at position `offset' in `s'.
 * Return 0 on success, -1 otherwise.
 * On success:
 *   - Set *dn to the DN.
 *   - Set *mods to the modifications.
 */
int
ldif_read_modify(FILE *s, long offset, char **dn, LDAPMod ***mods)
{
	GString *tmp1 = g_string_new("");
	GString *tmp2 = g_string_new("");
	char *d;
	LDAPMod **m;

	int rc = ldif_read_header(tmp1, tmp2, s, offset, 0, &d, 0);
	if (rc) {
		g_string_free(tmp1, 1);
		g_string_free(tmp2, 1);
		return rc;
	}

	m = ldif_read_modify_body(s, tmp1, tmp2);
	g_string_free(tmp1, 1);
	g_string_free(tmp2, 1);

	if (!m) {
		free(d);
		return -1;
	}
	if (dn) *dn = d; else free(d);
	if (mods) *mods = m; else ldap_mods_free(m, 1);
	return 0;
}

/*
 * Parse a complete entry or changerecord and ignore it.  Set *key accordingly.
 * Leave the stream positioned after the entry.
 *
 * Treat EOF as success and set *key to NULL.
 *
 * return value:
 *   0 on success
 *   -1 on parse error
 */
int
ldif_skip_entry(FILE *s, long offset, char **key)
{
	GString *tmp1 = g_string_new("");
	GString *tmp2 = g_string_new("");
	char *k = 0;

	int rc = ldif_read_header(tmp1, tmp2, s, offset, &k, 0, 0);
	if (!rc && k)
		for (;;) {
			if (ldif_read_line1(s, tmp1, tmp2) == -1) {
				rc = -1;
				break;
			}
			if (tmp1->len == 0) {
				if (key) *key = k; else free(k);
				break;
			}
		}
	g_string_free(tmp1, 1);
	g_string_free(tmp2, 1);
	return rc;
}

tparser ldif_parser = {
	ldif_read_entry,
	ldif_peek_entry,
	ldif_skip_entry,
	ldif_read_rename,
	ldif_read_delete,
	ldif_read_modify,
	print_ldif_entry
};
