/*
 * lib/krb5/ccache/cccursor.c
 *
 * Copyright 2006 by the Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * Export of this software from the United States of America may
 *   require a specific license from the United States Government.
 *   It is the responsibility of any person or organization contemplating
 *   export to obtain such a license before exporting.
 * 
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 *
 * cursor for sequential traversal of ccaches
 */

#include "k5-int.h"

#include <assert.h>

#define CCCURSOR_CONTEXT 1
#define CCCURSOR_ENV 2
#define CCCURSOR_OS 3
#define CCCURSOR_PERTYPE 4

#define NFULLNAMES 3

/* Prefix and residual parts of a full ccache name. */
struct cc_fullname {
    char *pfx;
    char *res;
};

struct _krb5_cccol_cursor {
    int pos;
    krb5_cc_typecursor typecursor;
    const krb5_cc_ops *ops;
    krb5_cc_ptcursor ptcursor;
    int cur_fullname;
    struct cc_fullname fullnames[NFULLNAMES]; /* previously seen ccaches */
};
/* typedef of krb5_cccol_cursor is in krb5.h */

static int cccol_already(krb5_context, krb5_cccol_cursor, krb5_ccache *);

static int cccol_cmpname(const char *, const char *, struct cc_fullname *);

static krb5_error_code
cccol_do_resolve(krb5_context, krb5_cccol_cursor, const char *, krb5_ccache *);

static krb5_error_code
cccol_pertype_next(krb5_context, krb5_cccol_cursor, krb5_ccache *);

krb5_error_code KRB5_CALLCONV
krb5_cccol_cursor_new(
    krb5_context context,
    krb5_cccol_cursor *cursor)
{
    krb5_error_code ret = 0;
    krb5_cccol_cursor n = NULL;
    int i;

    *cursor = NULL;
    n = malloc(sizeof(*n));
    if (n == NULL)
	return ENOMEM;

    n->pos = CCCURSOR_CONTEXT;
    n->typecursor = NULL;
    n->ptcursor = NULL;
    n->ops = NULL;

    for (i = 0; i < NFULLNAMES; i++) {
	n->fullnames[i].pfx = n->fullnames[i].res = NULL;
    }
    n->cur_fullname = 0;
    ret = krb5int_cc_typecursor_new(context, &n->typecursor);
    if (ret)
	goto errout;

    do {
	/* Find first backend with ptcursor functionality. */
	ret = krb5int_cc_typecursor_next(context, n->typecursor, &n->ops);
	if (ret || n->ops == NULL)
	    goto errout;
    } while (n->ops->ptcursor_new == NULL);

    ret = n->ops->ptcursor_new(context, &n->ptcursor);
    if (ret)
	goto errout;

errout:
    if (ret) {
	krb5_cccol_cursor_free(context, &n);
    }
    *cursor = n;
    return ret;
}

krb5_error_code KRB5_CALLCONV
krb5_cccol_cursor_next(
    krb5_context context,
    krb5_cccol_cursor cursor,
    krb5_ccache *ccache)
{
    krb5_error_code ret = 0;
    char *name;
    krb5_os_context os_ctx = NULL;

    *ccache = NULL;
    os_ctx = context->os_context;

    switch (cursor->pos) {
    case CCCURSOR_CONTEXT:
	name = os_ctx->default_ccname;
	if (name != NULL) {
	    cursor->pos = CCCURSOR_ENV;
	    ret = cccol_do_resolve(context, cursor, name, ccache);
	    if (ret)
		goto errout;
	    if (*ccache != NULL)
		break;
	}
	/* fall through */
    case CCCURSOR_ENV:
	name = getenv(KRB5_ENV_CCNAME);
	if (name != NULL) {
	    cursor->pos = CCCURSOR_OS;
	    ret = cccol_do_resolve(context, cursor, name, ccache);
	    if (ret)
		goto errout;
	    if (*ccache != NULL)
		break;
	}
	/* fall through */
    case CCCURSOR_OS:
	ret = krb5int_cc_os_default_name(context, &name);
	if (ret) goto errout;
	if (name != NULL) {
	    cursor->pos = CCCURSOR_PERTYPE;
	    ret = cccol_do_resolve(context, cursor, name, ccache);
	    free(name);
	    if (ret)
		goto errout;
	    if (*ccache != NULL)
		break;
	}
	/* fall through */
    case CCCURSOR_PERTYPE:
	cursor->pos = CCCURSOR_PERTYPE;
	do {
	    ret = cccol_pertype_next(context, cursor, ccache);
	    if (ret)
		goto errout;
	} while (cccol_already(context, cursor, ccache));
	break;
    }
errout:
    return ret;
}

krb5_error_code KRB5_CALLCONV
krb5_cccol_cursor_free(
    krb5_context context,
    krb5_cccol_cursor *cursor)
{
    krb5_cccol_cursor c = *cursor;
    int i;

    if (c == NULL)
	return 0;

    for (i = 0; i < NFULLNAMES; i++) {
	if (c->fullnames[i].pfx != NULL)
	    free(c->fullnames[i].pfx);
	if (c->fullnames[i].res != NULL)
	    free(c->fullnames[i].res);
    }
    if (c->ptcursor != NULL)
	c->ops->ptcursor_free(context, &c->ptcursor);
    if (c->typecursor != NULL)
	krb5int_cc_typecursor_free(context, &c->typecursor);
    free(c);

    *cursor = NULL;
    return 0;
}

/*
 * Determine if a ccache from a per-type cursor was already one of the
 * higher-priority defaults.
 */
static int
cccol_already(
    krb5_context context,
    krb5_cccol_cursor c,
    krb5_ccache *ccache)
{
    const char *name = NULL, *prefix = NULL;
    int i;

    if (*ccache == NULL)
	return 0;
    name = krb5_cc_get_name(context, *ccache);
    if (name == NULL)
	return 0;
    prefix = krb5_cc_get_type(context, *ccache);

    assert(c->cur_fullname < NFULLNAMES);
    for (i = 0; i < c->cur_fullname; i++) {
	if (cccol_cmpname(prefix, name, &c->fullnames[i])) {
	    krb5_cc_close(context, *ccache);
	    *ccache = NULL;
	    return 1;
	}
    }
    return 0;
}

/*
 * Compare {prefix, name} against a cc_fullname.
 */
static int
cccol_cmpname(
    const char *prefix,
    const char *name,
    struct cc_fullname *fullname)
{
    if (fullname->pfx == NULL || fullname->res == NULL)
	return 0;
    if (strcmp(prefix, fullname->pfx))
	return 0;
    if (strcmp(name, fullname->res))
	return 0;

    return 1;
}

/*
 * Resolve one of the high-precedence ccaches, and cache its full name
 * {prefix, residual} for exclusion when doing per-type ccache
 * iteration.  Also check to see if we've already seen the ccache
 * name we're given.
 */
static krb5_error_code
cccol_do_resolve(
    krb5_context context,
    krb5_cccol_cursor cursor,
    const char *name,
    krb5_ccache *ccache)
{
    krb5_error_code ret = 0;
    struct cc_fullname *fullname;

    assert(cursor->cur_fullname < NFULLNAMES);
    ret = krb5_cc_resolve(context, name, ccache);
    if (ret)
	return ret;

    if (cccol_already(context, cursor, ccache))
	return 0;

    fullname = &cursor->fullnames[cursor->cur_fullname];
    fullname->pfx = strdup(krb5_cc_get_type(context, *ccache));
    fullname->res = strdup(krb5_cc_get_name(context, *ccache));
    cursor->cur_fullname++;
    return ret;
}

/*
 * Find next ccache in current backend, iterating through backends if
 * ccache list of the current backend is exhausted.
 */
static krb5_error_code
cccol_pertype_next(
    krb5_context context,
    krb5_cccol_cursor cursor,
    krb5_ccache *ccache)
{
    krb5_error_code ret = 0;

    *ccache = NULL;

    /* Are we out of backends? */
    if (cursor->ops == NULL)
	return 0;
    /*
     * Loop in case there are multiple backends with empty ccache
     * lists.
     */
    while (*ccache == NULL) {
	ret = cursor->ops->ptcursor_next(context, cursor->ptcursor, ccache);
	if (ret)
	    goto errout;
	if (*ccache != NULL)
	    return 0;

	ret = cursor->ops->ptcursor_free(context, &cursor->ptcursor);
	if (ret)
	    goto errout;

	do {
	    /* Find first backend with ptcursor functionality. */
	    ret = krb5int_cc_typecursor_next(context, cursor->typecursor,
					     &cursor->ops);
	    if (ret)
		goto errout;
	    if (cursor->ops == NULL)
		return 0;
	} while (cursor->ops->ptcursor_new == NULL);

	ret = cursor->ops->ptcursor_new(context, &cursor->ptcursor);
	if (ret)
	    goto errout;
    }
errout:
    return ret;
}
