/*
    SSSD

    ID-mapping library

    Authors:
        Sumit Bose <sbose@redhat.com>

    Copyright (C) 2012 Red Hat

    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 3 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, see <http://www.gnu.org/licenses/>.
*/

#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <inttypes.h>

#include "lib/idmap/sss_idmap.h"
#include "lib/idmap/sss_idmap_private.h"
#include "shared/murmurhash3.h"

#define SID_FMT "%s-%"PRIu32
#define SID_STR_MAX_LEN 1024

/* Hold all parameters for unix<->sid mapping relevant for
 * given slice. */
struct idmap_range_params {
    uint32_t min_id;
    uint32_t max_id;
    char *range_id;

    uint32_t first_rid;
    struct idmap_range_params *next;
};

struct idmap_domain_info {
    char *name;
    char *sid;
    struct idmap_range_params range_params;
    struct idmap_domain_info *next;
    bool external_mapping;

    struct idmap_range_params *helpers;
    bool auto_add_ranges;
    bool helpers_owner;

    idmap_store_cb cb;
    void *pvt;
};

static void *default_alloc(size_t size, void *pvt)
{
    return malloc(size);
}

static void default_free(void *ptr, void *pvt)
{
    free(ptr);
}

static char *idmap_strdup(struct sss_idmap_ctx *ctx, const char *str)
{
    char *new = NULL;
    size_t len;

    CHECK_IDMAP_CTX(ctx, NULL);

    len = strlen(str) + 1;

    new = ctx->alloc_func(len, ctx->alloc_pvt);
    if (new == NULL) {
        return NULL;
    }

    memcpy(new, str, len);

    return new;
}

static bool ranges_eq(const struct idmap_range_params *a,
                      const struct idmap_range_params *b)
{
    if (a == NULL || b == NULL) {
        return false;
    }

    if (a->first_rid == b->first_rid
            && a->min_id == b->min_id
            && a->max_id == b->max_id) {
        return true;
    }

    return false;
}

static enum idmap_error_code
construct_range(struct sss_idmap_ctx *ctx,
                const struct idmap_range_params *src,
                char *id,
                struct idmap_range_params **_dst)
{
    struct idmap_range_params *dst;

    if (src == NULL || id == NULL || _dst == NULL) {
        return IDMAP_ERROR;
    }

    dst = ctx->alloc_func(sizeof(struct idmap_range_params), ctx->alloc_pvt);
    if (dst == NULL) {
        return IDMAP_OUT_OF_MEMORY;
    }

    dst->min_id = src->min_id;
    dst->max_id = src->max_id;
    dst->first_rid = src->first_rid;
    dst->next = NULL;
    dst->range_id = id;

    *_dst = dst;
    return IDMAP_SUCCESS;
}

static bool id_is_in_range(uint32_t id,
                           struct idmap_range_params *rp,
                           uint32_t *rid)
{
    if (id == 0 || rp == NULL) {
        return false;
    }

    if (id >= rp->min_id && id <= rp->max_id) {
        if (rid != NULL) {
            *rid = rp->first_rid + (id - rp->min_id);
        }

        return true;
    }

    return false;
}

const char *idmap_error_string(enum idmap_error_code err)
{
    switch (err) {
        case IDMAP_SUCCESS:
            return "IDMAP operation successful";
            break;
        case IDMAP_NOT_IMPLEMENTED:
            return "IDMAP Function is not yet implemented";
            break;
        case IDMAP_ERROR:
            return "IDMAP general error";
            break;
        case IDMAP_OUT_OF_MEMORY:
            return "IDMAP operation ran out of memory";
            break;
        case IDMAP_NO_DOMAIN:
            return "IDMAP domain not found";
            break;
        case IDMAP_CONTEXT_INVALID:
            return "IDMAP context is invalid";
            break;
        case IDMAP_SID_INVALID:
            return "IDMAP SID is invalid";
            break;
        case IDMAP_SID_UNKNOWN:
            return "IDMAP SID not found";
            break;
        case IDMAP_NO_RANGE:
            return "IDMAP range not found";
            break;
        case IDMAP_BUILTIN_SID:
            return "IDMAP SID from BUILTIN domain";
            break;
        case IDMAP_OUT_OF_SLICES:
            return "IDMAP not more free slices";
            break;
        case IDMAP_COLLISION:
            return "IDMAP new range collides with existing one";
            break;
        case IDMAP_EXTERNAL:
            return "IDMAP ID managed externally";
            break;
        case IDMAP_NAME_UNKNOWN:
            return "IDMAP domain with the given name not found";
            break;
        default:
            return "IDMAP unknown error code";
    }
}

bool is_domain_sid(const char *sid)
{
    const char *p;
    long long a;
    char *endptr;
    size_t c;

    if (sid == NULL || strncmp(sid, DOM_SID_PREFIX, DOM_SID_PREFIX_LEN) != 0) {
        return false;
    }

    p = sid + DOM_SID_PREFIX_LEN;
    c = 0;

    do {
        errno = 0;
        a = strtoull(p, &endptr, 10);
        if (errno != 0 || a > UINT32_MAX) {
            return false;
        }

        if (*endptr == '-') {
            p = endptr + 1;
        } else if (*endptr != '\0') {
            return false;
        }
        c++;
    } while(c < 3 && *endptr != '\0');

    if (c != 3 || *endptr != '\0') {
        return false;
    }

    return true;
}

enum idmap_error_code sss_idmap_init(idmap_alloc_func *alloc_func,
                                     void *alloc_pvt,
                                     idmap_free_func *free_func,
                                     struct sss_idmap_ctx **_ctx)
{
    struct sss_idmap_ctx *ctx;

    if (alloc_func == NULL) {
        alloc_func = default_alloc;
    }

    ctx = alloc_func(sizeof(struct sss_idmap_ctx), alloc_pvt);
    if (ctx == NULL) {
        return IDMAP_OUT_OF_MEMORY;
    }
    memset(ctx, 0, sizeof(struct sss_idmap_ctx));

    ctx->alloc_func = alloc_func;
    ctx->alloc_pvt = alloc_pvt;
    ctx->free_func = (free_func == NULL) ? default_free : free_func;

    /* Set default values. */
    ctx->idmap_opts.autorid_mode = SSS_IDMAP_DEFAULT_AUTORID;
    ctx->idmap_opts.idmap_lower = SSS_IDMAP_DEFAULT_LOWER;
    ctx->idmap_opts.idmap_upper = SSS_IDMAP_DEFAULT_UPPER;
    ctx->idmap_opts.rangesize = SSS_IDMAP_DEFAULT_RANGESIZE;
    ctx->idmap_opts.extra_slice_init = SSS_IDMAP_DEFAULT_EXTRA_SLICE_INIT;

    *_ctx = ctx;

    return IDMAP_SUCCESS;
}

static void free_helpers(struct sss_idmap_ctx *ctx,
                         struct idmap_range_params *helpers,
                         bool helpers_owner)
{
    struct idmap_range_params *it = helpers;
    struct idmap_range_params *tmp;

    if (helpers_owner == false) {
        return;
    }

    while (it != NULL) {
        tmp = it->next;

        ctx->free_func(it->range_id, ctx->alloc_pvt);
        ctx->free_func(it, ctx->alloc_pvt);

        it = tmp;
    }
}

static struct idmap_range_params*
get_helper_by_id(struct idmap_range_params *helpers, const char *id)
{
    struct idmap_range_params *it;

    for (it = helpers; it != NULL; it = it->next) {
        if (strcmp(it->range_id, id) == 0) {
            return it;
        }
    }

    return NULL;
}

static void sss_idmap_free_domain(struct sss_idmap_ctx *ctx,
                                  struct idmap_domain_info *dom)
{
    if (ctx == NULL || dom == NULL) {
        return;
    }

    ctx->free_func(dom->range_params.range_id, ctx->alloc_pvt);

    free_helpers(ctx, dom->helpers, dom->helpers_owner);

    ctx->free_func(dom->name, ctx->alloc_pvt);
    ctx->free_func(dom->sid, ctx->alloc_pvt);
    ctx->free_func(dom, ctx->alloc_pvt);
}

enum idmap_error_code sss_idmap_free(struct sss_idmap_ctx *ctx)
{
    struct idmap_domain_info *dom;
    struct idmap_domain_info *next;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    next = ctx->idmap_domain_info;
    while (next) {
        dom = next;
        next = dom->next;
        sss_idmap_free_domain(ctx, dom);
    }

    ctx->free_func(ctx, ctx->alloc_pvt);

    return IDMAP_SUCCESS;
}

static enum idmap_error_code sss_idmap_free_ptr(struct sss_idmap_ctx *ctx,
                                                void *ptr)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    if (ptr != NULL) {
        ctx->free_func(ptr, ctx->alloc_pvt);
    }

    return IDMAP_SUCCESS;
}

enum idmap_error_code sss_idmap_free_sid(struct sss_idmap_ctx *ctx,
                                         char *sid)
{
    return sss_idmap_free_ptr(ctx, sid);
}

enum idmap_error_code sss_idmap_free_dom_sid(struct sss_idmap_ctx *ctx,
                                             struct sss_dom_sid *dom_sid)
{
    return sss_idmap_free_ptr(ctx, dom_sid);
}

enum idmap_error_code sss_idmap_free_smb_sid(struct sss_idmap_ctx *ctx,
                                             struct dom_sid *smb_sid)
{
    return sss_idmap_free_ptr(ctx, smb_sid);
}

enum idmap_error_code sss_idmap_free_bin_sid(struct sss_idmap_ctx *ctx,
                                             uint8_t *bin_sid)
{
    return sss_idmap_free_ptr(ctx, bin_sid);
}

static bool check_overlap(struct idmap_range_params *range,
                          id_t min, id_t max)
{
    return ((range->min_id <= min && range->max_id >= max)
                || (range->min_id >= min && range->min_id <= max)
                || (range->max_id >= min && range->max_id <= max));
}

static bool check_dom_overlap(struct idmap_range_params *prim_range,
                              /* struct idmap_range_params *sec_ranges, */
                              id_t min,
                              id_t max)
{
    return check_overlap(prim_range, min, max);
}

enum idmap_error_code sss_idmap_calculate_range(struct sss_idmap_ctx *ctx,
                                                const char *range_id,
                                                id_t *slice_num,
                                                struct sss_idmap_range *_range)
{
    id_t max_slices;
    id_t orig_slice;
    id_t new_slice = 0;
    id_t min;
    id_t max;
    id_t idmap_lower;
    id_t idmap_upper;
    id_t rangesize;
    bool autorid_mode;
    uint32_t hash_val;
    struct idmap_domain_info *dom;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    idmap_lower = ctx->idmap_opts.idmap_lower;
    idmap_upper = ctx->idmap_opts.idmap_upper;
    rangesize = ctx->idmap_opts.rangesize;
    autorid_mode = ctx->idmap_opts.autorid_mode;

    max_slices = (idmap_upper - idmap_lower) / rangesize;

    if (slice_num && *slice_num != -1) {
        /* The slice is being set explicitly.
         * This may happen at system startup when we're loading
         * previously-determined slices. In the future, we may also
         * permit configuration to select the slice for a domain
         * explicitly.
         */
        new_slice = *slice_num;
        min = (rangesize * new_slice) + idmap_lower;
        max = min + rangesize - 1;
        for (dom = ctx->idmap_domain_info; dom != NULL; dom = dom->next) {
                if (check_dom_overlap(&dom->range_params,min, max)) {
                    /* This range overlaps one already registered
                     * Fail, because the slice was manually configured
                     */
                    return IDMAP_COLLISION;
                }
        }
    } else {
        /* If slice is -1, we're being asked to pick a new slice */

        if (autorid_mode) {
            /* In autorid compatibility mode, always start at 0 and find the
             * first free value.
             */
            orig_slice = 0;
        } else {
            /* Hash the range identifier string */
            hash_val = murmurhash3(range_id, strlen(range_id), 0xdeadbeef);

            /* Now get take the modulus of the hash val and the max_slices
             * to determine its optimal position in the range.
             */
            new_slice = hash_val % max_slices;
            orig_slice = new_slice;
        }

        min = (rangesize * new_slice) + idmap_lower;
        max = min + rangesize - 1;
        /* Verify that this slice is not already in use */
        do {
            for (dom = ctx->idmap_domain_info; dom != NULL; dom = dom->next) {

                if (check_dom_overlap(&dom->range_params,
                                      min, max)) {
                    /* This range overlaps one already registered
                     * We'll try the next available slot
                     */
                    new_slice++;
                    if (new_slice >= max_slices) {
                        /* loop around to the beginning if necessary */
                        new_slice = 0;
                    }

                    min = (rangesize * new_slice) + idmap_lower;
                    max = min + rangesize - 1;
                    break;
                }
            }

            /* Keep trying until dom is NULL (meaning we got to the end
             * without matching) or we have run out of slices and gotten
             * back to the first one we tried.
             */
        } while (dom && new_slice != orig_slice);

        if (dom) {
            /* We looped all the way through and found no empty slots */
            return IDMAP_OUT_OF_SLICES;
        }
    }

    _range->min = (rangesize * new_slice) + idmap_lower;
    _range->max = _range->min + rangesize - 1;

    if (slice_num) {
        *slice_num = new_slice;
    }

    return IDMAP_SUCCESS;
}

enum idmap_error_code sss_idmap_check_collision_ex(const char *o_name,
                                                const char *o_sid,
                                                struct sss_idmap_range *o_range,
                                                uint32_t o_first_rid,
                                                const char *o_range_id,
                                                bool o_external_mapping,
                                                const char *n_name,
                                                const char *n_sid,
                                                struct sss_idmap_range *n_range,
                                                uint32_t n_first_rid,
                                                const char *n_range_id,
                                                bool n_external_mapping)
{
    bool names_equal;
    bool sids_equal;

    /* TODO: if both ranges have the same ID check if an update is
     * needed. */

    /* Check if ID ranges overlap.
     * ID ranges with external mapping may overlap. */
    if ((!n_external_mapping && !o_external_mapping)
        && ((n_range->min >= o_range->min
                && n_range->min <= o_range->max)
            || (n_range->max >= o_range->min
                && n_range->max <= o_range->max))) {
        return IDMAP_COLLISION;
    }

    names_equal = (strcasecmp(n_name, o_name) == 0);
    sids_equal = ((n_sid == NULL && o_sid == NULL)
                    || (n_sid != NULL && o_sid != NULL
                        && strcasecmp(n_sid, o_sid) == 0));

    /* check if domain name and SID are consistent */
    if ((names_equal && !sids_equal) || (!names_equal && sids_equal)) {
        return IDMAP_COLLISION;
    }

    /* check if external_mapping is consistent */
    if (names_equal && sids_equal
            && n_external_mapping != o_external_mapping) {
        return IDMAP_COLLISION;
    }

    /* check if RID ranges overlap */
    if (names_equal && sids_equal
            && n_external_mapping == false
            && n_first_rid >= o_first_rid
            && n_first_rid <= o_first_rid + (o_range->max - o_range->min)) {
        return IDMAP_COLLISION;
    }

    return IDMAP_SUCCESS;
}

enum idmap_error_code sss_idmap_check_collision(struct sss_idmap_ctx *ctx,
                                                char *n_name, char *n_sid,
                                                struct sss_idmap_range *n_range,
                                                uint32_t n_first_rid,
                                                char *n_range_id,
                                                bool n_external_mapping)
{
    struct idmap_domain_info *dom;
    enum idmap_error_code err;
    struct sss_idmap_range range;

    for (dom = ctx->idmap_domain_info; dom != NULL; dom = dom->next) {

        range.min = dom->range_params.min_id;
        range.max = dom->range_params.max_id;

        err = sss_idmap_check_collision_ex(dom->name, dom->sid,
                                           &range,
                                           dom->range_params.first_rid,
                                           dom->range_params.range_id,
                                           dom->external_mapping,
                                           n_name, n_sid, n_range, n_first_rid,
                                           n_range_id, n_external_mapping);
        if (err != IDMAP_SUCCESS) {
            return err;
        }
    }
    return IDMAP_SUCCESS;
}

static enum
idmap_error_code dom_check_collision(struct idmap_domain_info *dom_list,
                                     struct idmap_domain_info *new_dom)
{
    struct idmap_domain_info *dom;
    enum idmap_error_code err;
    struct sss_idmap_range range;
    struct sss_idmap_range new_dom_range = { new_dom->range_params.min_id,
                                             new_dom->range_params.max_id };

    for (dom = dom_list; dom != NULL; dom = dom->next) {
        range.min = dom->range_params.min_id;
        range.max = dom->range_params.max_id;

        err = sss_idmap_check_collision_ex(dom->name, dom->sid,
                                           &range,
                                           dom->range_params.first_rid,
                                           dom->range_params.range_id,
                                           dom->external_mapping,
                                           new_dom->name, new_dom->sid,
                                           &new_dom_range,
                                           new_dom->range_params.first_rid,
                                           new_dom->range_params.range_id,
                                           new_dom->external_mapping);
        if (err != IDMAP_SUCCESS) {
            return err;
        }
    }
    return IDMAP_SUCCESS;
}

static char*
generate_sec_slice_name(struct sss_idmap_ctx *ctx,
                        const char *domain_sid, uint32_t rid)
{
    const char *SEC_SLICE_NAME_FMT = "%s-%"PRIu32;
    char *slice_name;
    int len, len2;

    len = snprintf(NULL, 0, SEC_SLICE_NAME_FMT, domain_sid, rid);
    if (len <= 0) {
        return NULL;
    }

    slice_name = ctx->alloc_func(len + 1, ctx->alloc_pvt);
    if (slice_name == NULL) {
        return NULL;
    }

    len2 = snprintf(slice_name, len + 1, SEC_SLICE_NAME_FMT, domain_sid,
                    rid);
    if (len != len2) {
        ctx->free_func(slice_name, ctx->alloc_pvt);
        return NULL;
    }

    return slice_name;
}

static enum idmap_error_code
generate_slice(struct sss_idmap_ctx *ctx, char *slice_name, uint32_t first_rid,
               struct idmap_range_params **_slice)
{
    struct idmap_range_params *slice;
    struct sss_idmap_range tmp_range;
    enum idmap_error_code err;

    slice = ctx->alloc_func(sizeof(struct idmap_range_params), ctx->alloc_pvt);
    if (slice == NULL) {
        return IDMAP_OUT_OF_MEMORY;
    }

    slice->next = NULL;

    err = sss_idmap_calculate_range(ctx, slice_name, NULL, &tmp_range);
    if (err != IDMAP_SUCCESS) {
        ctx->free_func(slice, ctx->alloc_pvt);
        return err;
    }

    slice->min_id = tmp_range.min;
    slice->max_id = tmp_range.max;
    slice->range_id = slice_name;
    slice->first_rid = first_rid;

    *_slice = slice;
    return IDMAP_SUCCESS;
}

static enum idmap_error_code
get_helpers(struct sss_idmap_ctx *ctx,
            const char *domain_sid,
            uint32_t first_rid,
            struct idmap_range_params **_sec_slices)
{
    struct idmap_range_params *prev = NULL;
    struct idmap_range_params *sec_slices = NULL;
    static enum idmap_error_code err;
    struct idmap_range_params *slice;
    char *secondary_name;

    for (int i = 0; i < ctx->idmap_opts.extra_slice_init; i++) {
        secondary_name = generate_sec_slice_name(ctx, domain_sid, first_rid);
        if (secondary_name == NULL) {
            err = IDMAP_OUT_OF_MEMORY;
            goto fail;
        }

        err = generate_slice(ctx, secondary_name, first_rid, &slice);
        if (err != IDMAP_SUCCESS) {
            goto fail;
        }

        first_rid += ctx->idmap_opts.rangesize;

        if (prev != NULL) {
            prev->next = slice;
        }

        if (sec_slices == NULL) {
            sec_slices = slice;
        }

        prev = slice;
    }

    *_sec_slices = sec_slices;
    return IDMAP_SUCCESS;

fail:
    ctx->free_func(secondary_name, ctx->alloc_pvt);

    /* Free already generated helpers. */
    free_helpers(ctx, sec_slices, true);

    return err;
}

enum idmap_error_code sss_idmap_add_domain_ex(struct sss_idmap_ctx *ctx,
                                              const char *domain_name,
                                              const char *domain_sid,
                                              struct sss_idmap_range *range,
                                              const char *range_id,
                                              uint32_t rid,
                                              bool external_mapping)
{
    struct idmap_domain_info *dom = NULL;
    enum idmap_error_code err;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    if (domain_name == NULL) {
        return IDMAP_NO_DOMAIN;
    }

    if (range == NULL) {
        return IDMAP_NO_RANGE;
    }

    /* For algorithmic mapping a valid domain SID is required, for external
     * mapping it may be NULL, but if set it should be valid. */
    if ((!external_mapping && !is_domain_sid(domain_sid))
            || (external_mapping
                && domain_sid != NULL
                && !is_domain_sid(domain_sid))) {
        return IDMAP_SID_INVALID;
    }

    dom = ctx->alloc_func(sizeof(struct idmap_domain_info), ctx->alloc_pvt);
    if (dom == NULL) {
        return IDMAP_OUT_OF_MEMORY;
    }
    memset(dom, 0, sizeof(struct idmap_domain_info));

    dom->name = idmap_strdup(ctx, domain_name);
    if (dom->name == NULL) {
        err = IDMAP_OUT_OF_MEMORY;
        goto fail;
    }

    if (domain_sid != NULL) {
        dom->sid = idmap_strdup(ctx, domain_sid);
        if (dom->sid == NULL) {
            err = IDMAP_OUT_OF_MEMORY;
            goto fail;
        }
    }

    dom->range_params.min_id = range->min;
    dom->range_params.max_id = range->max;

    if (range_id != NULL) {
        dom->range_params.range_id = idmap_strdup(ctx, range_id);
        if (dom->range_params.range_id == NULL) {
            err = IDMAP_OUT_OF_MEMORY;
            goto fail;
        }
    }

    dom->range_params.first_rid = rid;
    dom->external_mapping = external_mapping;

    err = dom_check_collision(ctx->idmap_domain_info, dom);
    if (err != IDMAP_SUCCESS) {
        goto fail;
    }

    dom->next = ctx->idmap_domain_info;
    ctx->idmap_domain_info = dom;

    return IDMAP_SUCCESS;

fail:
    sss_idmap_free_domain(ctx, dom);

    return err;
}

enum idmap_error_code
sss_idmap_add_auto_domain_ex(struct sss_idmap_ctx *ctx,
                             const char *domain_name,
                             const char *domain_sid,
                             struct sss_idmap_range *range,
                             const char *range_id,
                             uint32_t rid,
                             bool external_mapping,
                             idmap_store_cb cb,
                             void *pvt)
{
    enum idmap_error_code err;

    err = sss_idmap_add_domain_ex(ctx, domain_name, domain_sid, range,
                                  range_id, rid, external_mapping);
    if (err != IDMAP_SUCCESS) {
        return err;
    }

    if (external_mapping) {
        /* There's no point in generating secondary ranges if external_mapping
           is enabled. */
        ctx->idmap_domain_info->auto_add_ranges = false;
        return IDMAP_SUCCESS;
    }

    if ((range->max - range->min + 1) != ctx->idmap_opts.rangesize) {
        /* Range of primary slice is not equal to the value of
           ldap_idmap_range_size option. */
        return IDMAP_ERROR;
    }

    /* No additional secondary ranges should be added if no sec ranges are
       predeclared. */
    if (ctx->idmap_opts.extra_slice_init == 0) {
        ctx->idmap_domain_info->auto_add_ranges = false;
        return IDMAP_SUCCESS;
    }

    /* Add size of primary slice for first_rid of secondary slices. */
    rid += ctx->idmap_opts.rangesize;
    err = get_helpers(ctx, domain_sid, rid,
                      &ctx->idmap_domain_info->helpers);
    if (err == IDMAP_SUCCESS) {
        ctx->idmap_domain_info->auto_add_ranges = true;
        ctx->idmap_domain_info->helpers_owner = true;
    } else {
        /* Running out of slices for secondary mapping is a non-fatal
         * problem. */
        if (err == IDMAP_OUT_OF_SLICES) {
            err = IDMAP_SUCCESS;
        }
        ctx->idmap_domain_info->auto_add_ranges = false;
    }

    ctx->idmap_domain_info->cb = cb;
    ctx->idmap_domain_info->pvt = pvt;

    return err;
}

enum idmap_error_code sss_idmap_add_domain(struct sss_idmap_ctx *ctx,
                                           const char *domain_name,
                                           const char *domain_sid,
                                           struct sss_idmap_range *range)
{
    return sss_idmap_add_domain_ex(ctx, domain_name, domain_sid, range, NULL,
                                   0, false);
}

static bool sss_idmap_sid_is_builtin(const char *sid)
{
    if (strncmp(sid, "S-1-5-32-", 9) == 0) {
        return true;
    }

    return false;
}

static bool parse_rid(const char *sid, size_t dom_prefix_len, long long *_rid)
{
    long long rid;
    char *endptr;

    errno = 0;
    /* Use suffix of sid - part after domain and following '-' */
    rid = strtoull(sid + dom_prefix_len + 1, &endptr, 10);
    if (errno != 0 || rid > UINT32_MAX || *endptr != '\0') {
        return false;
    }

    *_rid = rid;
    return true;
}

static bool is_sid_from_dom(const char *dom_sid, const char *sid,
                            size_t *_dom_sid_len)
{
    size_t dom_sid_len;

    if (dom_sid == NULL) {
        return false;
    }

    dom_sid_len = strlen(dom_sid);
    *_dom_sid_len = dom_sid_len;

    if (strlen(sid) < dom_sid_len || sid[dom_sid_len] != '-') {
        return false;
    }

    return strncmp(sid, dom_sid, dom_sid_len) == 0;
}

static bool comp_id(struct idmap_range_params *range_params, long long rid,
                    uint32_t *_id)
{
    uint32_t id;

    if (rid >= range_params->first_rid
            && ((UINT32_MAX - range_params->min_id) >
               (rid - range_params->first_rid))) {
        id = range_params->min_id + (rid - range_params->first_rid);
        if (id <= range_params->max_id) {
            *_id = id;
            return true;
        }
    }
    return false;
}

static enum idmap_error_code
get_range(struct sss_idmap_ctx *ctx,
          struct idmap_range_params *helpers,
          const char *dom_sid,
          long long rid,
          struct idmap_range_params **_range)
{
    char *secondary_name = NULL;
    enum idmap_error_code err;
    int first_rid;
    struct idmap_range_params *range;
    struct idmap_range_params *helper;

    first_rid = (rid / ctx->idmap_opts.rangesize) * ctx->idmap_opts.rangesize;

    secondary_name = generate_sec_slice_name(ctx, dom_sid, first_rid);
    if (secondary_name == NULL) {
        err = IDMAP_OUT_OF_MEMORY;
        goto error;
    }

    helper = get_helper_by_id(helpers, secondary_name);
    if (helper != NULL) {
        /* Utilize helper's range. */
        err = construct_range(ctx, helper, secondary_name, &range);
    } else {
        /* Have to generate a whole new range. */
        err = generate_slice(ctx, secondary_name, first_rid, &range);
    }

    if (err != IDMAP_SUCCESS) {
        goto error;
    }

    *_range = range;
    return IDMAP_SUCCESS;

error:
    ctx->free_func(secondary_name, ctx->alloc_pvt);
    return err;
}

static enum idmap_error_code
spawn_dom(struct sss_idmap_ctx *ctx,
          struct idmap_domain_info *parent,
          struct idmap_range_params *range)
{
    struct sss_idmap_range tmp;
    static enum idmap_error_code err;
    struct idmap_domain_info *it;

    tmp.min = range->min_id;
    tmp.max = range->max_id;

    err = sss_idmap_add_domain_ex(ctx,
                                  parent->name,
                                  parent->sid,
                                  &tmp, range->range_id,
                                  range->first_rid, false);
    if (err != IDMAP_SUCCESS) {
        return err;
    }

    it = ctx->idmap_domain_info;
    while (it != NULL) {
        /* Find the newly added domain. */
        if (ranges_eq(&it->range_params, range)) {

            /* Share helpers. */
            it->helpers = parent->helpers;
            it->auto_add_ranges = parent->auto_add_ranges;

            /* Share call back for storing domains */
            it->cb = parent->cb;
            it->pvt = parent->pvt;
            break;
        }

        it = it->next;
    }

    if (it == NULL) {
        /* Failed to find just added domain. */
        return IDMAP_ERROR;
    }

    /* Store mapping for newly created domain. */
    if (it->cb != NULL) {
        err = it->cb(it->name,
                     it->sid,
                     it->range_params.range_id,
                     it->range_params.min_id,
                     it->range_params.max_id,
                     it->range_params.first_rid,
                     it->pvt);
        if (err != IDMAP_SUCCESS) {
            return err;
        }
    }

    return IDMAP_SUCCESS;
}

static enum idmap_error_code
add_dom_for_sid(struct sss_idmap_ctx *ctx,
                struct idmap_domain_info *matched_dom,
                const char *sid,
                uint32_t *_id)
{
    enum idmap_error_code err;
    long long rid;
    struct idmap_range_params *range = NULL;

    if (parse_rid(sid, strlen(matched_dom->sid), &rid) == false) {
        err = IDMAP_SID_INVALID;
        goto done;
    }

    err = get_range(ctx, matched_dom->helpers, matched_dom->sid, rid, &range);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = spawn_dom(ctx, matched_dom, range);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    if (!comp_id(range, rid, _id)) {
        err = IDMAP_ERROR;
        goto done;
    }

    err =  IDMAP_SUCCESS;

done:
    if (range != NULL) {
        ctx->free_func(range->range_id, ctx->alloc_pvt);
    }
    ctx->free_func(range, ctx->alloc_pvt);
    return err;
}

enum idmap_error_code sss_idmap_sid_to_unix(struct sss_idmap_ctx *ctx,
                                            const char *sid,
                                            uint32_t *_id)
{
    struct idmap_domain_info *idmap_domain_info;
    struct idmap_domain_info *matched_dom = NULL;
    size_t dom_len;
    long long rid;

    if (sid == NULL || _id == NULL) {
        return IDMAP_ERROR;
    }

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    idmap_domain_info = ctx->idmap_domain_info;

    if (sss_idmap_sid_is_builtin(sid)) {
        return IDMAP_BUILTIN_SID;
    }

    /* Try primary slices */
    while (idmap_domain_info != NULL) {

        if (is_sid_from_dom(idmap_domain_info->sid, sid, &dom_len)) {

            if (idmap_domain_info->external_mapping == true) {
                return IDMAP_EXTERNAL;
            }

            if (parse_rid(sid, dom_len, &rid) == false) {
                return IDMAP_SID_INVALID;
            }

            if (comp_id(&idmap_domain_info->range_params, rid, _id)) {
                return IDMAP_SUCCESS;
            }

            matched_dom = idmap_domain_info;
        }

        idmap_domain_info = idmap_domain_info->next;
    }

    if (matched_dom != NULL && matched_dom->auto_add_ranges) {
        return add_dom_for_sid(ctx, matched_dom, sid, _id);
    }

    return matched_dom ? IDMAP_NO_RANGE : IDMAP_NO_DOMAIN;
}

enum idmap_error_code sss_idmap_check_sid_unix(struct sss_idmap_ctx *ctx,
                                               const char *sid,
                                               uint32_t id)
{
    struct idmap_domain_info *idmap_domain_info;
    size_t dom_len;
    bool no_range = false;

    if (sid == NULL) {
        return IDMAP_ERROR;
    }

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    if (ctx->idmap_domain_info == NULL) {
        return IDMAP_NO_DOMAIN;
    }

    idmap_domain_info = ctx->idmap_domain_info;

    if (sss_idmap_sid_is_builtin(sid)) {
        return IDMAP_BUILTIN_SID;
    }

    while (idmap_domain_info != NULL) {
        if (idmap_domain_info->sid != NULL) {
            dom_len = strlen(idmap_domain_info->sid);
            if (strlen(sid) > dom_len && sid[dom_len] == '-'
                    && strncmp(sid, idmap_domain_info->sid, dom_len) == 0) {

                if (id >= idmap_domain_info->range_params.min_id
                    && id <= idmap_domain_info->range_params.max_id) {
                    return IDMAP_SUCCESS;
                }

                no_range = true;
            }
        }

        idmap_domain_info = idmap_domain_info->next;
    }

    return no_range ? IDMAP_NO_RANGE : IDMAP_SID_UNKNOWN;
}

static enum idmap_error_code generate_sid(struct sss_idmap_ctx *ctx,
                                          const char *dom_sid,
                                          uint32_t rid,
                                          char **_sid)
{
    char *sid;
    int len;
    int ret;

    len = snprintf(NULL, 0, SID_FMT, dom_sid, rid);
    if (len <= 0 || len > SID_STR_MAX_LEN) {
        return IDMAP_ERROR;
    }

    sid = ctx->alloc_func(len + 1, ctx->alloc_pvt);
    if (sid == NULL) {
        return IDMAP_OUT_OF_MEMORY;
    }

    ret = snprintf(sid, len + 1, SID_FMT, dom_sid, rid);
    if (ret != len) {
        ctx->free_func(sid, ctx->alloc_pvt);
        return IDMAP_ERROR;
    }

    *_sid = sid;
    return IDMAP_SUCCESS;
}

enum idmap_error_code sss_idmap_unix_to_sid(struct sss_idmap_ctx *ctx,
                                            uint32_t id,
                                            char **_sid)
{
    struct idmap_domain_info *idmap_domain_info;
    uint32_t rid;
    enum idmap_error_code err;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    idmap_domain_info = ctx->idmap_domain_info;

    while (idmap_domain_info != NULL) {
        if (id_is_in_range(id, &idmap_domain_info->range_params, &rid)) {

            if (idmap_domain_info->external_mapping == true
                    || idmap_domain_info->sid == NULL) {
                return IDMAP_EXTERNAL;
            }

            return generate_sid(ctx, idmap_domain_info->sid, rid, _sid);
        }

        idmap_domain_info = idmap_domain_info->next;
    }

    /* Check secondary ranges. */
    idmap_domain_info = ctx->idmap_domain_info;
    while (idmap_domain_info != NULL) {

        for (struct idmap_range_params *it = idmap_domain_info->helpers;
             it != NULL;
             it = it->next) {

            if (idmap_domain_info->helpers_owner == false) {
                /* Checking helpers on owner is sufficient. */
                continue;
            }

            if (id_is_in_range(id, it, &rid)) {

                if (idmap_domain_info->external_mapping == true
                    || idmap_domain_info->sid == NULL) {
                    return IDMAP_EXTERNAL;
                }

                err = spawn_dom(ctx, idmap_domain_info, it);
                if (err != IDMAP_SUCCESS) {
                    return err;
                }

                return generate_sid(ctx, idmap_domain_info->sid, rid, _sid);
            }
        }

        idmap_domain_info = idmap_domain_info->next;
    }

    return IDMAP_NO_DOMAIN;
}

enum idmap_error_code sss_idmap_dom_sid_to_unix(struct sss_idmap_ctx *ctx,
                                                struct sss_dom_sid *dom_sid,
                                                uint32_t *id)
{
    enum idmap_error_code err;
    char *sid;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    err = sss_idmap_dom_sid_to_sid(ctx, dom_sid, &sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = sss_idmap_sid_to_unix(ctx, sid, id);

done:
    ctx->free_func(sid, ctx->alloc_pvt);

    return err;
}

enum idmap_error_code sss_idmap_bin_sid_to_unix(struct sss_idmap_ctx *ctx,
                                                uint8_t *bin_sid,
                                                size_t length,
                                                uint32_t *id)
{
    enum idmap_error_code err;
    char *sid;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    err = sss_idmap_bin_sid_to_sid(ctx, bin_sid, length, &sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = sss_idmap_sid_to_unix(ctx, sid, id);

done:
    ctx->free_func(sid, ctx->alloc_pvt);

    return err;
}

enum idmap_error_code sss_idmap_smb_sid_to_unix(struct sss_idmap_ctx *ctx,
                                                struct dom_sid *smb_sid,
                                                uint32_t *id)
{
    enum idmap_error_code err;
    char *sid;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    err = sss_idmap_smb_sid_to_sid(ctx, smb_sid, &sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = sss_idmap_sid_to_unix(ctx, sid, id);

done:
    ctx->free_func(sid, ctx->alloc_pvt);

    return err;
}

enum idmap_error_code sss_idmap_check_dom_sid_to_unix(struct sss_idmap_ctx *ctx,
                                                    struct sss_dom_sid *dom_sid,
                                                    uint32_t id)
{
    enum idmap_error_code err;
    char *sid;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    err = sss_idmap_dom_sid_to_sid(ctx, dom_sid, &sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = sss_idmap_check_sid_unix(ctx, sid, id);

done:
    ctx->free_func(sid, ctx->alloc_pvt);

    return err;
}

enum idmap_error_code sss_idmap_check_bin_sid_unix(struct sss_idmap_ctx *ctx,
                                                   uint8_t *bin_sid,
                                                   size_t length,
                                                   uint32_t id)
{
    enum idmap_error_code err;
    char *sid;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    err = sss_idmap_bin_sid_to_sid(ctx, bin_sid, length, &sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = sss_idmap_check_sid_unix(ctx, sid, id);

done:
    ctx->free_func(sid, ctx->alloc_pvt);

    return err;
}

enum idmap_error_code sss_idmap_check_smb_sid_unix(struct sss_idmap_ctx *ctx,
                                                   struct dom_sid *smb_sid,
                                                   uint32_t id)
{
    enum idmap_error_code err;
    char *sid;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    err = sss_idmap_smb_sid_to_sid(ctx, smb_sid, &sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = sss_idmap_check_sid_unix(ctx, sid, id);

done:
    ctx->free_func(sid, ctx->alloc_pvt);

    return err;
}
enum idmap_error_code sss_idmap_unix_to_dom_sid(struct sss_idmap_ctx *ctx,
                                                uint32_t id,
                                                struct sss_dom_sid **_dom_sid)
{
    enum idmap_error_code err;
    char *sid = NULL;
    struct sss_dom_sid *dom_sid = NULL;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    err = sss_idmap_unix_to_sid(ctx, id, &sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = sss_idmap_sid_to_dom_sid(ctx, sid, &dom_sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    *_dom_sid = dom_sid;
    err = IDMAP_SUCCESS;

done:
    ctx->free_func(sid, ctx->alloc_pvt);
    if (err != IDMAP_SUCCESS) {
        ctx->free_func(dom_sid, ctx->alloc_pvt);
    }

    return err;
}

enum idmap_error_code sss_idmap_unix_to_bin_sid(struct sss_idmap_ctx *ctx,
                                                uint32_t id,
                                                uint8_t **_bin_sid,
                                                size_t *_length)
{
    enum idmap_error_code err;
    char *sid = NULL;
    uint8_t *bin_sid = NULL;
    size_t length;

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    err = sss_idmap_unix_to_sid(ctx, id, &sid);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    err = sss_idmap_sid_to_bin_sid(ctx, sid, &bin_sid, &length);
    if (err != IDMAP_SUCCESS) {
        goto done;
    }

    *_bin_sid = bin_sid;
    *_length = length;
    err = IDMAP_SUCCESS;

done:
    ctx->free_func(sid, ctx->alloc_pvt);
    if (err != IDMAP_SUCCESS) {
        ctx->free_func(bin_sid, ctx->alloc_pvt);
    }

    return err;

}

enum idmap_error_code
sss_idmap_ctx_set_autorid(struct sss_idmap_ctx *ctx, bool use_autorid)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
    ctx->idmap_opts.autorid_mode = use_autorid;
    return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_ctx_set_lower(struct sss_idmap_ctx *ctx, id_t lower)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
    ctx->idmap_opts.idmap_lower = lower;
    return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_ctx_set_upper(struct sss_idmap_ctx *ctx, id_t upper)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
    ctx->idmap_opts.idmap_upper = upper;
    return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_ctx_set_rangesize(struct sss_idmap_ctx *ctx, id_t rangesize)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
    ctx->idmap_opts.rangesize = rangesize;
    return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_ctx_set_extra_slice_init(struct sss_idmap_ctx *ctx,
                                  int extra_slice_init)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
    ctx->idmap_opts.extra_slice_init = extra_slice_init;
    return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_ctx_get_autorid(struct sss_idmap_ctx *ctx, bool *_autorid)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
     *_autorid = ctx->idmap_opts.autorid_mode;
     return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_ctx_get_lower(struct sss_idmap_ctx *ctx, id_t *_lower)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
    *_lower = ctx->idmap_opts.idmap_lower;
    return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_ctx_get_upper(struct sss_idmap_ctx *ctx, id_t *_upper)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
    *_upper = ctx->idmap_opts.idmap_upper;
    return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_ctx_get_rangesize(struct sss_idmap_ctx *ctx, id_t *_rangesize)
{
    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);
    *_rangesize =  ctx->idmap_opts.rangesize;
    return IDMAP_SUCCESS;
}

enum idmap_error_code
sss_idmap_domain_has_algorithmic_mapping(struct sss_idmap_ctx *ctx,
                                         const char *dom_sid,
                                         bool *has_algorithmic_mapping)
{
    struct idmap_domain_info *idmap_domain_info;
    size_t len;
    size_t dom_sid_len;

    if (dom_sid == NULL) {
        return IDMAP_SID_INVALID;
    }

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    if (ctx->idmap_domain_info == NULL) {
        return IDMAP_NO_DOMAIN;
    }

    idmap_domain_info = ctx->idmap_domain_info;

    while (idmap_domain_info != NULL) {
        if (idmap_domain_info->sid != NULL) {
            len = strlen(idmap_domain_info->sid);
            dom_sid_len = strlen(dom_sid);
            if (((dom_sid_len > len && dom_sid[len] == '-')
                        || dom_sid_len == len)
                    && strncmp(dom_sid, idmap_domain_info->sid, len) == 0) {

                *has_algorithmic_mapping = !idmap_domain_info->external_mapping;
                return IDMAP_SUCCESS;

            }
        }

        idmap_domain_info = idmap_domain_info->next;
    }

    return IDMAP_SID_UNKNOWN;
}

enum idmap_error_code
sss_idmap_domain_by_name_has_algorithmic_mapping(struct sss_idmap_ctx *ctx,
                                                 const char *dom_name,
                                                 bool *has_algorithmic_mapping)
{
    struct idmap_domain_info *idmap_domain_info;

    if (dom_name == NULL) {
        return IDMAP_ERROR;
    }

    CHECK_IDMAP_CTX(ctx, IDMAP_CONTEXT_INVALID);

    if (ctx->idmap_domain_info == NULL) {
        return IDMAP_NO_DOMAIN;
    }

    idmap_domain_info = ctx->idmap_domain_info;

    while (idmap_domain_info != NULL) {
        if (idmap_domain_info->name != NULL
                && strcmp(dom_name, idmap_domain_info->name) == 0) {

            *has_algorithmic_mapping = !idmap_domain_info->external_mapping;
            return IDMAP_SUCCESS;
        }

        idmap_domain_info = idmap_domain_info->next;
    }

    return IDMAP_NAME_UNKNOWN;
}
