/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* util/support/plugins.c - Plugin module support functions */
/*
 * Copyright 2006, 2008 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.
 */

#include "k5-platform.h"
#include "k5-plugin.h"
#if USE_DLOPEN
#include <dlfcn.h>
#endif
#include <sys/types.h>
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#if USE_DLOPEN
#ifdef RTLD_GROUP
#define GROUP RTLD_GROUP
#else
#define GROUP 0
#endif
#ifdef RTLD_NODELETE
#define NODELETE RTLD_NODELETE
#else
#define NODELETE 0
#endif
#define PLUGIN_DLOPEN_FLAGS (RTLD_NOW | RTLD_LOCAL | GROUP | NODELETE)
#endif

/*
 * glibc bug 11941, fixed in release 2.25, can cause an assertion failure in
 * dlclose() on process exit.  Our workaround is to leak dlopen() handles
 * (which doesn't typically manifest in leak detection tools because the
 * handles are still reachable via a global table in libdl).  Because we
 * dlopen() with RTLD_NODELETE, we weren't going to unload the plugin objects
 * anyway.
 */
#ifdef __GLIBC_PREREQ
#if ! __GLIBC_PREREQ(2, 25)
#define dlclose(x)
#endif
#endif

#if USE_DLOPEN && USE_CFBUNDLE
#include <CoreFoundation/CoreFoundation.h>

/* Currently CoreFoundation only exists on the Mac so we just use
 * pthreads directly to avoid creating empty function calls on other
 * platforms.  If a thread initializer ever gets created in the common
 * plugin code, move this there */
static pthread_mutex_t krb5int_bundle_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif

#include <stdarg.h>
static void Tprintf (const char *fmt, ...)
{
#ifdef DEBUG
    va_list va;
    va_start (va, fmt);
    vfprintf (stderr, fmt, va);
    va_end (va);
#endif
}

struct plugin_file_handle {
#if USE_DLOPEN
    void *dlhandle;
#endif
#ifdef _WIN32
    HMODULE hinstPlugin;
#endif
#if !defined (USE_DLOPEN) && !defined (_WIN32)
    char dummy;
#endif
};

#ifdef _WIN32
struct dirent {
    long d_ino;                 /* inode (always 1 in WIN32) */
    off_t d_off;                /* offset to this dirent */
    unsigned short d_reclen;    /* length of d_name */
    char d_name[_MAX_FNAME+1];  /* filename (null terminated) */
};

typedef struct {
    intptr_t handle;            /* _findfirst/_findnext handle */
    short offset;               /* offset into directory */
    short finished;             /* 1 if there are not more files */
    struct _finddata_t fileinfo;/* from _findfirst/_findnext */
    char *dir;                  /* the dir we are reading */
    struct dirent dent;         /* the dirent to return */
} DIR;

DIR * opendir(const char *dir)
{
    DIR *dp;
    char *filespec;
    intptr_t handle;
    int index;

    filespec = malloc(strlen(dir) + 2 + 1);
    strcpy(filespec, dir);
    index = strlen(filespec) - 1;
    if (index >= 0 && (filespec[index] == '/' || filespec[index] == '\\'))
        filespec[index] = '\0';
    strcat(filespec, "/*");

    dp = (DIR *)malloc(sizeof(DIR));
    dp->offset = 0;
    dp->finished = 0;
    dp->dir = strdup(dir);

    if ((handle = _findfirst(filespec, &(dp->fileinfo))) < 0) {
        if (errno == ENOENT)
            dp->finished = 1;
        else {
            free(filespec);
            free(dp->dir);
            free(dp);
            return NULL;
        }
    }

    dp->handle = handle;
    free(filespec);

    return dp;
}

struct dirent * readdir(DIR *dp)
{
    if (!dp || dp->finished) return NULL;

    if (dp->offset != 0) {
        if (_findnext(dp->handle, &(dp->fileinfo)) < 0) {
            dp->finished = 1;
            return NULL;
        }
    }
    dp->offset++;

    strncpy(dp->dent.d_name, dp->fileinfo.name, _MAX_FNAME);
    dp->dent.d_ino = 1;
    dp->dent.d_reclen = (unsigned short)strlen(dp->dent.d_name);
    dp->dent.d_off = dp->offset;

    return &(dp->dent);
}

int closedir(DIR *dp)
{
    if (!dp) return 0;
    _findclose(dp->handle);
    free(dp->dir);
    free(dp);

    return 0;
}
#endif

long KRB5_CALLCONV
krb5int_open_plugin (const char *filepath, struct plugin_file_handle **h, struct errinfo *ep)
{
    long err = 0;
    struct plugin_file_handle *htmp = NULL;
    int got_plugin = 0;
#if defined(USE_CFBUNDLE) || defined(_WIN32)
    struct stat statbuf;

    if (!err) {
        if (stat (filepath, &statbuf) < 0) {
            err = errno;
            Tprintf ("stat(%s): %s\n", filepath, strerror (err));
            k5_set_error(ep, err, _("unable to find plugin [%s]: %s"),
                         filepath, strerror(err));
        }
    }
#endif

    if (!err) {
        htmp = calloc (1, sizeof (*htmp)); /* calloc initializes ptrs to NULL */
        if (htmp == NULL) { err = ENOMEM; }
    }

#if USE_DLOPEN
    if (!err
#if USE_CFBUNDLE
                 && ((statbuf.st_mode & S_IFMT) == S_IFREG
                 || (statbuf.st_mode & S_IFMT) == S_IFDIR)
#endif /* USE_CFBUNDLE */
        ) {
        void *handle = NULL;

#if USE_CFBUNDLE
        char executablepath[MAXPATHLEN];

        if ((statbuf.st_mode & S_IFMT) == S_IFDIR) {
            int lock_err = 0;
            CFStringRef pluginString = NULL;
            CFURLRef pluginURL = NULL;
            CFBundleRef pluginBundle = NULL;
            CFURLRef executableURL = NULL;

            /* Lock around CoreFoundation calls since objects are refcounted
             * and the refcounts are not thread-safe.  Using pthreads directly
             * because this code is Mac-specific */
            lock_err = pthread_mutex_lock(&krb5int_bundle_mutex);
            if (lock_err) { err = lock_err; }

            if (!err) {
                pluginString = CFStringCreateWithCString (kCFAllocatorDefault,
                                                          filepath,
                                                          kCFStringEncodingASCII);
                if (pluginString == NULL) { err = ENOMEM; }
            }

            if (!err) {
                pluginURL = CFURLCreateWithFileSystemPath (kCFAllocatorDefault,
                                                           pluginString,
                                                           kCFURLPOSIXPathStyle,
                                                           true);
                if (pluginURL == NULL) { err = ENOMEM; }
            }

            if (!err) {
                pluginBundle = CFBundleCreate (kCFAllocatorDefault, pluginURL);
                if (pluginBundle == NULL) { err = ENOENT; } /* XXX need better error */
            }

            if (!err) {
                executableURL = CFBundleCopyExecutableURL (pluginBundle);
                if (executableURL == NULL) { err = ENOMEM; }
            }

            if (!err) {
                if (!CFURLGetFileSystemRepresentation (executableURL,
                                                       true, /* absolute */
                                                       (UInt8 *)executablepath,
                                                       sizeof (executablepath))) {
                    err = ENOMEM;
                }
            }

            if (!err) {
                /* override the path the caller passed in */
                filepath = executablepath;
            }

            if (executableURL    != NULL) { CFRelease (executableURL); }
            if (pluginBundle     != NULL) { CFRelease (pluginBundle); }
            if (pluginURL        != NULL) { CFRelease (pluginURL); }
            if (pluginString     != NULL) { CFRelease (pluginString); }

            /* unlock after CFRelease calls since they modify refcounts */
            if (!lock_err) { pthread_mutex_unlock (&krb5int_bundle_mutex); }
        }
#endif /* USE_CFBUNDLE */

        if (!err) {
            handle = dlopen(filepath, PLUGIN_DLOPEN_FLAGS);
            if (handle == NULL) {
                const char *e = dlerror();
                if (e == NULL)
                    e = _("unknown failure");
                Tprintf ("dlopen(%s): %s\n", filepath, e);
                err = ENOENT; /* XXX */
                k5_set_error(ep, err, _("unable to load plugin [%s]: %s"),
                             filepath, e);
            }
        }

        if (!err) {
            got_plugin = 1;
            htmp->dlhandle = handle;
            handle = NULL;
        }

        if (handle != NULL) { dlclose (handle); }
    }
#endif /* USE_DLOPEN */

#ifdef _WIN32
    if (!err && (statbuf.st_mode & S_IFMT) == S_IFREG) {
        HMODULE handle = NULL;

        handle = LoadLibrary(filepath);
        if (handle == NULL) {
            Tprintf ("Unable to load dll: %s\n", filepath);
            err = ENOENT; /* XXX */
            k5_set_error(ep, err, _("unable to load DLL [%s]"), filepath);
        }

        if (!err) {
            got_plugin = 1;
            htmp->hinstPlugin = handle;
            handle = NULL;
        }

        if (handle != NULL)
            FreeLibrary(handle);
    }
#endif

    if (!err && !got_plugin) {
        err = ENOENT;  /* no plugin or no way to load plugins */
        k5_set_error(ep, err, _("plugin unavailable: %s"), strerror(err));
    }

    if (!err) {
        *h = htmp;
        htmp = NULL;  /* h takes ownership */
    }

    free(htmp);

    return err;
}

static long
krb5int_get_plugin_sym (struct plugin_file_handle *h,
                        const char *csymname, int isfunc, void **ptr,
                        struct errinfo *ep)
{
    long err = 0;
    void *sym = NULL;

#if USE_DLOPEN
    if (!err && !sym && (h->dlhandle != NULL)) {
        /* XXX Do we need to add a leading "_" to the symbol name on any
           modern platforms?  */
        sym = dlsym (h->dlhandle, csymname);
        if (sym == NULL) {
            const char *e = dlerror (); /* XXX copy and save away */
            if (e == NULL)
                e = "unknown failure";
            Tprintf ("dlsym(%s): %s\n", csymname, e);
            err = ENOENT; /* XXX */
            k5_set_error(ep, err, "%s", e);
        }
    }
#endif

#ifdef _WIN32
    LPVOID lpMsgBuf;
    DWORD dw;

    if (!err && !sym && (h->hinstPlugin != NULL)) {
        sym = GetProcAddress(h->hinstPlugin, csymname);
        if (sym == NULL) {
            const char *e = "unable to get dll symbol"; /* XXX copy and save away */
            Tprintf ("GetProcAddress(%s): %i\n", csymname, GetLastError());
            err = ENOENT; /* XXX */
            k5_set_error(ep, err, "%s", e);

            dw = GetLastError();
            if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
                              FORMAT_MESSAGE_FROM_SYSTEM,
                              NULL,
                              dw,
                              MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                              (LPTSTR) &lpMsgBuf,
                              0, NULL )) {

                fprintf (stderr, "unable to get dll symbol, %s\n", (LPCTSTR)lpMsgBuf);
                LocalFree(lpMsgBuf);
            }
        }
    }
#endif

    if (!err && (sym == NULL)) {
        err = ENOENT;  /* unimplemented */
    }

    if (!err) {
        *ptr = sym;
    }

    return err;
}

long KRB5_CALLCONV
krb5int_get_plugin_data (struct plugin_file_handle *h, const char *csymname,
                         void **ptr, struct errinfo *ep)
{
    return krb5int_get_plugin_sym (h, csymname, 0, ptr, ep);
}

long KRB5_CALLCONV
krb5int_get_plugin_func (struct plugin_file_handle *h, const char *csymname,
                         void (**ptr)(), struct errinfo *ep)
{
    void *dptr = NULL;
    long err = krb5int_get_plugin_sym (h, csymname, 1, &dptr, ep);
    if (!err) {
        /* Cast function pointers to avoid code duplication */
        *ptr = (void (*)()) dptr;
    }
    return err;
}

void KRB5_CALLCONV
krb5int_close_plugin (struct plugin_file_handle *h)
{
#if USE_DLOPEN
    if (h->dlhandle != NULL) { dlclose(h->dlhandle); }
#endif
#ifdef _WIN32
    if (h->hinstPlugin != NULL) { FreeLibrary(h->hinstPlugin); }
#endif
    free (h);
}

/* autoconf docs suggest using this preference order */
#if HAVE_DIRENT_H || USE_DIRENT_H
#include <dirent.h>
#define NAMELEN(D) strlen((D)->d_name)
#else
#ifndef _WIN32
#define dirent direct
#define NAMELEN(D) ((D)->d->namlen)
#else
#define NAMELEN(D) strlen((D)->d_name)
#endif
#if HAVE_SYS_NDIR_H
# include <sys/ndir.h>
#elif HAVE_SYS_DIR_H
# include <sys/dir.h>
#elif HAVE_NDIR_H
# include <ndir.h>
#endif
#endif

static long
krb5int_plugin_file_handle_array_init (struct plugin_file_handle ***harray)
{
    long err = 0;

    *harray = calloc (1, sizeof (**harray)); /* calloc initializes to NULL */
    if (*harray == NULL) { err = ENOMEM; }

    return err;
}

static long
krb5int_plugin_file_handle_array_add (struct plugin_file_handle ***harray, size_t *count,
                                      struct plugin_file_handle *p)
{
    long err = 0;
    struct plugin_file_handle **newharray = NULL;
    size_t newcount = *count + 1;

    newharray = realloc (*harray, ((newcount + 1) * sizeof (**harray))); /* +1 for NULL */
    if (newharray == NULL) {
        err = ENOMEM;
    } else {
        newharray[newcount - 1] = p;
        newharray[newcount] = NULL;
        *count = newcount;
        *harray = newharray;
    }

    return err;
}

static void
krb5int_plugin_file_handle_array_free (struct plugin_file_handle **harray)
{
    if (harray != NULL) {
        int i;
        for (i = 0; harray[i] != NULL; i++) {
            krb5int_close_plugin (harray[i]);
        }
        free (harray);
    }
}

#if TARGET_OS_MAC
#define FILEEXTS { "", ".bundle", ".dylib", ".so", NULL }
#elif defined(_WIN32)
#define FILEEXTS  { "", ".dll", NULL }
#else
#define FILEEXTS  { "", ".so", NULL }
#endif


static void
krb5int_free_plugin_filenames (char **filenames)
{
    if (filenames != NULL) {
        int i;
        for (i = 0; filenames[i] != NULL; i++) {
            free (filenames[i]);
        }
        free (filenames);
    }
}


static long
krb5int_get_plugin_filenames (const char * const *filebases, char ***filenames)
{
    long err = 0;
    static const char *const fileexts[] = FILEEXTS;
    char **tempnames = NULL;
    size_t bases_count = 0;
    size_t exts_count = 0;
    size_t i;

    if (!filebases) { err = EINVAL; }
    if (!filenames) { err = EINVAL; }

    if (!err) {
        for (i = 0; filebases[i]; i++) { bases_count++; }
        for (i = 0; fileexts[i]; i++) { exts_count++; }
        tempnames = calloc ((bases_count * exts_count)+1, sizeof (char *));
        if (!tempnames) { err = ENOMEM; }
    }

    if (!err) {
        size_t j;
        for (i = 0; !err && filebases[i]; i++) {
            for (j = 0; !err && fileexts[j]; j++) {
                if (asprintf(&tempnames[(i*exts_count)+j], "%s%s",
                             filebases[i], fileexts[j]) < 0) {
                    tempnames[(i*exts_count)+j] = NULL;
                    err = ENOMEM;
                }
            }
        }
        tempnames[bases_count * exts_count] = NULL; /* NUL-terminate */
    }

    if (!err) {
        *filenames = tempnames;
        tempnames = NULL;
    }

    krb5int_free_plugin_filenames(tempnames);

    return err;
}


/* Takes a NULL-terminated list of directories.  If filebases is NULL, filebases is ignored
 * all plugins in the directories are loaded.  If filebases is a NULL-terminated array of names,
 * only plugins in the directories with those name (plus any platform extension) are loaded. */

long KRB5_CALLCONV
krb5int_open_plugin_dirs (const char * const *dirnames,
                          const char * const *filebases,
                          struct plugin_dir_handle *dirhandle,
                          struct errinfo *ep)
{
    long err = 0;
    struct plugin_file_handle **h = NULL;
    size_t count = 0;
    char **filenames = NULL;
    int i;

    if (!err) {
        err = krb5int_plugin_file_handle_array_init (&h);
    }

    if (!err && (filebases != NULL)) {
        err = krb5int_get_plugin_filenames (filebases, &filenames);
    }

    for (i = 0; !err && dirnames[i] != NULL; i++) {
        if (filenames != NULL) {
            /* load plugins with names from filenames from each directory */
            int j;

            for (j = 0; !err && filenames[j] != NULL; j++) {
                struct plugin_file_handle *handle = NULL;
                char *filepath = NULL;

                if (!err) {
                    if (asprintf(&filepath, "%s/%s", dirnames[i], filenames[j]) < 0) {
                        filepath = NULL;
                        err = ENOMEM;
                    }
                }

                if (!err && krb5int_open_plugin(filepath, &handle, ep) == 0) {
                    err = krb5int_plugin_file_handle_array_add (&h, &count, handle);
                    if (!err)
                        handle = NULL; /* h takes ownership */
                }

                free(filepath);
                if (handle   != NULL) { krb5int_close_plugin (handle); }
            }
        } else {
            /* load all plugins in each directory */
            DIR *dir = opendir (dirnames[i]);

            while (dir != NULL && !err) {
                struct dirent *d = NULL;
                char *filepath = NULL;
                struct plugin_file_handle *handle = NULL;

                d = readdir (dir);
                if (d == NULL) { break; }

                if ((strcmp (d->d_name, ".") == 0) ||
                    (strcmp (d->d_name, "..") == 0)) {
                    continue;
                }

                if (!err) {
                    int len = NAMELEN (d);
                    if (asprintf(&filepath, "%s/%*s", dirnames[i], len, d->d_name) < 0) {
                        filepath = NULL;
                        err = ENOMEM;
                    }
                }

                if (!err) {
                    if (krb5int_open_plugin (filepath, &handle, ep) == 0) {
                        err = krb5int_plugin_file_handle_array_add (&h, &count, handle);
                        if (!err) { handle = NULL; }  /* h takes ownership */
                    }
                }

                free(filepath);
                if (handle    != NULL) { krb5int_close_plugin (handle); }
            }

            if (dir != NULL) { closedir (dir); }
        }
    }

    if (err == ENOENT) {
        err = 0;  /* ran out of plugins -- do nothing */
    }

    if (!err) {
        dirhandle->files = h;
        h = NULL;  /* dirhandle->files takes ownership */
    }

    if (filenames != NULL) { krb5int_free_plugin_filenames (filenames); }
    if (h         != NULL) { krb5int_plugin_file_handle_array_free (h); }

    return err;
}

void KRB5_CALLCONV
krb5int_close_plugin_dirs (struct plugin_dir_handle *dirhandle)
{
    if (dirhandle->files != NULL) {
        int i;
        for (i = 0; dirhandle->files[i] != NULL; i++) {
            krb5int_close_plugin (dirhandle->files[i]);
        }
        free (dirhandle->files);
        dirhandle->files = NULL;
    }
}

void KRB5_CALLCONV
krb5int_free_plugin_dir_data (void **ptrs)
{
    /* Nothing special to be done per pointer.  */
    free(ptrs);
}

long KRB5_CALLCONV
krb5int_get_plugin_dir_data (struct plugin_dir_handle *dirhandle,
                             const char *symname,
                             void ***ptrs,
                             struct errinfo *ep)
{
    long err = 0;
    void **p = NULL;
    size_t count = 0;

    /* XXX Do we need to add a leading "_" to the symbol name on any
       modern platforms?  */

    Tprintf("get_plugin_data_sym(%s)\n", symname);

    if (!err) {
        p = calloc (1, sizeof (*p)); /* calloc initializes to NULL */
        if (p == NULL) { err = ENOMEM; }
    }

    if (!err && (dirhandle != NULL) && (dirhandle->files != NULL)) {
        int i = 0;

        for (i = 0; !err && (dirhandle->files[i] != NULL); i++) {
            void *sym = NULL;

            if (krb5int_get_plugin_data (dirhandle->files[i], symname, &sym, ep) == 0) {
                void **newp = NULL;

                count++;
                newp = realloc (p, ((count + 1) * sizeof (*p))); /* +1 for NULL */
                if (newp == NULL) {
                    err = ENOMEM;
                } else {
                    p = newp;
                    p[count - 1] = sym;
                    p[count] = NULL;
                }
            }
        }
    }

    if (!err) {
        *ptrs = p;
        p = NULL; /* ptrs takes ownership */
    }

    free(p);

    return err;
}

void KRB5_CALLCONV
krb5int_free_plugin_dir_func (void (**ptrs)(void))
{
    /* Nothing special to be done per pointer.  */
    free(ptrs);
}

long KRB5_CALLCONV
krb5int_get_plugin_dir_func (struct plugin_dir_handle *dirhandle,
                             const char *symname,
                             void (***ptrs)(void),
                             struct errinfo *ep)
{
    long err = 0;
    void (**p)() = NULL;
    size_t count = 0;

    /* XXX Do we need to add a leading "_" to the symbol name on any
       modern platforms?  */

    Tprintf("get_plugin_data_sym(%s)\n", symname);

    if (!err) {
        p = calloc (1, sizeof (*p)); /* calloc initializes to NULL */
        if (p == NULL) { err = ENOMEM; }
    }

    if (!err && (dirhandle != NULL) && (dirhandle->files != NULL)) {
        int i = 0;

        for (i = 0; !err && (dirhandle->files[i] != NULL); i++) {
            void (*sym)() = NULL;

            if (krb5int_get_plugin_func (dirhandle->files[i], symname, &sym, ep) == 0) {
                void (**newp)() = NULL;

                count++;
                newp = realloc (p, ((count + 1) * sizeof (*p))); /* +1 for NULL */
                if (newp == NULL) {
                    err = ENOMEM;
                } else {
                    p = newp;
                    p[count - 1] = sym;
                    p[count] = NULL;
                }
            }
        }
    }

    if (!err) {
        *ptrs = p;
        p = NULL; /* ptrs takes ownership */
    }

    free(p);

    return err;
}
