/* 
   ldb database library

   Copyright (C) Simo Sorce 2005

     ** NOTE! The following LGPL license applies to the ldb
     ** library. This does NOT imply that all of Samba is released
     ** under the LGPL
   
   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 3 of the License, or (at your option) any later version.

   This library 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/

/*
 *  Name: ldb
 *
 *  Component: oLschema2ldif
 *
 *  Description: utility to convert an OpenLDAP schema into AD LDIF
 *
 *  Author: Simo Sorce
 */

#include "includes.h"
#include "ldb/include/includes.h"
#include "ldb/tools/cmdline.h"
#include "ldb/tools/convert.h"

#define SCHEMA_UNKNOWN 0
#define SCHEMA_NAME 1
#define SCHEMA_SUP 2
#define SCHEMA_STRUCTURAL 3
#define SCHEMA_ABSTRACT 4
#define SCHEMA_AUXILIARY 5
#define SCHEMA_MUST 6
#define SCHEMA_MAY 7
#define SCHEMA_SINGLE_VALUE 8
#define SCHEMA_EQUALITY 9
#define SCHEMA_ORDERING 10
#define SCHEMA_SUBSTR 11
#define SCHEMA_SYNTAX 12
#define SCHEMA_DESC 13

struct schema_conv {
	int count;
	int failures;
};

struct schema_token {
	int type;
	char *value;
};

struct ldb_context *ldb_ctx;
struct ldb_dn *basedn;

static int check_braces(const char *string)
{
	int b;
	char *c;

	b = 0;
	if ((c = strchr(string, '(')) == NULL) {
		return -1;
	}
	b++;
	c++;
	while (b) {
		c = strpbrk(c, "()");
		if (c == NULL) return 1;
		if (*c == '(') b++;
		if (*c == ')') b--;
		c++;
	}
	return 0;
}

static char *skip_spaces(char *string) {
	return (string + strspn(string, " \t\n"));
}

static int add_multi_string(struct ldb_message *msg, const char *attr, char *values)
{
	char *c;
	char *s;
	int n;

	c = skip_spaces(values);
	while (*c) {
		n = strcspn(c, " \t$");
		s = talloc_strndup(msg, c, n);
		if (ldb_msg_add_string(msg, attr, s) != 0) {
			return -1;
		}
		c += n;
		c += strspn(c, " \t$");
	}

	return 0;
}

#define MSG_ADD_STRING(a, v) do { if (ldb_msg_add_string(msg, a, v) != 0) goto failed; } while(0)
#define MSG_ADD_M_STRING(a, v) do { if (add_multi_string(msg, a, v) != 0) goto failed; } while(0)

static char *get_def_value(TALLOC_CTX *ctx, char **string)
{
	char *c = *string;
	char *value;
	int n;

	if (*c == '\'') {
		c++;
		n = strcspn(c, "\'");
		value = talloc_strndup(ctx, c, n);
		c += n;
		c++; /* skip closing \' */
	} else {
		n = strcspn(c, " \t\n");
		value = talloc_strndup(ctx, c, n);
		c += n;
	}
	*string = c;

	return value;
}

static struct schema_token *get_next_schema_token(TALLOC_CTX *ctx, char **string)
{
	char *c = skip_spaces(*string);
	char *type;
	struct schema_token *token;
	int n;

	token = talloc(ctx, struct schema_token);

	n = strcspn(c, " \t\n");
	type = talloc_strndup(token, c, n);
	c += n;
	c = skip_spaces(c);

	if (strcasecmp("NAME", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_NAME;
		/* we do not support aliases so we get only the first name given and skip others */
		if (*c == '(') {
			char *s = strchr(c, ')');
			if (s == NULL) return NULL;
			s = skip_spaces(s);
			*string = s;

			c++;
			c = skip_spaces(c);
		}

		token->value = get_def_value(ctx, &c);

		if (*string < c) { /* single name */
			c = skip_spaces(c);
			*string = c;
		}
		return token;
	}
	if (strcasecmp("SUP", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_SUP;

		if (*c == '(') {
			c++;
			n = strcspn(c, ")");
			token->value = talloc_strndup(ctx, c, n);
			c += n;
			c++;
		} else {
			token->value = get_def_value(ctx, &c);
		}

		c = skip_spaces(c);
		*string = c;
		return token;
	}

	if (strcasecmp("STRUCTURAL", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_STRUCTURAL;
		*string = c;
		return token;
	}

	if (strcasecmp("ABSTRACT", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_ABSTRACT;
		*string = c;
		return token;
	}

	if (strcasecmp("AUXILIARY", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_AUXILIARY;
		*string = c;
		return token;
	}

	if (strcasecmp("MUST", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_MUST;

		if (*c == '(') {
			c++;
			n = strcspn(c, ")");
			token->value = talloc_strndup(ctx, c, n);
			c += n;
			c++;
		} else {
			token->value = get_def_value(ctx, &c);
		}

		c = skip_spaces(c);
		*string = c;
		return token;
	}

	if (strcasecmp("MAY", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_MAY;

		if (*c == '(') {
			c++;
			n = strcspn(c, ")");
			token->value = talloc_strndup(ctx, c, n);
			c += n;
			c++;
		} else {
			token->value = get_def_value(ctx, &c);
		}

		c = skip_spaces(c);
		*string = c;
		return token;
	}

	if (strcasecmp("SINGLE-VALUE", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_SINGLE_VALUE;
		*string = c;
		return token;
	}

	if (strcasecmp("EQUALITY", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_EQUALITY;

		token->value = get_def_value(ctx, &c);

		c = skip_spaces(c);
		*string = c;
		return token;
	}

	if (strcasecmp("ORDERING", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_ORDERING;

		token->value = get_def_value(ctx, &c);

		c = skip_spaces(c);
		*string = c;
		return token;
	}

	if (strcasecmp("SUBSTR", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_SUBSTR;

		token->value = get_def_value(ctx, &c);

		c = skip_spaces(c);
		*string = c;
		return token;
	}

	if (strcasecmp("SYNTAX", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_SYNTAX;

		token->value = get_def_value(ctx, &c);

		c = skip_spaces(c);
		*string = c;
		return token;
	}

	if (strcasecmp("DESC", type) == 0) {
		talloc_free(type);
		token->type = SCHEMA_DESC;

		token->value = get_def_value(ctx, &c);

		c = skip_spaces(c);
		*string = c;
		return token;
	}

	token->type = SCHEMA_UNKNOWN;
	token->value = type;
	if (*c == ')') {
		*string = c;
		return token;
	}
	if (*c == '\'') {
		c = strchr(++c, '\'');
		c++;
	} else {
		c += strcspn(c, " \t\n");
	}
	c = skip_spaces(c);
	*string = c;

	return token;
}

static struct ldb_message *process_entry(TALLOC_CTX *mem_ctx, const char *entry)
{
	TALLOC_CTX *ctx;
	struct ldb_message *msg;
	struct schema_token *token;
        char *c, *s;
        int n;

	ctx = talloc_new(mem_ctx);
	msg = ldb_msg_new(ctx);

	ldb_msg_add_string(msg, "objectClass", "top");

	c = talloc_strdup(ctx, entry);
	if (!c) return NULL;

	c = skip_spaces(c);

	switch (*c) {
	case 'a':
		if (strncmp(c, "attributetype", 13) == 0) {
			c += 13;
			MSG_ADD_STRING("objectClass", "attributeSchema");
			break;
		}
		goto failed;
	case 'o':
		if (strncmp(c, "objectclass", 11) == 0) {
			c += 11;
			MSG_ADD_STRING("objectClass", "classSchema");
			break;
		}
		goto failed;
	default:
		goto failed;
	}

	c = strchr(c, '(');
	if (c == NULL) goto failed;
	c++;

	c = skip_spaces(c);

	/* get attributeID */
	n = strcspn(c, " \t");
	s = talloc_strndup(msg, c, n);
	MSG_ADD_STRING("attributeID", s);
	c += n;
	c = skip_spaces(c);	

	while (*c != ')') {
		token = get_next_schema_token(msg, &c);
		if (!token) goto failed;

		switch (token->type) {
		case SCHEMA_NAME:
			MSG_ADD_STRING("cn", token->value);
			MSG_ADD_STRING("name", token->value);
			MSG_ADD_STRING("lDAPDisplayName", token->value);
			msg->dn = ldb_dn_string_compose(msg, basedn,
							"CN=%s,CN=Schema,CN=Configuration",
							token->value);
			break;

		case SCHEMA_SUP:
			MSG_ADD_M_STRING("subClassOf", token->value);
			break;

		case SCHEMA_STRUCTURAL:
			MSG_ADD_STRING("objectClassCategory", "1");
			break;

		case SCHEMA_ABSTRACT:
			MSG_ADD_STRING("objectClassCategory", "2");
			break;

		case SCHEMA_AUXILIARY:
			MSG_ADD_STRING("objectClassCategory", "3");
			break;

		case SCHEMA_MUST:
			MSG_ADD_M_STRING("mustContain", token->value);
			break;

		case SCHEMA_MAY:
			MSG_ADD_M_STRING("mayContain", token->value);
			break;

		case SCHEMA_SINGLE_VALUE:
			MSG_ADD_STRING("isSingleValued", "TRUE");
			break;

		case SCHEMA_EQUALITY:
			/* TODO */
			break;

		case SCHEMA_ORDERING:
			/* TODO */
			break;

		case SCHEMA_SUBSTR:
			/* TODO */
			break;

		case SCHEMA_SYNTAX:
		{
			const struct syntax_map *map = 
				find_syntax_map_by_standard_oid(token->value);
			if (!map) {
				break;
			}
			MSG_ADD_STRING("attributeSyntax", map->AD_OID);
			break;
		}
		case SCHEMA_DESC:
			MSG_ADD_STRING("description", token->value);
			break;

		default:
			fprintf(stderr, "Unknown Definition: %s\n", token->value);
		}
	}

	talloc_steal(mem_ctx, msg);
	talloc_free(ctx);
	return msg;

failed:
	talloc_free(ctx);
	return NULL;
}

static struct schema_conv process_file(FILE *in, FILE *out)
{
	TALLOC_CTX *ctx;
	struct schema_conv ret;
	char *entry;
	int c, t, line;
	struct ldb_ldif ldif;

	ldif.changetype = LDB_CHANGETYPE_NONE;

	ctx = talloc_new(NULL);

	ret.count = 0;
	ret.failures = 0;
	line = 0;

	while ((c = fgetc(in)) != EOF) {
		line++;
		/* fprintf(stderr, "Parsing line %d\n", line); */
		if (c == '#') {
			do {
				c = fgetc(in);
			} while (c != EOF && c != '\n');
			continue;
		}
		if (c == '\n') {
			continue;
		}

		t = 0;
		entry = talloc_array(ctx, char, 1024);
		if (entry == NULL) exit(-1);

		do { 
			if (c == '\n') {
				entry[t] = '\0';	
				if (check_braces(entry) == 0) {
					ret.count++;
					ldif.msg = process_entry(ctx, entry);
					if (ldif.msg == NULL) {
						ret.failures++;
						fprintf(stderr, "No valid msg from entry \n[%s]\n at line %d\n", entry, line);
						break;
					}
					ldb_ldif_write_file(ldb_ctx, out, &ldif);
					break;
				}
				line++;
			} else {
				entry[t] = c;
				t++;
			}
			if ((t % 1023) == 0) {
				entry = talloc_realloc(ctx, entry, char, t + 1024);
				if (entry == NULL) exit(-1);
			}
		} while ((c = fgetc(in)) != EOF); 

		if (c != '\n') {
			entry[t] = '\0';
			if (check_braces(entry) == 0) {
				ret.count++;
				ldif.msg = process_entry(ctx, entry);
				if (ldif.msg == NULL) {
					ret.failures++;
					fprintf(stderr, "No valid msg from entry \n[%s]\n at line %d\n", entry, line);
					break;
				}
				ldb_ldif_write_file(ldb_ctx, out, &ldif);
			} else {
				fprintf(stderr, "malformed entry on line %d\n", line);
				ret.failures++;
			}
		}
	
		if (c == EOF) break;
	}

	return ret;
}

static void usage(void)
{
	printf("Usage: oLschema2ldif -H NONE <options>\n");
	printf("\nConvert OpenLDAP schema to AD-like LDIF format\n\n");
	printf("Options:\n");
	printf("  -I inputfile     inputfile of OpenLDAP style schema otherwise STDIN\n");
	printf("  -O outputfile    outputfile otherwise STDOUT\n");
	printf("  -o options       pass options like modules to activate\n");
	printf("              e.g: -o modules:timestamps\n");
	printf("\n");
	printf("Converts records from an openLdap formatted schema to an ldif schema\n\n");
	exit(1);
}

 int main(int argc, const char **argv)
{
	TALLOC_CTX *ctx;
	struct schema_conv ret;
	struct ldb_cmdline *options;
	FILE *in = stdin;
	FILE *out = stdout;
	ldb_global_init();

	ctx = talloc_new(NULL);
	ldb_ctx = ldb_init(ctx);

	setenv("LDB_URL", "NONE", 1);
	options = ldb_cmdline_process(ldb_ctx, argc, argv, usage);

	if (options->basedn == NULL) {
		perror("Base DN not specified");
		exit(1);
	} else {
		basedn = ldb_dn_explode(ctx, options->basedn);
		if (basedn == NULL) {
			perror("Malformed Base DN");
			exit(1);
		}
	}

	if (options->input) {
		in = fopen(options->input, "r");
		if (!in) {
			perror(options->input);
			exit(1);
		}
	}
	if (options->output) {
		out = fopen(options->output, "w");
		if (!out) {
			perror(options->output);
			exit(1);
		}
	}

	ret = process_file(in, out);

	fclose(in);
	fclose(out);

	printf("Converted %d records with %d failures\n", ret.count, ret.failures);

	return 0;
}
