/* variant.c - variant overlay */
/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
 *
 * Copyright 2016-2021 Symas Corporation.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted only as authorized by the OpenLDAP
 * Public License.
 *
 * A copy of this license is available in the file LICENSE in the
 * top-level directory of the distribution or, alternatively, at
 * <http://www.OpenLDAP.org/license.html>.
 */
/* ACKNOWLEDGEMENTS:
 * This work was developed in 2016-2017 by Ondřej Kuzník for Symas Corp.
 */

#include "portable.h"

#ifdef SLAPD_OVER_VARIANT

#include "slap.h"
#include "slap-config.h"
#include "ldap_queue.h"

typedef enum variant_type_t {
	VARIANT_INFO_PLAIN = 1 << 0,
	VARIANT_INFO_REGEX = 1 << 1,

	VARIANT_INFO_ALL = ~0
} variant_type_t;

typedef struct variant_info_t {
	int passReplication;
	LDAP_STAILQ_HEAD(variant_list, variantEntry_info) variants, regex_variants;
} variant_info_t;

typedef struct variantEntry_info {
	variant_info_t *ov;
	struct berval dn;
	variant_type_t type;
	regex_t *regex;
	LDAP_SLIST_HEAD(attribute_list, variantAttr_info) attributes;
	LDAP_STAILQ_ENTRY(variantEntry_info) next;
} variantEntry_info;

typedef struct variantAttr_info {
	variantEntry_info *variant;
	struct berval dn;
	AttributeDescription *attr, *alternative;
	LDAP_SLIST_ENTRY(variantAttr_info) next;
} variantAttr_info;

static int
variant_build_dn(
		Operation *op,
		variantAttr_info *vai,
		int nmatch,
		regmatch_t *pmatch,
		struct berval *out )
{
	struct berval dn, *ndn = &op->o_req_ndn;
	char *dest, *p, *prev, *end = vai->dn.bv_val + vai->dn.bv_len;
	size_t len = vai->dn.bv_len;
	int rc;

	p = vai->dn.bv_val;
	while ( (p = memchr( p, '$', end - p )) != NULL ) {
		len -= 1;
		p += 1;

		if ( ( *p >= '0' ) && ( *p <= '9' ) ) {
			int i = *p - '0';

			len += ( pmatch[i].rm_eo - pmatch[i].rm_so );
		} else if ( *p != '$' ) {
			/* Should have been checked at configuration time */
			assert(0);
		}
		len -= 1;
		p += 1;
	}

	dest = dn.bv_val = ch_realloc( out->bv_val, len + 1 );
	dn.bv_len = len;

	prev = vai->dn.bv_val;
	while ( (p = memchr( prev, '$', end - prev )) != NULL ) {
		len = p - prev;
		AC_MEMCPY( dest, prev, len );
		dest += len;
		p += 1;

		if ( ( *p >= '0' ) && ( *p <= '9' ) ) {
			int i = *p - '0';
			len = pmatch[i].rm_eo - pmatch[i].rm_so;

			AC_MEMCPY( dest, ndn->bv_val + pmatch[i].rm_so, len );
			dest += len;
		} else if ( *p == '$' ) {
			*dest++ = *p;
		}
		prev = p + 1;
	}
	len = end - prev;
	AC_MEMCPY( dest, prev, len );
	dest += len;
	*dest = '\0';

	rc = dnNormalize( 0, NULL, NULL, &dn, out, NULL );
	ch_free( dn.bv_val );

	return rc;
}

static int
variant_build_entry(
		Operation *op,
		variantEntry_info *vei,
		struct berval *dn,
		Entry **ep,
		int nmatch,
		regmatch_t *pmatch )
{
	slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
	BackendDB *be_orig = op->o_bd, *db;
	struct berval ndn = BER_BVNULL;
	variantAttr_info *vai;
	Attribute *a;
	BerVarray nvals;
	Entry *e;
	unsigned int i;
	int rc;

	assert( ep );
	assert( !*ep );

	rc = overlay_entry_get_ov( op, dn, NULL, NULL, 0, &e, on );
	if ( rc == LDAP_SUCCESS && is_entry_referral( e ) ) {
		overlay_entry_release_ov( op, e, 0, on );
		rc = LDAP_REFERRAL;
	}

	if ( rc != LDAP_SUCCESS ) {
		goto done;
	}

	*ep = entry_dup( e );
	overlay_entry_release_ov( op, e, 0, on );

	LDAP_SLIST_FOREACH( vai, &vei->attributes, next ) {
		if ( vei->type == VARIANT_INFO_REGEX ) {
			rc = variant_build_dn( op, vai, nmatch, pmatch, &ndn );
			if ( rc != LDAP_SUCCESS ) {
				goto done;
			}
		} else {
			ndn = vai->dn;
		}

		(void)attr_delete( &(*ep)->e_attrs, vai->attr );
		op->o_bd = be_orig;

		/* only select backend if not served by ours, would retrace all
		 * overlays again */
		db = select_backend( &ndn, 0 );
		if ( db && db != be_orig->bd_self ) {
			op->o_bd = db;
			rc = be_entry_get_rw( op, &ndn, NULL, vai->alternative, 0, &e );
		} else {
			rc = overlay_entry_get_ov(
					op, &ndn, NULL, vai->alternative, 0, &e, on );
		}

		switch ( rc ) {
			case LDAP_SUCCESS:
				break;
			case LDAP_INSUFFICIENT_ACCESS:
			case LDAP_NO_SUCH_ATTRIBUTE:
			case LDAP_NO_SUCH_OBJECT:
				rc = LDAP_SUCCESS;
				continue;
				break;
			default:
				goto done;
				break;
		}

		a = attr_find( e->e_attrs, vai->alternative );

		/* back-ldif doesn't check the attribute exists in the entry before
		 * returning it */
		if ( a ) {
			if ( a->a_nvals ) {
				nvals = a->a_nvals;
			} else {
				nvals = a->a_vals;
			}

			for ( i = 0; i < a->a_numvals; i++ ) {
				if ( backend_access( op, e, &ndn, vai->alternative, &nvals[i],
							ACL_READ, NULL ) != LDAP_SUCCESS ) {
					continue;
				}

				rc = attr_merge_one( *ep, vai->attr, &a->a_vals[i], &nvals[i] );
				if ( rc != LDAP_SUCCESS ) {
					break;
				}
			}
		}

		if ( db && db != be_orig->bd_self ) {
			be_entry_release_rw( op, e, 0 );
		} else {
			overlay_entry_release_ov( op, e, 0, on );
		}
		if ( rc != LDAP_SUCCESS ) {
			goto done;
		}
	}

done:
	op->o_bd = be_orig;
	if ( rc != LDAP_SUCCESS && *ep ) {
		entry_free( *ep );
		*ep = NULL;
	}
	if ( vei->type == VARIANT_INFO_REGEX ) {
		ch_free( ndn.bv_val );
	}

	return rc;
}

static int
variant_find_config(
		Operation *op,
		variant_info_t *ov,
		struct berval *ndn,
		int which,
		variantEntry_info **veip,
		size_t nmatch,
		regmatch_t *pmatch )
{
	variantEntry_info *vei;

	assert( veip );

	if ( which & VARIANT_INFO_PLAIN ) {
		int diff;

		LDAP_STAILQ_FOREACH( vei, &ov->variants, next ) {
			dnMatch( &diff, 0, NULL, NULL, ndn, &vei->dn );
			if ( diff ) continue;

			*veip = vei;
			return LDAP_SUCCESS;
		}
	}

	if ( which & VARIANT_INFO_REGEX ) {
		LDAP_STAILQ_FOREACH( vei, &ov->regex_variants, next ) {
			if ( regexec( vei->regex, ndn->bv_val, nmatch, pmatch, 0 ) ) {
				continue;
			}

			*veip = vei;
			return LDAP_SUCCESS;
		}
	}

	return SLAP_CB_CONTINUE;
}

static int
variant_op_add( Operation *op, SlapReply *rs )
{
	slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
	variant_info_t *ov = on->on_bi.bi_private;
	variantEntry_info *vei;
	int rc;

	/* Replication always uses the rootdn */
	if ( ov->passReplication && SLAPD_SYNC_IS_SYNCCONN(op->o_connid) &&
			be_isroot( op ) ) {
		return SLAP_CB_CONTINUE;
	}

	Debug( LDAP_DEBUG_TRACE, "variant_op_add: "
			"dn=%s\n", op->o_req_ndn.bv_val );

	rc = variant_find_config(
			op, ov, &op->o_req_ndn, VARIANT_INFO_ALL, &vei, 0, NULL );
	if ( rc == LDAP_SUCCESS ) {
		variantAttr_info *vai;

		LDAP_SLIST_FOREACH( vai, &vei->attributes, next ) {
			Attribute *a;
			for ( a = op->ora_e->e_attrs; a; a = a->a_next ) {
				if ( a->a_desc == vai->attr ) {
					rc = LDAP_CONSTRAINT_VIOLATION;
					send_ldap_error( op, rs, rc,
							"variant: trying to add variant attributes" );
					goto done;
				}
			}
		}
	}
	rc = SLAP_CB_CONTINUE;

done:
	Debug( LDAP_DEBUG_TRACE, "variant_op_add: "
			"finished with %d\n",
			rc );
	return rc;
}

static int
variant_op_compare( Operation *op, SlapReply *rs )
{
	slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
	variant_info_t *ov = on->on_bi.bi_private;
	variantEntry_info *vei;
	regmatch_t pmatch[10];
	int rc, nmatch = sizeof(pmatch) / sizeof(regmatch_t);

	Debug( LDAP_DEBUG_TRACE, "variant_op_compare: "
			"dn=%s\n", op->o_req_ndn.bv_val );

	rc = variant_find_config(
			op, ov, &op->o_req_ndn, VARIANT_INFO_ALL, &vei, nmatch, pmatch );
	if ( rc == LDAP_SUCCESS ) {
		Entry *e = NULL;

		rc = variant_build_entry( op, vei, &op->o_req_ndn, &e, nmatch, pmatch );
		/* in case of error, just let the backend deal with the mod and the
		 * client should get a meaningful error back */
		if ( rc != LDAP_SUCCESS ) {
			rc = SLAP_CB_CONTINUE;
		} else {
			rc = slap_compare_entry( op, e, op->orc_ava );

			entry_free( e );
			e = NULL;
		}
	}

	if ( rc != SLAP_CB_CONTINUE ) {
		rs->sr_err = rc;
		send_ldap_result( op, rs );
	}

	Debug( LDAP_DEBUG_TRACE, "variant_op_compare: "
			"finished with %d\n", rc );
	return rc;
}

static int
variant_cmp_op( const void *l, const void *r )
{
	const Operation *left = l, *right = r;
	int diff;

	dnMatch( &diff, 0, NULL, NULL, (struct berval *)&left->o_req_ndn,
			(void *)&right->o_req_ndn );

	return diff;
}

static int
variant_run_mod( void *nop, void *arg )
{
	SlapReply nrs = { REP_RESULT };
	slap_callback cb = { 0 };
	Operation *op = nop;
	slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
	int *rc = arg;

	cb.sc_response = slap_null_cb;
	op->o_callback = &cb;

	Debug( LDAP_DEBUG_TRACE, "variant_run_mod: "
			"running mod on dn=%s\n",
			op->o_req_ndn.bv_val );
	*rc = on->on_info->oi_orig->bi_op_modify( op, &nrs );
	Debug( LDAP_DEBUG_TRACE, "variant_run_mod: "
			"finished with %d\n", *rc );

	return ( *rc != LDAP_SUCCESS );
}

/** Move the Modifications back to the original Op so that they can be disposed
 * of by the original creator
 */
static int
variant_reassign_mods( void *nop, void *arg )
{
	Operation *op = nop, *orig_op = arg;
	Modifications *mod;

	assert( op->orm_modlist );

	for ( mod = op->orm_modlist; mod->sml_next; mod = mod->sml_next )
		/* get the tail mod */;

	mod->sml_next = orig_op->orm_modlist;
	orig_op->orm_modlist = op->orm_modlist;

	return LDAP_SUCCESS;
}

void
variant_free_op( void *op )
{
	ch_free( ((Operation *)op)->o_req_ndn.bv_val );
	ch_free( op );
}

static int
variant_op_mod( Operation *op, SlapReply *rs )
{
	slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
	variant_info_t *ov = on->on_bi.bi_private;
	variantEntry_info *vei;
	variantAttr_info *vai;
	Avlnode *ops = NULL;
	Entry *e = NULL;
	Modifications *mod, *nextmod;
	regmatch_t pmatch[10];
	int rc, nmatch = sizeof(pmatch) / sizeof(regmatch_t);

	/* Replication always uses the rootdn */
	if ( ov->passReplication && SLAPD_SYNC_IS_SYNCCONN(op->o_connid) &&
			be_isroot( op ) ) {
		return SLAP_CB_CONTINUE;
	}

	Debug( LDAP_DEBUG_TRACE, "variant_op_mod: "
			"dn=%s\n", op->o_req_ndn.bv_val );

	rc = variant_find_config(
			op, ov, &op->o_req_ndn, VARIANT_INFO_ALL, &vei, nmatch, pmatch );
	if ( rc != LDAP_SUCCESS ) {
		Debug( LDAP_DEBUG_TRACE, "variant_op_mod: "
				"not a variant\n" );
		rc = SLAP_CB_CONTINUE;
		goto done;
	}

	rc = variant_build_entry( op, vei, &op->o_req_ndn, &e, nmatch, pmatch );
	/* in case of error, just let the backend deal with the mod and the client
	 * should get a meaningful error back */
	if ( rc != LDAP_SUCCESS ) {
		Debug( LDAP_DEBUG_TRACE, "variant_op_mod: "
				"failed to retrieve entry\n" );
		rc = SLAP_CB_CONTINUE;
		goto done;
	}

	rc = acl_check_modlist( op, e, op->orm_modlist );
	entry_free( e );

	if ( !rc ) {
		rc = rs->sr_err = LDAP_INSUFFICIENT_ACCESS;
		send_ldap_error( op, rs, rc, "" );
		return rc;
	}

	for ( mod = op->orm_modlist; mod; mod = nextmod ) {
		Operation needle = { .o_req_ndn = BER_BVNULL }, *nop;

		nextmod = mod->sml_next;

		LDAP_SLIST_FOREACH( vai, &vei->attributes, next ) {
			if ( vai->attr == mod->sml_desc ) {
				break;
			}
		}

		if ( vai ) {
			if ( vei->type == VARIANT_INFO_REGEX ) {
				rc = variant_build_dn(
						op, vai, nmatch, pmatch, &needle.o_req_ndn );
				if ( rc != LDAP_SUCCESS ) {
					continue;
				}
			} else {
				needle.o_req_ndn = vai->dn;
			}

			nop = ldap_avl_find( ops, &needle, variant_cmp_op );
			if ( nop == NULL ) {
				nop = ch_calloc( 1, sizeof(Operation) );
				*nop = *op;

				ber_dupbv( &nop->o_req_ndn, &needle.o_req_ndn );
				nop->o_req_dn = nop->o_req_ndn;
				nop->orm_modlist = NULL;

				rc = ldap_avl_insert( &ops, nop, variant_cmp_op, ldap_avl_dup_error );
				assert( rc == 0 );
			}
			mod->sml_desc = vai->alternative;

			op->orm_modlist = nextmod;
			mod->sml_next = nop->orm_modlist;
			nop->orm_modlist = mod;

			if ( vei->type == VARIANT_INFO_REGEX ) {
				ch_free( needle.o_req_ndn.bv_val );
			}
		}
	}

	if ( !ops ) {
		Debug( LDAP_DEBUG_TRACE, "variant_op_mod: "
				"no variant attributes in mod\n" );
		return SLAP_CB_CONTINUE;
	}

	/*
	 * First run original Operation
	 * This will take care of making sure the entry exists as well.
	 *
	 * FIXME?
	 * Since we cannot make the subsequent Ops atomic wrt. this one, we just
	 * let it send the response as well. After all, the changes on the main DN
	 * have finished by then
	 */
	rc = on->on_info->oi_orig->bi_op_modify( op, rs );
	if ( rc == LDAP_SUCCESS ) {
		/* FIXME: if a mod fails, should we attempt to apply the rest? */
		ldap_avl_apply( ops, variant_run_mod, &rc, -1, AVL_INORDER );
	}

	ldap_avl_apply( ops, variant_reassign_mods, op, -1, AVL_INORDER );
	ldap_avl_free( ops, variant_free_op );

done:
	Debug( LDAP_DEBUG_TRACE, "variant_op_mod: "
			"finished with %d\n", rc );
	return rc;
}

static int
variant_search_response( Operation *op, SlapReply *rs )
{
	slap_overinst *on = op->o_callback->sc_private;
	variant_info_t *ov = on->on_bi.bi_private;
	variantEntry_info *vei;
	int rc;

	if ( rs->sr_type == REP_RESULT ) {
		ch_free( op->o_callback );
		op->o_callback = NULL;
	}

	if ( rs->sr_type != REP_SEARCH ) {
		return SLAP_CB_CONTINUE;
	}

	rc = variant_find_config(
			op, ov, &rs->sr_entry->e_nname, VARIANT_INFO_PLAIN, &vei, 0, NULL );
	if ( rc == LDAP_SUCCESS ) {
		rs->sr_nentries--;
		return rc;
	}

	return SLAP_CB_CONTINUE;
}

static int
variant_op_search( Operation *op, SlapReply *rs )
{
	slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
	variant_info_t *ov = on->on_bi.bi_private;
	variantEntry_info *vei;
	slap_callback *cb;
	Entry *e = NULL;
	regmatch_t pmatch[10];
	int variantInScope = 0, rc = SLAP_CB_CONTINUE,
		nmatch = sizeof(pmatch) / sizeof(regmatch_t);

	if ( ov->passReplication && ( op->o_sync > SLAP_CONTROL_IGNORED ) ) {
		return SLAP_CB_CONTINUE;
	}

	Debug( LDAP_DEBUG_TRACE, "variant_op_search: "
			"dn=%s, scope=%d\n",
			op->o_req_ndn.bv_val, op->ors_scope );

	LDAP_STAILQ_FOREACH( vei, &ov->variants, next ) {
		if ( !dnIsSuffixScope( &vei->dn, &op->o_req_ndn, op->ors_scope ) )
			continue;

		variantInScope = 1;

		rc = variant_build_entry( op, vei, &vei->dn, &e, 0, NULL );
		if ( rc == LDAP_NO_SUCH_OBJECT || rc == LDAP_REFERRAL ) {
			rc = SLAP_CB_CONTINUE;
			continue;
		} else if ( rc != LDAP_SUCCESS ) {
			Debug( LDAP_DEBUG_TRACE, "variant_op_search: "
					"failed to retrieve entry: dn=%s\n",
					vei->dn.bv_val );
			goto done;
		}

		if ( test_filter( op, e, op->ors_filter ) == LDAP_COMPARE_TRUE ) {
			Debug( LDAP_DEBUG_TRACE, "variant_op_search: "
					"entry matched: dn=%s\n",
					vei->dn.bv_val );
			rs->sr_entry = e;
			rs->sr_attrs = op->ors_attrs;
			rc = send_search_entry( op, rs );
		}
		entry_free( e );
		e = NULL;
	}

	/* Three options:
	 * - the entry has been handled above, in that case vei->type is VARIANT_INFO_PLAIN
	 * - the entry matches a regex, use the first one and we're finished
	 * - no configuration matches entry - do nothing
	 */
	if ( op->ors_scope == LDAP_SCOPE_BASE &&
			variant_find_config( op, ov, &op->o_req_ndn, VARIANT_INFO_ALL, &vei,
					nmatch, pmatch ) == LDAP_SUCCESS &&
			vei->type == VARIANT_INFO_REGEX ) {
		rc = variant_build_entry( op, vei, &op->o_req_ndn, &e, nmatch, pmatch );
		if ( rc == LDAP_NO_SUCH_OBJECT || rc == LDAP_REFERRAL ) {
			rc = SLAP_CB_CONTINUE;
		} else if ( rc != LDAP_SUCCESS ) {
			Debug( LDAP_DEBUG_TRACE, "variant_op_search: "
					"failed to retrieve entry: dn=%s\n",
					vei->dn.bv_val );
			goto done;
		} else {
			if ( test_filter( op, e, op->ors_filter ) == LDAP_COMPARE_TRUE ) {
				Debug( LDAP_DEBUG_TRACE, "variant_op_search: "
						"entry matched: dn=%s\n",
						vei->dn.bv_val );
				rs->sr_entry = e;
				rs->sr_attrs = op->ors_attrs;
				rc = send_search_entry( op, rs );
			}
			entry_free( e );
			e = NULL;
			goto done;
		}
	}
	rc = SLAP_CB_CONTINUE;

	if ( variantInScope ) {
		cb = ch_calloc( 1, sizeof(slap_callback) );
		cb->sc_private = on;
		cb->sc_response = variant_search_response;
		cb->sc_next = op->o_callback;

		op->o_callback = cb;
	}

done:
	if ( rc != SLAP_CB_CONTINUE ) {
		rs->sr_err = (rc == LDAP_SUCCESS) ? rc : LDAP_OTHER;
		send_ldap_result( op, rs );
	}
	Debug( LDAP_DEBUG_TRACE, "variant_op_search: "
			"finished with %d\n", rc );
	return rc;
}

/* Configuration */

static ConfigLDAPadd variant_ldadd;
static ConfigLDAPadd variant_regex_ldadd;
static ConfigLDAPadd variant_attr_ldadd;

static ConfigDriver variant_set_dn;
static ConfigDriver variant_set_regex;
static ConfigDriver variant_set_alt_dn;
static ConfigDriver variant_set_alt_pattern;
static ConfigDriver variant_set_attribute;
static ConfigDriver variant_add_alt_attr;
static ConfigDriver variant_add_alt_attr_regex;

static ConfigCfAdd variant_cfadd;

enum
{
	VARIANT_ATTR = 1,
	VARIANT_ATTR_ALT,

	VARIANT_LAST,
};

static ConfigTable variant_cfg[] = {
	{ "passReplication", "on|off", 2, 2, 0,
		ARG_ON_OFF|ARG_OFFSET,
		(void *)offsetof( variant_info_t, passReplication ),
		"( OLcfgOvAt:FIXME.1 NAME 'olcVariantPassReplication' "
			"DESC 'Whether to let searches with replication control "
				"pass unmodified' "
			"SYNTAX OMsBoolean "
			"SINGLE-VALUE )",
			NULL, NULL
	},
	{ "variantDN", "dn", 2, 2, 0,
		ARG_DN|ARG_QUOTE|ARG_MAGIC,
		variant_set_dn,
		"( OLcfgOvAt:FIXME.2 NAME 'olcVariantEntry' "
			"DESC 'DN of the variant entry' "
			"EQUALITY distinguishedNameMatch "
			"SYNTAX OMsDN "
			"SINGLE-VALUE )",
			NULL, NULL
	},
	{ "variantRegex", "regex", 2, 2, 0,
		ARG_BERVAL|ARG_QUOTE|ARG_MAGIC,
		variant_set_regex,
		"( OLcfgOvAt:FIXME.6 NAME 'olcVariantEntryRegex' "
			"DESC 'Pattern for the variant entry' "
			"EQUALITY caseExactMatch "
			"SYNTAX OMsDirectoryString "
			"SINGLE-VALUE )",
			NULL, NULL
	},
	/* These have no equivalent in slapd.conf */
	{ "", NULL, 2, 2, 0,
		ARG_STRING|ARG_MAGIC|VARIANT_ATTR,
		variant_set_attribute,
		"( OLcfgOvAt:FIXME.3 NAME 'olcVariantVariantAttribute' "
			"DESC 'Attribute to fill in the entry' "
			"EQUALITY caseIgnoreMatch "
			"SYNTAX OMsDirectoryString "
			"SINGLE-VALUE )",
			NULL, NULL
	},
	{ "", NULL, 2, 2, 0,
		ARG_STRING|ARG_MAGIC|VARIANT_ATTR_ALT,
		variant_set_attribute,
		"( OLcfgOvAt:FIXME.4 NAME 'olcVariantAlternativeAttribute' "
			"DESC 'Attribute to take from the alternative entry' "
			"EQUALITY caseIgnoreMatch "
			"SYNTAX OMsDirectoryString "
			"SINGLE-VALUE )",
			NULL, NULL
	},
	{ "", NULL, 2, 2, 0,
		ARG_DN|ARG_QUOTE|ARG_MAGIC,
		variant_set_alt_dn,
		"( OLcfgOvAt:FIXME.5 NAME 'olcVariantAlternativeEntry' "
			"DESC 'DN of the alternative entry' "
			"EQUALITY distinguishedNameMatch "
			"SYNTAX OMsDN "
			"SINGLE-VALUE )",
			NULL, NULL
	},
	{ "", NULL, 2, 2, 0,
		ARG_BERVAL|ARG_QUOTE|ARG_MAGIC,
		variant_set_alt_pattern,
		"( OLcfgOvAt:FIXME.7 NAME 'olcVariantAlternativeEntryPattern' "
			"DESC 'Replacement pattern to locate the alternative entry' "
			"EQUALITY caseExactMatch "
			"SYNTAX OMsDirectoryString "
			"SINGLE-VALUE )",
			NULL, NULL
	},
	/* slapd.conf alternatives for the four above */
	{ "variantSpec", "attr attr2 dn", 4, 4, 0,
		ARG_QUOTE|ARG_MAGIC,
		variant_add_alt_attr,
		NULL, NULL, NULL
	},
	{ "variantRegexSpec", "attr attr2 pattern", 4, 4, 0,
		ARG_QUOTE|ARG_MAGIC,
		variant_add_alt_attr_regex,
		NULL, NULL, NULL
	},

	{ NULL, NULL, 0, 0, 0, ARG_IGNORED }
};

static ConfigOCs variant_ocs[] = {
	{ "( OLcfgOvOc:FIXME.1 "
		"NAME 'olcVariantConfig' "
		"DESC 'Variant overlay configuration' "
		"SUP olcOverlayConfig "
		"MAY ( olcVariantPassReplication ) )",
		Cft_Overlay, variant_cfg, NULL, variant_cfadd },
	{ "( OLcfgOvOc:FIXME.2 "
		"NAME 'olcVariantVariant' "
		"DESC 'Variant configuration' "
		"MUST ( olcVariantEntry ) "
		"MAY ( name ) "
		"SUP top "
		"STRUCTURAL )",
		Cft_Misc, variant_cfg, variant_ldadd },
	{ "( OLcfgOvOc:FIXME.3 "
		"NAME 'olcVariantAttribute' "
		"DESC 'Variant attribute description' "
		"MUST ( olcVariantVariantAttribute $ "
			"olcVariantAlternativeAttribute $ "
			"olcVariantAlternativeEntry "
		") "
		"MAY name "
		"SUP top "
		"STRUCTURAL )",
		Cft_Misc, variant_cfg, variant_attr_ldadd },
	{ "( OLcfgOvOc:FIXME.4 "
		"NAME 'olcVariantRegex' "
		"DESC 'Variant configuration' "
		"MUST ( olcVariantEntryRegex ) "
		"MAY ( name ) "
		"SUP top "
		"STRUCTURAL )",
		Cft_Misc, variant_cfg, variant_regex_ldadd },
	{ "( OLcfgOvOc:FIXME.5 "
		"NAME 'olcVariantAttributePattern' "
		"DESC 'Variant attribute description' "
		"MUST ( olcVariantVariantAttribute $ "
			"olcVariantAlternativeAttribute $ "
			"olcVariantAlternativeEntryPattern "
		") "
		"MAY name "
		"SUP top "
		"STRUCTURAL )",
		Cft_Misc, variant_cfg, variant_attr_ldadd },

	{ NULL, 0, NULL }
};

static int
variant_set_dn( ConfigArgs *ca )
{
	variantEntry_info *vei2, *vei = ca->ca_private;
	slap_overinst *on = (slap_overinst *)ca->bi;
	variant_info_t *ov = on->on_bi.bi_private;
	int diff;

	if ( ca->op == SLAP_CONFIG_EMIT ) {
		value_add_one( &ca->rvalue_vals, &vei->dn );
		return LDAP_SUCCESS;
	} else if ( ca->op == LDAP_MOD_DELETE ) {
		ber_memfree( vei->dn.bv_val );
		BER_BVZERO( &vei->dn );
		return LDAP_SUCCESS;
	}

	if ( !vei ) {
		vei = ch_calloc( 1, sizeof(variantEntry_info) );
		vei->ov = ov;
		vei->type = VARIANT_INFO_PLAIN;
		LDAP_SLIST_INIT(&vei->attributes);
		LDAP_STAILQ_ENTRY_INIT(vei, next);
		LDAP_STAILQ_INSERT_TAIL(&ov->variants, vei, next);

		ca->ca_private = vei;
	}
	vei->dn = ca->value_ndn;
	ber_memfree( ca->value_dn.bv_val );

	/* Each DN should only be listed once */
	LDAP_STAILQ_FOREACH( vei2, &vei->ov->variants, next ) {
		if ( vei == vei2 ) continue;

		dnMatch( &diff, 0, NULL, NULL, &vei->dn, &vei2->dn );
		if ( !diff ) {
			return LDAP_CONSTRAINT_VIOLATION;
		}
	}

	return LDAP_SUCCESS;
}

static int
variant_set_regex( ConfigArgs *ca )
{
	variantEntry_info *vei2, *vei = ca->ca_private;
	slap_overinst *on = (slap_overinst *)ca->bi;
	variant_info_t *ov = on->on_bi.bi_private;

	if ( ca->op == SLAP_CONFIG_EMIT ) {
		ca->value_bv = vei->dn;
		return LDAP_SUCCESS;
	} else if ( ca->op == LDAP_MOD_DELETE ) {
		ber_memfree( vei->dn.bv_val );
		BER_BVZERO( &vei->dn );
		regfree( vei->regex );
		return LDAP_SUCCESS;
	}

	if ( !vei ) {
		vei = ch_calloc( 1, sizeof(variantEntry_info) );
		vei->ov = ov;
		vei->type = VARIANT_INFO_REGEX;
		LDAP_SLIST_INIT(&vei->attributes);
		LDAP_STAILQ_ENTRY_INIT(vei, next);
		LDAP_STAILQ_INSERT_TAIL(&ov->regex_variants, vei, next);

		ca->ca_private = vei;
	}
	vei->dn = ca->value_bv;

	/* Each regex should only be listed once */
	LDAP_STAILQ_FOREACH( vei2, &vei->ov->regex_variants, next ) {
		if ( vei == vei2 ) continue;

		if ( !ber_bvcmp( &ca->value_bv, &vei2->dn ) ) {
			ch_free( vei );
			ca->ca_private = NULL;
			return LDAP_CONSTRAINT_VIOLATION;
		}
	}

	vei->regex = ch_calloc( 1, sizeof(regex_t) );
	if ( regcomp( vei->regex, vei->dn.bv_val, REG_EXTENDED ) ) {
		ch_free( vei->regex );
		ch_free( vei->dn.bv_val );
		return LDAP_CONSTRAINT_VIOLATION;
	}

	return LDAP_SUCCESS;
}

static int
variant_set_alt_dn( ConfigArgs *ca )
{
	variantAttr_info *vai = ca->ca_private;

	if ( ca->op == SLAP_CONFIG_EMIT ) {
		value_add_one( &ca->rvalue_vals, &vai->dn );
		return LDAP_SUCCESS;
	} else if ( ca->op == LDAP_MOD_DELETE ) {
		ber_memfree( vai->dn.bv_val );
		BER_BVZERO( &vai->dn );
		return LDAP_SUCCESS;
	}

	vai->dn = ca->value_ndn;
	ber_memfree( ca->value_dn.bv_val );

	return LDAP_SUCCESS;
}

static int
variant_set_alt_pattern( ConfigArgs *ca )
{
	variantAttr_info *vai = ca->ca_private;
	char *p = ca->value_bv.bv_val,
		 *end = ca->value_bv.bv_val + ca->value_bv.bv_len;

	if ( ca->op == SLAP_CONFIG_EMIT ) {
		ca->value_bv = vai->dn;
		return LDAP_SUCCESS;
	} else if ( ca->op == LDAP_MOD_DELETE ) {
		ber_memfree( vai->dn.bv_val );
		BER_BVZERO( &vai->dn );
		return LDAP_SUCCESS;
	}

	while ( (p = memchr( p, '$', end - p )) != NULL ) {
		p += 1;

		if ( ( ( *p >= '0' ) && ( *p <= '9' ) ) || ( *p == '$' ) ) {
			p += 1;
		} else {
			Debug( LDAP_DEBUG_ANY, "variant_set_alt_pattern: "
					"invalid replacement pattern supplied '%s'\n",
					ca->value_bv.bv_val );
			return LDAP_CONSTRAINT_VIOLATION;
		}
	}

	vai->dn = ca->value_bv;

	return LDAP_SUCCESS;
}

static int
variant_set_attribute( ConfigArgs *ca )
{
	variantAttr_info *vai2, *vai = ca->ca_private;
	char *s = ca->value_string;
	const char *text;
	AttributeDescription **ad;
	int rc;

	if ( ca->type == VARIANT_ATTR ) {
		ad = &vai->attr;
	} else {
		ad = &vai->alternative;
	}

	if ( ca->op == SLAP_CONFIG_EMIT ) {
		ca->value_string = ch_strdup( (*ad)->ad_cname.bv_val );
		return LDAP_SUCCESS;
	} else if ( ca->op == LDAP_MOD_DELETE ) {
		*ad = NULL;
		return LDAP_SUCCESS;
	}

	if ( *s == '{' ) {
		s = strchr( s, '}' );
		if ( !s ) {
			return LDAP_UNDEFINED_TYPE;
		}
		s += 1;
	}

	rc = slap_str2ad( s, ad, &text );
	ber_memfree( ca->value_string );
	if ( rc ) {
		return rc;
	}

	/* Both attributes have to share the same syntax */
	if ( vai->attr && vai->alternative &&
			vai->attr->ad_type->sat_syntax !=
					vai->alternative->ad_type->sat_syntax ) {
		return LDAP_CONSTRAINT_VIOLATION;
	}

	if ( ca->type == VARIANT_ATTR ) {
		/* Each attribute should only be listed once */
		LDAP_SLIST_FOREACH( vai2, &vai->variant->attributes, next ) {
			if ( vai == vai2 ) continue;
			if ( vai->attr == vai2->attr ) {
				return LDAP_CONSTRAINT_VIOLATION;
			}
		}
	}

	return LDAP_SUCCESS;
}

static int
variant_add_alt_attr( ConfigArgs *ca )
{
	slap_overinst *on = (slap_overinst *)ca->bi;
	variant_info_t *ov = on->on_bi.bi_private;
	variantEntry_info *vei =
			LDAP_STAILQ_LAST( &ov->variants, variantEntry_info, next );
	variantAttr_info *vai;
	struct berval dn, ndn;
	int rc;

	vai = ch_calloc( 1, sizeof(variantAttr_info) );
	vai->variant = vei;
	LDAP_SLIST_ENTRY_INIT( vai, next );
	ca->ca_private = vai;

	ca->value_string = ch_strdup( ca->argv[1] );
	ca->type = VARIANT_ATTR;
	rc = variant_set_attribute( ca );
	if ( rc != LDAP_SUCCESS ) {
		goto done;
	}

	ca->value_string = ch_strdup( ca->argv[2] );
	ca->type = VARIANT_ATTR_ALT;
	rc = variant_set_attribute( ca );
	if ( rc != LDAP_SUCCESS ) {
		goto done;
	}

	dn.bv_val = ca->argv[3];
	dn.bv_len = strlen( dn.bv_val );
	rc = dnNormalize( 0, NULL, NULL, &dn, &ndn, NULL );
	if ( rc != LDAP_SUCCESS ) {
		goto done;
	}

	ca->type = 0;
	BER_BVZERO( &ca->value_dn );
	ca->value_ndn = ndn;
	rc = variant_set_alt_dn( ca );
	if ( rc != LDAP_SUCCESS ) {
		ch_free( ndn.bv_val );
		goto done;
	}

done:
	if ( rc == LDAP_SUCCESS ) {
		LDAP_SLIST_INSERT_HEAD( &vei->attributes, vai, next );
	}

	return rc;
}

static int
variant_add_alt_attr_regex( ConfigArgs *ca )
{
	slap_overinst *on = (slap_overinst *)ca->bi;
	variant_info_t *ov = on->on_bi.bi_private;
	variantEntry_info *vei =
			LDAP_STAILQ_LAST( &ov->regex_variants, variantEntry_info, next );
	variantAttr_info *vai;
	int rc;

	vai = ch_calloc( 1, sizeof(variantAttr_info) );
	vai->variant = vei;
	LDAP_SLIST_ENTRY_INIT( vai, next );
	ca->ca_private = vai;

	ca->value_string = ch_strdup( ca->argv[1] );
	ca->type = VARIANT_ATTR;
	rc = variant_set_attribute( ca );
	if ( rc != LDAP_SUCCESS ) {
		goto done;
	}

	ca->value_string = ch_strdup( ca->argv[2] );
	ca->type = VARIANT_ATTR_ALT;
	rc = variant_set_attribute( ca );
	if ( rc != LDAP_SUCCESS ) {
		goto done;
	}

	ca->type = 0;
	ber_str2bv( ca->argv[3], 0, 1, &ca->value_bv );
	rc = variant_set_alt_pattern( ca );
	if ( rc != LDAP_SUCCESS ) {
		goto done;
	}

done:
	if ( rc == LDAP_SUCCESS ) {
		LDAP_SLIST_INSERT_HEAD( &vei->attributes, vai, next );
	}

	return rc;
}

static int
variant_ldadd_cleanup( ConfigArgs *ca )
{
	variantEntry_info *vei = ca->ca_private;
	slap_overinst *on = (slap_overinst *)ca->bi;
	variant_info_t *ov = on->on_bi.bi_private;

	if ( ca->reply.err != LDAP_SUCCESS ) {
		assert( LDAP_SLIST_EMPTY(&vei->attributes) );
		ch_free( vei->dn.bv_val );
		ch_free( vei );
		return LDAP_SUCCESS;
	}

	if ( vei->type == VARIANT_INFO_PLAIN ) {
		LDAP_STAILQ_INSERT_TAIL(&ov->variants, vei, next);
	} else {
		LDAP_STAILQ_INSERT_TAIL(&ov->regex_variants, vei, next);
	}

	return LDAP_SUCCESS;
}

static int
variant_ldadd( CfEntryInfo *cei, Entry *e, ConfigArgs *ca )
{
	slap_overinst *on;
	variant_info_t *ov;
	variantEntry_info *vei;

	if ( cei->ce_type != Cft_Overlay || !cei->ce_bi ||
			cei->ce_bi->bi_cf_ocs != variant_ocs )
		return LDAP_CONSTRAINT_VIOLATION;

	on = (slap_overinst *)cei->ce_bi;
	ov = on->on_bi.bi_private;

	vei = ch_calloc( 1, sizeof(variantEntry_info) );
	vei->ov = ov;
	vei->type = VARIANT_INFO_PLAIN;
	LDAP_SLIST_INIT(&vei->attributes);
	LDAP_STAILQ_ENTRY_INIT(vei, next);

	ca->bi = cei->ce_bi;
	ca->ca_private = vei;
	config_push_cleanup( ca, variant_ldadd_cleanup );
	/* config_push_cleanup is only run in the case of online config but we use it to
	 * save the new config when done with the entry */
	ca->lineno = 0;

	return LDAP_SUCCESS;
}

static int
variant_regex_ldadd( CfEntryInfo *cei, Entry *e, ConfigArgs *ca )
{
	slap_overinst *on;
	variant_info_t *ov;
	variantEntry_info *vei;

	if ( cei->ce_type != Cft_Overlay || !cei->ce_bi ||
			cei->ce_bi->bi_cf_ocs != variant_ocs )
		return LDAP_CONSTRAINT_VIOLATION;

	on = (slap_overinst *)cei->ce_bi;
	ov = on->on_bi.bi_private;

	vei = ch_calloc( 1, sizeof(variantEntry_info) );
	vei->ov = ov;
	vei->type = VARIANT_INFO_REGEX;
	LDAP_SLIST_INIT(&vei->attributes);
	LDAP_STAILQ_ENTRY_INIT(vei, next);

	ca->bi = cei->ce_bi;
	ca->ca_private = vei;
	config_push_cleanup( ca, variant_ldadd_cleanup );
	/* config_push_cleanup is only run in the case of online config but we use it to
	 * save the new config when done with the entry */
	ca->lineno = 0;

	return LDAP_SUCCESS;
}

static int
variant_attr_ldadd_cleanup( ConfigArgs *ca )
{
	variantAttr_info *vai = ca->ca_private;
	variantEntry_info *vei = vai->variant;

	if ( ca->reply.err != LDAP_SUCCESS ) {
		ch_free( vai->dn.bv_val );
		ch_free( vai );
		return LDAP_SUCCESS;
	}

	LDAP_SLIST_INSERT_HEAD(&vei->attributes, vai, next);

	return LDAP_SUCCESS;
}

static int
variant_attr_ldadd( CfEntryInfo *cei, Entry *e, ConfigArgs *ca )
{
	variantEntry_info *vei;
	variantAttr_info *vai;
	CfEntryInfo *parent = cei->ce_parent;

	if ( cei->ce_type != Cft_Misc || !parent || !parent->ce_bi ||
			parent->ce_bi->bi_cf_ocs != variant_ocs )
		return LDAP_CONSTRAINT_VIOLATION;

	vei = (variantEntry_info *)cei->ce_private;

	vai = ch_calloc( 1, sizeof(variantAttr_info) );
	vai->variant = vei;
	LDAP_SLIST_ENTRY_INIT(vai, next);

	ca->ca_private = vai;
	config_push_cleanup( ca, variant_attr_ldadd_cleanup );
	/* config_push_cleanup is only run in the case of online config but we use it to
	 * save the new config when done with the entry */
	ca->lineno = 0;

	return LDAP_SUCCESS;
}

static int
variant_cfadd( Operation *op, SlapReply *rs, Entry *p, ConfigArgs *ca )
{
	slap_overinst *on = (slap_overinst *)ca->bi;
	variant_info_t *ov = on->on_bi.bi_private;
	variantEntry_info *vei;
	variantAttr_info *vai;
	Entry *e;
	struct berval rdn;
	int i = 0;

	LDAP_STAILQ_FOREACH( vei, &ov->variants, next ) {
		int j = 0;
		rdn.bv_len = snprintf(
				ca->cr_msg, sizeof(ca->cr_msg), "name={%d}variant", i++ );
		rdn.bv_val = ca->cr_msg;

		ca->ca_private = vei;
		e = config_build_entry(
				op, rs, p->e_private, ca, &rdn, &variant_ocs[1], NULL );
		assert( e );

		LDAP_SLIST_FOREACH( vai, &vei->attributes, next ) {
			rdn.bv_len = snprintf( ca->cr_msg, sizeof(ca->cr_msg),
					"olcVariantVariantAttribute={%d}%s", j++,
					vai->attr->ad_cname.bv_val );
			rdn.bv_val = ca->cr_msg;

			ca->ca_private = vai;
			config_build_entry(
					op, rs, e->e_private, ca, &rdn, &variant_ocs[2], NULL );
		}
	}

	LDAP_STAILQ_FOREACH( vei, &ov->regex_variants, next ) {
		int j = 0;
		rdn.bv_len = snprintf(
				ca->cr_msg, sizeof(ca->cr_msg), "name={%d}regex", i++ );
		rdn.bv_val = ca->cr_msg;

		ca->ca_private = vei;
		e = config_build_entry(
				op, rs, p->e_private, ca, &rdn, &variant_ocs[3], NULL );
		assert( e );

		LDAP_SLIST_FOREACH( vai, &vei->attributes, next ) {
			rdn.bv_len = snprintf( ca->cr_msg, sizeof(ca->cr_msg),
					"olcVariantVariantAttribute={%d}%s", j++,
					vai->attr->ad_cname.bv_val );
			rdn.bv_val = ca->cr_msg;

			ca->ca_private = vai;
			config_build_entry(
					op, rs, e->e_private, ca, &rdn, &variant_ocs[4], NULL );
		}
	}
	return LDAP_SUCCESS;
}

static slap_overinst variant;

static int
variant_db_init( BackendDB *be, ConfigReply *cr )
{
	slap_overinst *on = (slap_overinst *)be->bd_info;
	variant_info_t *ov;

	if ( SLAP_ISGLOBALOVERLAY(be) ) {
		Debug( LDAP_DEBUG_ANY, "variant overlay must be instantiated within "
				"a database.\n" );
		return 1;
	}

	ov = ch_calloc( 1, sizeof(variant_info_t) );
	LDAP_STAILQ_INIT(&ov->variants);
	LDAP_STAILQ_INIT(&ov->regex_variants);

	on->on_bi.bi_private = ov;

	return LDAP_SUCCESS;
}

static int
variant_db_destroy( BackendDB *be, ConfigReply *cr )
{
	slap_overinst *on = (slap_overinst *)be->bd_info;
	variant_info_t *ov = on->on_bi.bi_private;

	if ( ov ) {
		while ( !LDAP_STAILQ_EMPTY( &ov->variants ) ) {
			variantEntry_info *vei = LDAP_STAILQ_FIRST( &ov->variants );
			LDAP_STAILQ_REMOVE_HEAD( &ov->variants, next );

			while ( !LDAP_SLIST_EMPTY( &vei->attributes ) ) {
				variantAttr_info *vai = LDAP_SLIST_FIRST( &vei->attributes );
				LDAP_SLIST_REMOVE_HEAD( &vei->attributes, next );

				ber_memfree( vai->dn.bv_val );
				ch_free( vai );
			}
			ber_memfree( vei->dn.bv_val );
			ch_free( vei );
		}
		while ( !LDAP_STAILQ_EMPTY( &ov->regex_variants ) ) {
			variantEntry_info *vei = LDAP_STAILQ_FIRST( &ov->regex_variants );
			LDAP_STAILQ_REMOVE_HEAD( &ov->regex_variants, next );

			while ( !LDAP_SLIST_EMPTY( &vei->attributes ) ) {
				variantAttr_info *vai = LDAP_SLIST_FIRST( &vei->attributes );
				LDAP_SLIST_REMOVE_HEAD( &vei->attributes, next );

				ber_memfree( vai->dn.bv_val );
				ch_free( vai );
			}
			ber_memfree( vei->dn.bv_val );
			regfree( vei->regex );
			ch_free( vei->regex );
			ch_free( vei );
		}
		ch_free( ov );
	}

	return LDAP_SUCCESS;
}

int
variant_initialize()
{
	int rc;

	variant.on_bi.bi_type = "variant";
	variant.on_bi.bi_db_init = variant_db_init;
	variant.on_bi.bi_db_destroy = variant_db_destroy;

	variant.on_bi.bi_op_add = variant_op_add;
	variant.on_bi.bi_op_compare = variant_op_compare;
	variant.on_bi.bi_op_modify = variant_op_mod;
	variant.on_bi.bi_op_search = variant_op_search;

	variant.on_bi.bi_cf_ocs = variant_ocs;

	rc = config_register_schema( variant_cfg, variant_ocs );
	if ( rc ) return rc;

	return overlay_register( &variant );
}

#if SLAPD_OVER_VARIANT == SLAPD_MOD_DYNAMIC
int
init_module( int argc, char *argv[] )
{
	return variant_initialize();
}
#endif

#endif /* SLAPD_OVER_VARIANT */
