/*
 * Launcher program for OS X application bundles of PuTTY.
 */

/*
 * The 'gtk-mac-bundler' utility arranges to build an OS X application
 * bundle containing a program compiled against the Quartz GTK
 * backend. It does this by including all the necessary GTK shared
 * libraries and data files inside the bundle as well as the binary.
 *
 * But the GTK program won't start up unless all those shared
 * libraries etc are already pointed to by environment variables like
 * DYLD_LIBRARY_PATH, which won't be set up when the bundle is
 * launched.
 *
 * Hence, gtk-mac-bundler expects to install the program in the bundle
 * under a name like 'Contents/MacOS/Program-bin'; and the file called
 * 'Contents/MacOS/Program', which is the one actually executed when
 * the bundle is launched, is a wrapper script that sets up the
 * environment before running the actual GTK-using program.
 *
 * In our case, however, that's not good enough. pterm will want to
 * launch subprocesses with general-purpose shell sessions in them,
 * and those subprocesses _won't_ want the random stuff dumped in the
 * environment by the gtk-mac-bundler standard wrapper script. So I
 * have to provide my own wrapper, which has a more complicated job:
 * not only setting up the environment for the GTK app, but also
 * preserving all details of the _previous_ environment, so that when
 * pterm forks off a subprocess to run in a terminal session, it can
 * restore the environment that was in force before the wrapper
 * started messing about. This source file implements that wrapper,
 * and does it in C so as to make string processing more reliable and
 * less annoying.
 *
 * My strategy for saving the old environment is to pick a prefix
 * that's unused by anything currently in the environment; let's
 * suppose it's "P" for this discussion. Any environment variable I
 * overwrite, say "VAR", I will either set "PsVAR=old value", or
 * "PuVAR=" ("s" and "u" for "set" and "unset"). Then I pass the
 * prefix itself as a command-line argument to the main GTK
 * application binary, which then knows how to restore the original
 * environment in pterm subprocesses.
 */

#include <assert.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#ifndef __APPLE__
/* When we're not compiling for OS X, it's easier to just turn this
 * program into a trivial hello-world by ifdef in the source than it
 * is to remove it in the makefile edifice. */
int main(int argc, char **argv)
{
    fprintf(stderr, "launcher does nothing on non-OSX platforms\n");
    return 1;
}
#else /* __APPLE__ */

#include <unistd.h>
#include <libgen.h>
#include <mach-o/dyld.h>

/* ----------------------------------------------------------------------
 * Find an alphabetic prefix unused by any environment variable name.
 */

/*
 * This linked-list based system is a bit overkill, but I enjoy an
 * algorithmic challenge. We essentially do an incremental radix sort
 * of all the existing environment variable names: initially divide
 * them into 26 buckets by their first letter (discarding those that
 * don't have a letter at that position), then subdivide each bucket
 * in turn into 26 sub-buckets, and so on. We maintain each bucket as
 * a linked list, and link their heads together into a secondary list
 * that functions as a queue (meaning that we go breadth-first,
 * processing all the buckets of a given depth before moving on to the
 * next depth down). At any stage, if we find one of our 26
 * sub-buckets is empty, that's our unused prefix.
 *
 * The running time is O(number of strings * length of output), and I
 * doubt it's possible to do better.
 */

#define FANOUT 26
int char_index(int ch)
{
    if (ch >= 'A' && ch <= 'Z')
        return ch - 'A';
    else if (ch >= 'a' && ch <= 'z')
        return ch - 'a';
    else
        return -1;
}

struct bucket {
    int prefixlen;
    struct bucket *next_bucket;
    struct node *first_node;
};

struct node {
    const char *string;
    int len, prefixlen;
    struct node *next;
};

struct node *new_node(struct node *prev_head, const char *string, int len)
{
    struct node *ret = (struct node *)malloc(sizeof(struct node));

    if (!ret) {
        fprintf(stderr, "out of memory\n");
        exit(1);
    }

    ret->next = prev_head;
    ret->string = string;
    ret->len = len;

    return ret;
}

char *get_unused_env_prefix(void)
{
    struct bucket *qhead, *qtail;
    extern char **environ;
    char **e;

    qhead = (struct bucket *)malloc(sizeof(struct bucket));
    if (!qhead) {
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    qhead->prefixlen = 0;
    for (e = environ; *e; e++)
        qhead->first_node = new_node(qhead->first_node, *e, strcspn(*e, "="));

    qtail = qhead;
    while (1) {
        struct bucket *buckets[FANOUT];
        struct node *bucketnode;
        int i, index;

        for (i = 0; i < FANOUT; i++) {
            buckets[i] = (struct bucket *)malloc(sizeof(struct bucket));
            if (!buckets[i]) {
                fprintf(stderr, "out of memory\n");
                exit(1);
            }
            buckets[i]->prefixlen = qhead->prefixlen + 1;
            qtail->next_bucket = buckets[i];
            qtail = buckets[i];
        }
        qtail->next_bucket = NULL;

        bucketnode = qhead->first_node;
        while (bucketnode) {
            struct node *node = bucketnode;
            bucketnode = bucketnode->next;

            if (node->len <= qhead->prefixlen)
                continue;
            index = char_index(node->string[qhead->prefixlen]);
            if (!(index >= 0 && index < FANOUT))
                continue;
            node->prefixlen++;
            node->next = buckets[index]->first_node;
            buckets[index]->first_node = node;
        }

        for (i = 0; i < FANOUT; i++) {
            if (!buckets[i]->first_node) {
                char *ret = malloc(qhead->prefixlen + 2);
                if (!ret) {
                    fprintf(stderr, "out of memory\n");
                    exit(1);
                }
                memcpy(ret, qhead->first_node->string, qhead->prefixlen);
                ret[qhead->prefixlen] = i + 'A';
                ret[qhead->prefixlen + 1] = '\0';

                /* This would be where we freed everything, if we
                 * didn't know it didn't matter because we were
                 * imminently going to exec another program */
                return ret;
            }
        }

        qhead = qhead->next_bucket;
    }
}

/* ----------------------------------------------------------------------
 * Get the pathname of this executable, so we can locate the rest of
 * the app bundle relative to it.
 */

/*
 * There are several ways to try to retrieve the pathname to the
 * running executable:
 *
 * (a) Declare main() as taking four arguments int main(int argc, char
 * **argv, char **envp, char **apple); and look at apple[0].
 *
 * (b) Use sysctl(KERN_PROCARGS) to get the process arguments for the
 * current pid. This involves two steps:
 *  - sysctl(mib, 2, &argmax, &argmax_size, NULL, 0)
 *     + mib is an array[2] of int containing
 *       { CTL_KERN, KERN_ARGMAX }
 *     + argmax is an int
 *     + argmax_size is a size_t initialised to sizeof(argmax)
 *     + returns in argmax the amount of memory you need for the next
 *       call.
 *  - sysctl(mib, 3, procargs, &procargs_size, NULL, 0)
 *     + mib is an array[3] of int containing
 *       { CTL_KERN, KERN_PROCARGS, current pid }
 *     + procargs is a buffer of size 'argmax'
 *     + procargs_size is a size_t initialised to argmax
 *     + returns in the procargs buffer a collection of
 *       zero-terminated strings of which the first is the program
 *       name.
 *
 * (c) Call _NSGetExecutablePath, once to find out the needed buffer
 * size and again to fetch the actual path.
 *
 * (d) Use Objective-C and Cocoa and call
 * [[[NSProcessInfo processInfo] arguments] objectAtIndex: 0].
 *
 * So, how do those work in various cases? Experiments show:
 *
 *  - if you run the program as 'binary' (or whatever you called it)
 *    and rely on the shell to search your PATH, all four methods
 *    return a sensible-looking absolute pathname.
 *
 *  - if you run the program as './binary', (a) and (b) return just
 *    "./binary", which has a particularly bad race condition if you
 *    try to convert it into an absolute pathname using realpath(3).
 *    (c) returns "/full/path/to/./binary", which still needs
 *    realpath(3)ing to get rid of that ".", but at least it's
 *    _trying_ to be fully qualified. (d) returns
 *    "/full/path/to/binary" - full marks!
 *     + Similar applies if you run it via a more interesting relative
 *       path such as one with a ".." in: (c) gives you an absolute
 *       path containing a ".." element, whereas (d) has sorted that
 *       out.
 *
 *  - if you run the program via a path with a symlink on, _none_ of
 *    these options successfully returns a path without the symlink.
 *
 * That last point suggests that even (d) is not a perfect solution on
 * its own, and you'll have to realpath() whatever you get back from
 * it regardless.
 *
 * And (d) is extra inconvenient because it returns an NSString, which
 * is implicitly Unicode, so it's not clear how you turn that back
 * into a char * representing a correct Unix pathname (what charset
 * should you interpret it in?). Also because you have to bring in all
 * of ObjC and Cocoa, which for a low-level Unix API client like this
 * seems like overkill.
 *
 * So my conclusion is that (c) is most practical for these purposes.
 */

char *get_program_path(void)
{
    char *our_path;
    uint32_t pathlen = 0;
    _NSGetExecutablePath(NULL, &pathlen);
    our_path = malloc(pathlen);
    if (!our_path) {
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    if (_NSGetExecutablePath(our_path, &pathlen)) {
        fprintf(stderr, "unable to get launcher executable path\n");
        exit(1);
    }

    /* OS X guarantees to malloc the return value if we pass NULL */
    char *our_real_path = realpath(our_path, NULL);
    if (!our_real_path) {
        fprintf(stderr, "realpath failed\n");
        exit(1);
    }

    free(our_path);
    return our_real_path;
}

/* ----------------------------------------------------------------------
 * Wrapper on dirname(3) which mallocs its return value to whatever
 * size is needed.
 */

char *dirname_wrapper(const char *path)
{
    char *path_copy = malloc(strlen(path) + 1);
    if (!path_copy) {
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    strcpy(path_copy, path);
    char *ret_orig = dirname(path_copy);
    char *ret = malloc(strlen(ret_orig) + 1);
    if (!ret) {
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    strcpy(ret, ret_orig);
    free(path_copy);
    return ret;
}

/* ----------------------------------------------------------------------
 * mallocing string concatenation function.
 */

char *alloc_cat(const char *str1, const char *str2)
{
    int len1 = strlen(str1), len2 = strlen(str2);
    char *ret = malloc(len1 + len2 + 1);
    if (!ret) {
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    strcpy(ret, str1);
    strcpy(ret + len1, str2);
    return ret;
}

/* ----------------------------------------------------------------------
 * Overwrite an environment variable, preserving the old one for the
 * real app to restore.
 */
char *prefix, *prefixset, *prefixunset;
void overwrite_env(const char *name, const char *value)
{
    const char *oldvalue = getenv(name);
    if (oldvalue) {
        setenv(alloc_cat(prefixset, name), oldvalue, 1);
    } else {
        setenv(alloc_cat(prefixunset, name), "", 1);
    }
    if (value)
        setenv(name, value, 1);
    else
        unsetenv(name);
}

/* ----------------------------------------------------------------------
 * Main program.
 */

int main(int argc, char **argv)
{
    prefix = get_unused_env_prefix();
    prefixset = alloc_cat(prefix, "s");
    prefixunset = alloc_cat(prefix, "u");

    char *prog_path = get_program_path(); // <bundle>/Contents/MacOS/<filename>
    char *macos = dirname_wrapper(prog_path); // <bundle>/Contents/MacOS
    char *contents = dirname_wrapper(macos);  // <bundle>/Contents
//    char *bundle = dirname_wrapper(contents); // <bundle>
    char *resources = alloc_cat(contents, "/Resources");
//    char *bin = alloc_cat(resources, "/bin");
    char *etc = alloc_cat(resources, "/etc");
    char *lib = alloc_cat(resources, "/lib");
    char *share = alloc_cat(resources, "/share");
    char *xdg = alloc_cat(etc, "/xdg");
//    char *gtkrc = alloc_cat(etc, "/gtk-2.0/gtkrc");
    char *locale = alloc_cat(share, "/locale");
    char *realbin = alloc_cat(prog_path, "-bin");

    overwrite_env("DYLD_LIBRARY_PATH", lib);
    overwrite_env("XDG_CONFIG_DIRS", xdg);
    overwrite_env("XDG_DATA_DIRS", share);
    overwrite_env("GTK_DATA_PREFIX", resources);
    overwrite_env("GTK_EXE_PREFIX", resources);
    overwrite_env("GTK_PATH", resources);
    overwrite_env("PANGO_LIBDIR", lib);
    overwrite_env("PANGO_SYSCONFDIR", etc);
    overwrite_env("I18NDIR", locale);
    overwrite_env("LANG", NULL);
    overwrite_env("LC_MESSAGES", NULL);
    overwrite_env("LC_MONETARY", NULL);
    overwrite_env("LC_COLLATE", NULL);

    char **new_argv = malloc((argc + 16) * sizeof(const char *));
    if (!new_argv) {
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    int j = 0;
    new_argv[j++] = realbin;
    {
        int i = 1;
        if (i < argc && !strncmp(argv[i], "-psn_", 5))
            i++;

        for (; i < argc; i++)
            new_argv[j++] = argv[i];
    }
    new_argv[j++] = prefix;
    new_argv[j++] = NULL;

    execv(realbin, new_argv);
    perror("execv");
    free(new_argv);
    free(contents);
    free(macos);
    return 127;
}

#endif /* __APPLE__ */
