/* 
   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.h"
#include "tools/cmdline.h"
#include "dsdb/samdb/samdb.h"
#include "../lib/crypto/sha256.h"
#include "../librpc/gen_ndr/ndr_misc.h"
#include "lib/cmdline/popt_common.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--;
			if (*(c - 1) != ' ' && c && (*(c + 1) == '\0')) {
				return 2;
			}
		}
		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;

	SHA256_CTX sha256_context;
	uint8_t digest[SHA256_DIGEST_LENGTH];

	struct GUID guid;

	bool isAttribute = false;
	bool single_valued = false;

	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");
			isAttribute = true;
			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);
	if (isAttribute) {
		MSG_ADD_STRING("attributeID", s);
	} else {
		MSG_ADD_STRING("governsID", s);
	}

	samba_SHA256_Init(&sha256_context);
	samba_SHA256_Update(&sha256_context, (uint8_t*)s, strlen(s));
	samba_SHA256_Final(digest, &sha256_context);

	memcpy(&guid, digest, sizeof(struct GUID));

	if (dsdb_msg_add_guid(msg, &guid, "schemaIdGuid") != 0) {
		goto failed;
	}

	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_copy(msg, basedn);
			ldb_dn_add_child_fmt(msg->dn, "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:
        		single_valued = true;
			break;

		case SCHEMA_EQUALITY:
			/* TODO */
			break;

		case SCHEMA_ORDERING:
			/* TODO */
			break;

		case SCHEMA_SUBSTR:
			/* TODO */
			break;

		case SCHEMA_SYNTAX:
		{
			char *syntax_oid;
			const struct dsdb_syntax *map;
			char *oMSyntax;

			n = strcspn(token->value, "{");
			syntax_oid = talloc_strndup(ctx, token->value, n);

			map = find_syntax_map_by_standard_oid(syntax_oid);
			if (!map) {
				break;
			}

			MSG_ADD_STRING("attributeSyntax", map->attributeSyntax_oid);

			oMSyntax = talloc_asprintf(msg, "%d", map->oMSyntax);
			MSG_ADD_STRING("oMSyntax", oMSyntax);

			break;
		}
		case SCHEMA_DESC:
			MSG_ADD_STRING("description", token->value);
			break;

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

	if (isAttribute) {
		MSG_ADD_STRING("isSingleValued", single_valued ? "TRUE" : "FALSE");
	} else {
		MSG_ADD_STRING("defaultObjectCategory", ldb_dn_get_linearized(msg->dn));
	}

	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') {
				int ret2 = 0;
				entry[t] = '\0';
				ret2 = check_braces(entry);
				if (ret2 == 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;
				}
				if (ret2 == 2) {
					fprintf(stderr, "Invalid entry %s, closing braces needs to be preceeded by a space\n", entry);
					ret.failures++;
					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 struct options {
	const char *basedn;
	const char *input;
	const char *output;
} options;

static struct poptOption popt_options[] = {
	POPT_AUTOHELP
	{ "basedn",    'b', POPT_ARG_STRING, &options.basedn, 0, "base DN", "DN" },
	{ "input", 'I', POPT_ARG_STRING, &options.input, 0, 
	  "inputfile of OpenLDAP style schema otherwise STDIN", "inputfile"},
	{ "output", 'O', POPT_ARG_STRING, &options.output, 0, 
	  "outputfile otherwise STDOUT", "outputfile"},
	POPT_COMMON_VERSION
	{ NULL }
};


static void usage(void)
{
	poptContext pc;
	printf("Usage: oLschema2ldif <options>\n");
	printf("\nConvert OpenLDAP schema to AD-like LDIF format\n\n");
	printf("Converts records from an openLdap formatted schema to an ldif schema\n\n");
	pc = poptGetContext("oLschema2ldif", 0, NULL, popt_options, 
			    POPT_CONTEXT_KEEP_FIRST);
	poptPrintHelp(pc, stdout, 0);
	exit(1);
}


 int main(int argc, const char **argv)
{
	TALLOC_CTX *ctx;
	struct schema_conv ret;
	FILE *in = stdin;
	FILE *out = stdout;
	poptContext pc;
	int opt;

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

	setenv("LDB_URL", "NONE", 1);

	pc = poptGetContext(argv[0], argc, argv, popt_options, 
			    POPT_CONTEXT_KEEP_FIRST);

	while((opt = poptGetNextOpt(pc)) != -1) {
		fprintf(stderr, "Invalid option %s: %s\n", 
			poptBadOption(pc, 0), poptStrerror(opt));
		usage();
	}

	if (options.basedn == NULL) {		
		printf("Base DN not specified\n");
		usage();
		exit(1);
	} else {
		basedn = ldb_dn_new(ctx, ldb_ctx, options.basedn);
		if ( ! ldb_dn_validate(basedn)) {
			printf("Malformed Base DN\n");
			usage();
			exit(1);
		}
	}

	if (options.input) {
		in = fopen(options.input, "r");
		if (!in) {
			perror(options.input);
			usage();
			exit(1);
		}
	}
	if (options.output) {
		out = fopen(options.output, "w");
		if (!out) {
			perror(options.output);
			usage();
			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;
}
