mojosetup.c
author Ryan C. Gordon <icculus@icculus.org>
Fri, 17 Dec 2010 20:43:41 -0500
changeset 802 c18c8fc916dd
parent 753 9f3500cd1f11
child 820 6f236a9ea4c1
permissions -rw-r--r--
Changed panic() messages for startup failures.

The Lua error is extremely unhelpful, since it's what you get if your
download is broken/incomplete, since it'll fail to find the appended .zip
file with the Lua scripts. The new error points out this possibility
directly.
/**
 * MojoSetup; a portable, flexible installation application.
 *
 * Please see the file LICENSE.txt in the source's root directory.
 *
 *  This file written by Ryan C. Gordon.
 */

#include <stdarg.h>

#include "universal.h"
#include "platform.h"
#include "gui.h"
#include "lua_glue.h"
#include "fileio.h"

#define TEST_LAUNCH_BROWSER_CODE 0
int MojoSetup_testLaunchBrowserCode(int argc, char **argv);

#define TEST_ARCHIVE_CODE 0
int MojoSetup_testArchiveCode(int argc, char **argv);

#define TEST_NETWORK_CODE 0
int MojoSetup_testNetworkCode(int argc, char **argv);


uint8 scratchbuf_128k[128 * 1024];
MojoSetupEntryPoints GEntryPoints =
{
    xmalloc,
    xrealloc,
    xstrdup,
    xstrncpy,
    translate,
    logWarning,
    logError,
    logInfo,
    logDebug,
    format,
    numstr,
    MojoPlatform_ticks,
    utf8codepoint,
    utf8len,
    splitText,
    isValidProductKey,
};

int GArgc = 0;
const char **GArgv = NULL;

static char *crashedmsg = NULL;
static char *termedmsg = NULL;

void MojoSetup_crashed(void)
{
    if (crashedmsg == NULL)
        panic("BUG: crash at startup");
    else
        fatal(crashedmsg);
} // MojoSetup_crash


void MojoSetup_terminated(void)
{
    if (termedmsg == NULL)  // no translation yet.
        panic("The installer has been stopped by the system.");
    else
        fatal(termedmsg);
} // MojoSetup_crash


#if !SUPPORT_MULTIARCH
#define trySwitchBinaries()
#else
static void trySwitchBinary(MojoArchive *ar)
{
    MojoInput *io = ar->openCurrentEntry(ar);
    if (io != NULL)
    {
        const uint32 imglen = (uint32) io->length(io);
        uint8 *img = (uint8 *) xmalloc(imglen);
        const uint32 br = io->read(io, img, imglen);
        io->close(io);
        if (br == imglen)
        {
            logInfo("Switching binary with '%0'...", ar->prevEnum.filename);
            MojoPlatform_switchBin(img, imglen);  // no return on success.
            logError("...Switch binary failed.");
        } // if
        free(img);
    } // if
} // trySwitchBinary


static void trySwitchBinaries(void)
{
    if (cmdlinestr("nobinswitch", "MOJOSETUP_NOBINSWITCH", NULL) != NULL)
        return;  // we are already switched or the user is preventing it.

    setenv("MOJOSETUP_NOBINSWITCH", "1", 1);
    setenv("MOJOSETUP_BASE", GBaseArchivePath, 1);

    if (GBaseArchive->enumerate(GBaseArchive))
    {
        const MojoArchiveEntry *entinfo;
        while ((entinfo = GBaseArchive->enumNext(GBaseArchive)) != NULL)
        {
            if (entinfo->type != MOJOARCHIVE_ENTRY_FILE)
                continue;

            if (strncmp(entinfo->filename, "arch/", 5) != 0)
                continue;

            trySwitchBinary(GBaseArchive);
        } // while
    } // if
    
} // trySwitchBinaries
#endif


static boolean trySpawnTerminalGui(void)
{
    if (cmdlinestr("notermspawn", "MOJOSETUP_NOTERMSPAWN", NULL) != NULL)
        return false;  // we already spawned or the user is preventing it.

    if (MojoPlatform_istty())  // maybe we can spawn a terminal for stdio?
        return false;  // We're a terminal already, no need to spawn one.

    logInfo("No usable GUI found. Trying to spawn a terminal...");
    if (!MojoPlatform_spawnTerminal())
    {
        logError("...Terminal spawning failed.");
        return false;
    } // if

    assert(MojoPlatform_istty());
    return (MojoGui_initGuiPlugin() != NULL);
} // trySpawnTerminalGui


static boolean initEverything(void)
{
    MojoLog_initLogging();

    logInfo("MojoSetup starting up...");

    // We have to panic on errors until the GUI is ready. Try to make things
    //  "succeed" unless they are catastrophic, and report problems later.

    // Start with the base archive work, since it might have GUI plugins.
    //  None of these panic() calls are localized, since localization isn't
    //  functional until MojoLua_initLua() succeeds.
    if (!MojoArchive_initBaseArchive())
        panic("Initial setup failed. Cannot continue.");

    trySwitchBinaries();  // may not return.

    if (!MojoGui_initGuiPlugin())
    {
        // This could terminate the process (and relaunch).
        if (!trySpawnTerminalGui())
            panic("Failed to start GUI. Is your download incomplete or corrupt?");
    } // if

    else if (!MojoLua_initLua())
    {
        // (...but if you're the developer: are your files in the wrong place?)
        panic("Failed to start. Is your download incomplete or corrupt?");
    } // else if

    crashedmsg = xstrdup(_("The installer has crashed due to a bug."));
    termedmsg = xstrdup(_("The installer has been stopped by the system."));

    return true;
} // initEverything


static void deinitEverything(void)
{
    char *tmp = NULL;

    logInfo("MojoSetup shutting down...");
    MojoLua_deinitLua();
    MojoGui_deinitGuiPlugin();
    MojoArchive_deinitBaseArchive();
    MojoLog_deinitLogging();

    tmp = crashedmsg;
    crashedmsg = NULL;
    free(tmp);
    tmp = termedmsg;
    termedmsg = NULL;
    free(tmp);
} // deinitEverything


void MojoChecksum_init(MojoChecksumContext *ctx)
{
    memset(ctx, '\0', sizeof (MojoChecksumContext));
    #if SUPPORT_CRC32
    MojoCrc32_init(&ctx->crc32);
    #endif
    #if SUPPORT_MD5
    MojoMd5_init(&ctx->md5);
    #endif
    #if SUPPORT_SHA1
    MojoSha1_init(&ctx->sha1);
    #endif
} // MojoChecksum_init


void MojoChecksum_append(MojoChecksumContext *ctx, const uint8 *d, uint32 len)
{
    #if SUPPORT_CRC32
    MojoCrc32_append(&ctx->crc32, d, len);
    #endif
    #if SUPPORT_MD5
    MojoMd5_append(&ctx->md5, d, len);
    #endif
    #if SUPPORT_SHA1
    MojoSha1_append(&ctx->sha1, d, len);
    #endif
} // MojoChecksum_append


void MojoChecksum_finish(MojoChecksumContext *ctx, MojoChecksums *sums)
{
    memset(sums, '\0', sizeof (MojoChecksums));
    #if SUPPORT_CRC32
    MojoCrc32_finish(&ctx->crc32, &sums->crc32);
    #endif
    #if SUPPORT_MD5
    MojoMd5_finish(&ctx->md5, sums->md5);
    #endif
    #if SUPPORT_SHA1
    MojoSha1_finish(&ctx->sha1, sums->sha1);
    #endif
} // MojoChecksum_finish


boolean cmdline(const char *arg)
{
    int argc = GArgc;
    const char **argv = GArgv;
    int i;

    if ((arg == NULL) || (argv == NULL))
        return false;

    while (*arg == '-')  // Skip all '-' chars, so "--nosound" == "-nosound"
        arg++;

    for (i = 1; i < argc; i++)
    {
        const char *thisarg = argv[i];
        if (*thisarg != '-')
            continue;  // no dash in the string, skip it.
        while (*(++thisarg) == '-') { /* keep looping. */ }
        if (strcmp(arg, thisarg) == 0)
            return true;
    } // for

    return false;
} // cmdline


const char *cmdlinestr(const char *arg, const char *envr, const char *deflt)
{
    uint32 len = 0;
    int argc = GArgc;
    const char **argv = GArgv;
    int i;

    if (envr != NULL)
    {
        const char *val = getenv(envr);
        if (val != NULL)
            return val;
    } // if

    if (arg == NULL)
        return deflt;

    while (*arg == '-')  // Skip all '-' chars, so "--nosound" == "-nosound"
        arg++;

    len = strlen(arg);

    for (i = 1; i < argc; i++)
    {
        const char *thisarg = argv[i];
        if (*thisarg != '-')
            continue;  // no dash in the string, skip it.

        while (*(++thisarg) == '-') { /* keep looping. */ }
        if (strncmp(arg, thisarg, len) != 0)
            continue;   // not us.

        thisarg += len;  // skip ahead in string to end of match.

        if (*thisarg == '=')  // --a=b format.
            return (thisarg + 1);
        else if (*thisarg == '\0')  // --a b format.
            return ((argv[i+1] == NULL) ? deflt : argv[i+1]);
    } // for

    return deflt;
} // cmdlinestr


boolean wildcardMatch(const char *str, const char *pattern)
{
    char sch = *(str++);
    while (true)
    {
        const char pch = *(pattern++);
        if (pch == '?')
        {
            if ((sch == '\0') || (sch == '/'))
                return false;
            sch = *(str++);
        } // else if

        else if (pch == '*')
        {
            char nextpch = *pattern;
            if ((nextpch != '?') && (nextpch != '*'))
            {
                while ((sch != '\0') && (sch != nextpch))
                    sch = *(str++);
            } // if
        } // else if

        else
        {
            if (pch != sch)
                return false;
            else if (pch == '\0')
                break;
            sch = *(str++);
        } // else
    } // while

    return true;
} // wildcardMatch


const char *numstr(int val)
{
    static int pos = 0;
    char *ptr = ((char *) scratchbuf_128k) + (pos * 128);
    snprintf(ptr, 128, "%d", val);
    pos = (pos + 1) % 1000;
    return ptr;
} // numstr


static char *format_internal(const char *fmt, va_list ap)
{
    // This is kinda nasty. String manipulation in C always is.
    char *retval = NULL;
    const char *strs[10];  // 0 through 9.
    const char *ptr = NULL;
    char *wptr = NULL;
    size_t len = 0;
    int maxfmtid = -2;
    int i;

    // figure out what this format string contains...
    for (ptr = fmt; *ptr; ptr++)
    {
        if (*ptr == '%')
        {
            const char ch = *(++ptr);
            if (ch == '%')  // a literal '%'
                maxfmtid = (maxfmtid == -2) ? -1 : maxfmtid;
            else if ((ch >= '0') && (ch <= '9'))
                maxfmtid = ((maxfmtid > (ch - '0')) ? maxfmtid : (ch - '0'));
            else
                fatal(_("BUG: Invalid format() string"));
        } // if
    } // while

    if (maxfmtid == -2)  // no formatters present at all.
        return xstrdup(fmt);  // just copy it, we're done.

    for (i = 0; i <= maxfmtid; i++)  // referenced varargs --> linear array.
    {
        strs[i] = va_arg(ap, const char *);
        if (strs[i] == NULL)
            strs[i] = "(null)";  // just to match sprintf() behaviour...
    } // for

    // allocate the string we'll need in one shot, so we don't have to resize.
    for (ptr = fmt; *ptr; ptr++)
    {
        if (*ptr != '%')
            len++;
        else
        {
            const char ch = *(++ptr);
            if (ch == '%')  // a literal '%'
                len++;  // just want '%' char.
            else //if ((ch >= '0') && (ch <= '9'))
                len += strlen(strs[ch - '0']);
        } // else
    } // while

    // Now write the formatted string...
    wptr = retval = (char *) xmalloc(len+1);
    for (ptr = fmt; *ptr; ptr++)
    {
        const char strch = *ptr;
        if (strch != '%')
            *(wptr++) = strch;
        else
        {
            const char ch = *(++ptr);
            if (ch == '%')  // a literal '%'
                *(wptr++) = '%';
            else //if ((ch >= '0') && (ch <= '9'))
            {
                const char *str = strs[ch - '0'];
                strcpy(wptr, str);
                wptr += strlen(str);
            } // else
        } // else
    } // while
    *wptr = '\0';

    return retval;
} // format_internal


char *format(const char *fmt, ...)
{
    char *retval = NULL;
    va_list ap;
    va_start(ap, fmt);
    retval = format_internal(fmt, ap);
    va_end(ap);
    return retval;
} // format


#if ((defined _NDEBUG) || (defined NDEBUG))
#define DEFLOGLEV "info"
#else
#define DEFLOGLEV "everything"
#endif
MojoSetupLogLevel MojoLog_logLevel = MOJOSETUP_LOG_EVERYTHING;
static void *logFile = NULL;

void MojoLog_initLogging(void)
{
    const char *level = cmdlinestr("loglevel","MOJOSETUP_LOGLEVEL", DEFLOGLEV);
    const char *fname = cmdlinestr("log", "MOJOSETUP_LOG", NULL);

    if (strcmp(level, "nothing") == 0)
        MojoLog_logLevel = MOJOSETUP_LOG_NOTHING;
    else if (strcmp(level, "errors") == 0)
        MojoLog_logLevel = MOJOSETUP_LOG_ERRORS;
    else if (strcmp(level, "warnings") == 0)
        MojoLog_logLevel = MOJOSETUP_LOG_WARNINGS;
    else if (strcmp(level, "info") == 0)
        MojoLog_logLevel = MOJOSETUP_LOG_INFO;
    else if (strcmp(level, "debug") == 0)
        MojoLog_logLevel = MOJOSETUP_LOG_DEBUG;
    else  // Unknown string gets everything...that'll teach you.
        MojoLog_logLevel = MOJOSETUP_LOG_EVERYTHING;

    if ((fname != NULL) && (strcmp(fname, "-") == 0))
        logFile = MojoPlatform_stdout();
    else if (fname != NULL)
    {
        const uint32 flags = MOJOFILE_WRITE|MOJOFILE_CREATE|MOJOFILE_TRUNCATE;
        const uint16 mode = MojoPlatform_defaultFilePerms();
        logFile = MojoPlatform_open(fname, flags, mode);
    } // if
} // MojoLog_initLogging


void MojoLog_deinitLogging(void)
{
    if (logFile != NULL)
    {
        MojoPlatform_close(logFile);
        logFile = NULL;
    } // if
} // MojoLog_deinitLogging


static inline void addLog(MojoSetupLogLevel level, char levelchar,
                          const char *fmt, va_list ap)
{
    if (level <= MojoLog_logLevel)
    {
        char *str = format_internal(fmt, ap);
        //int len = vsnprintf(buf + 2, sizeof (buf) - 2, fmt, ap) + 2;
        //buf[0] = levelchar;
        //buf[1] = ' ';
        int len = strlen(str);
        while ( (--len >= 0) && ((str[len] == '\n') || (str[len] == '\r')) ) {}
        str[len+1] = '\0';  // delete trailing newline crap.
        MojoPlatform_log(str);
        if (logFile != NULL)
        {
            const char *endl = MOJOPLATFORM_ENDLINE;
            MojoPlatform_write(logFile, str, strlen(str));
            MojoPlatform_write(logFile, endl, strlen(endl));
            MojoPlatform_flush(logFile);
        } // if
        free(str);
    } // if
} // addLog


void logWarning(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    addLog(MOJOSETUP_LOG_WARNINGS, '-', fmt, ap);
    va_end(ap);
} // logWarning


void logError(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    addLog(MOJOSETUP_LOG_ERRORS, '!', fmt, ap);
    va_end(ap);
} // logError


void logInfo(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    addLog(MOJOSETUP_LOG_INFO, '*', fmt, ap);
    va_end(ap);
} // logInfo


void logDebug(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    addLog(MOJOSETUP_LOG_DEBUG, '#', fmt, ap);
    va_end(ap);
} // logDebug


uint32 profile(const char *what, uint32 start_time)
{
    uint32 retval = MojoPlatform_ticks() - start_time;
    if (what != NULL)
        logDebug("%0 took %1 ms.", what, numstr((int) retval));
    return retval;
} // profile_start


int fatal(const char *fmt, ...)
{
    static boolean in_fatal = false;

    if (in_fatal)
        return panic("BUG: fatal() called more than once!");

    in_fatal = true;

    // may not want to show a message, since we displayed one elsewhere, etc.
    if (fmt != NULL)
    {
        char *buf = NULL;
        va_list ap;
        va_start(ap, fmt);
        buf = format_internal(fmt, ap);
        va_end(ap);

        logError("FATAL: %0", buf);

        if (GGui != NULL)
            GGui->msgbox(_("Fatal error"), buf);

        free(buf);
    } // if

    // Shouldn't call fatal() before app is initialized!
    if ( (GGui == NULL) || (!MojoLua_initialized()) )
        panic("fatal() called before app is initialized! Panicking...");

    MojoLua_callProcedure("revertinstall");

    deinitEverything();
    exit(23);
    return 0;
} // fatal


int panic(const char *err)
{
    static int panic_runs = 0;

    panic_runs++;
    if (panic_runs == 1)
    {
        logError("PANIC: %0", err);
        panic(err);
    } // if

    else if (panic_runs == 2)
    {
        boolean domsgbox = ((GGui != NULL) && (GGui->msgbox != NULL));
        if (domsgbox)
            GGui->msgbox(_("PANIC"), err);

        if ((GGui != NULL) && (GGui->deinit != NULL))
            GGui->deinit();

        if (!domsgbox)
            panic(err);  /* no GUI plugin...double-panic. */
    } // if

    else if (panic_runs == 3)  // no GUI or panic panicked...write to stderr...
        fprintf(stderr, "\n\n\n%s\n  %s\n\n\n", _("PANIC"), err);

    else  // panic is panicking in a loop, terminate without any cleanup...
        MojoPlatform_die();

    exit(22);
    return 0;  // shouldn't hit this.
} // panic


char *xstrncpy(char *dst, const char *src, size_t len)
{
    snprintf(dst, len, "%s", src);
    return dst;
} // xstrncpy


uint32 utf8codepoint(const char **_str)
{
    const char *str = *_str;
    uint32 retval = 0;
    uint32 octet = (uint32) ((uint8) *str);
    uint32 octet2, octet3, octet4;

    if (octet == 0)  // null terminator, end of string.
        return 0;

    else if (octet < 128)  // one octet char: 0 to 127
    {
        (*_str)++;  // skip to next possible start of codepoint.
        return octet;
    } // else if

    else if ((octet > 127) && (octet < 192))  // bad (starts with 10xxxxxx).
    {
        // Apparently each of these is supposed to be flagged as a bogus
        //  char, instead of just resyncing to the next valid codepoint.
        (*_str)++;  // skip to next possible start of codepoint.
        return UNICODE_BOGUS_CHAR_VALUE;
    } // else if

    else if (octet < 224)  // two octets
    {
        octet -= (128+64);
        octet2 = (uint32) ((uint8) *(++str));
        if ((octet2 & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        *_str += 2;  // skip to next possible start of codepoint.
        retval = ((octet << 6) | (octet2 - 128));
        if ((retval >= 0x80) && (retval <= 0x7FF))
            return retval;
    } // else if

    else if (octet < 240)  // three octets
    {
        octet -= (128+64+32);
        octet2 = (uint32) ((uint8) *(++str));
        if ((octet2 & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet3 = (uint32) ((uint8) *(++str));
        if ((octet3 & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        *_str += 3;  // skip to next possible start of codepoint.
        retval = ( ((octet << 12)) | ((octet2-128) << 6) | ((octet3-128)) );

        // There are seven "UTF-16 surrogates" that are illegal in UTF-8.
        switch (retval)
        {
            case 0xD800:
            case 0xDB7F:
            case 0xDB80:
            case 0xDBFF:
            case 0xDC00:
            case 0xDF80:
            case 0xDFFF:
                return UNICODE_BOGUS_CHAR_VALUE;
        } // switch

        // 0xFFFE and 0xFFFF are illegal, too, so we check them at the edge.
        if ((retval >= 0x800) && (retval <= 0xFFFD))
            return retval;
    } // else if

    else if (octet < 248)  // four octets
    {
        octet -= (128+64+32+16);
        octet2 = (uint32) ((uint8) *(++str));
        if ((octet2 & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet3 = (uint32) ((uint8) *(++str));
        if ((octet3 & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet4 = (uint32) ((uint8) *(++str));
        if ((octet4 & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        *_str += 4;  // skip to next possible start of codepoint.
        retval = ( ((octet << 18)) | ((octet2 - 128) << 12) |
                   ((octet3 - 128) << 6) | ((octet4 - 128)) );
        if ((retval >= 0x10000) && (retval <= 0x10FFFF))
            return retval;
    } // else if

    // Five and six octet sequences became illegal in rfc3629.
    //  We throw the codepoint away, but parse them to make sure we move
    //  ahead the right number of bytes and don't overflow the buffer.

    else if (octet < 252)  // five octets
    {
        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        *_str += 5;  // skip to next possible start of codepoint.
        return UNICODE_BOGUS_CHAR_VALUE;
    } // else if

    else  // six octets
    {
        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        octet = (uint32) ((uint8) *(++str));
        if ((octet & (128+64)) != 128)  // Format isn't 10xxxxxx?
            return UNICODE_BOGUS_CHAR_VALUE;

        *_str += 6;  // skip to next possible start of codepoint.
        return UNICODE_BOGUS_CHAR_VALUE;
    } // else if

    return UNICODE_BOGUS_CHAR_VALUE;
} // utf8codepoint


int utf8len(const char *str)
{
    int retval = 0;
    while (utf8codepoint(&str))
        retval++;
    return retval;
} // utf8len


static char *strfrombuf(const char *text, int len)
{
    char *retval = xmalloc(len + 1);
    memcpy(retval, text, len);
    retval[len] = '\0';
    return retval;
} // strfrombuf


char **splitText(const char *text, int scrw, int *_count, int *_w)
{
    int i = 0;
    int j = 0;
    char **retval = NULL;
    int count = 0;
    int w = 0;

    *_count = *_w = 0;
    while (*text)
    {
        const char *utf8text = text;
        uint32 ch = 0;
        int pos = 0;
        int furthest = 0;

        for (i = 0; ((ch = utf8codepoint(&utf8text))) && (i < scrw); i++)
        {
            if ((ch == '\r') || (ch == '\n'))
            {
                const char nextbyte = *utf8text;
                count++;
                retval = (char **) xrealloc(retval, count * sizeof (char *));
                retval[count-1] = strfrombuf(text, utf8text - text);
                if ((ch == '\r') && (nextbyte == '\n'))  // DOS endlines!
                    utf8text++; // skip it.
                text = (char *) utf8text;  // update to start of new line.

                if (i > w)
                    w = i;
                i = -1;  // will be zero on next iteration...
            } // if
            else if ((ch == ' ') || (ch == '\t'))
            {
                if (i != 0)  // trim spaces from start of line...
                    furthest = i;
                else
                {
                    text++;
                    i = -1;  // it'll be zero on next iteration.
                } // else
            } // else if
        } // for

        // line overflow or end of stream...
        pos = (ch) ? furthest : i;
        if ((ch) && (furthest == 0))  // uhoh, no split at all...hack it.
        {
            pos = utf8len(text);
            if (pos > scrw)  // too big, have to chop a string in the middle.
                pos = scrw;
        } // if

        if (pos > 0)
        {
            utf8text = text;  // adjust pointer by redecoding from start...
            for (j = 0; j < pos; j++)
                utf8codepoint(&utf8text);

            count++;
            retval = (char **) xrealloc(retval, count * sizeof (char*));
            retval[count-1] = strfrombuf(text, utf8text - text);
            text = (char *) utf8text;
            if (pos > w)
                w = pos;
        } // if
    } // while

    *_count = count;
    *_w = w;
    return retval;
} // splitText


static void outOfMemory(void)
{
    // Try to translate "out of memory", but not if it causes recursion.
    static boolean already_panicked = false;
    const char *errstr = "out of memory";
    if (!already_panicked)
    {
        already_panicked = true;
        errstr = translate(errstr);
    } // if
    panic(errstr);
} // outOfMemory


#undef calloc
void *xmalloc(size_t bytes)
{
    void *retval = calloc(1, bytes);
    if (retval == NULL)
        outOfMemory();
    return retval;
} // xmalloc
#define calloc(x,y) DO_NOT_CALL_CALLOC__USE_XMALLOC_INSTEAD

#undef realloc
void *xrealloc(void *ptr, size_t bytes)
{
    void *retval = realloc(ptr, bytes);
    if (retval == NULL)
        outOfMemory();
    return retval;
} // xrealloc
#define realloc(x,y) DO_NOT_CALL_REALLOC__USE_XREALLOC_INSTEAD

char *xstrdup(const char *str)
{
    char *retval = (char *) xmalloc(strlen(str) + 1);
    strcpy(retval, str);
    return retval;
} // xstrdup


// We have to supply this function under certain build types.
#if MOJOSETUP_INTERNAL_BZLIB && BZ_NO_STDIO
void bz_internal_error(int errcode)
{
    fatal(_("bzlib triggered an internal error: %0"), numstr(errcode));
} // bz_internal_error
#endif


#if SUPPORT_STBIMAGE
unsigned char *stbi_load_from_memory(unsigned char *buffer, int len, int *x,
                                     int *y, int *comp, int req_comp);
#endif

uint8 *decodeImage(const uint8 *data, uint32 size, uint32 *w, uint32 *h)
{
    uint8 *retval = MojoPlatform_decodeImage(data, size, w, h);

    #if SUPPORT_STBIMAGE
    if (retval == NULL)  // try our built-in routines.
    {
        const int siz = (int) size;
        unsigned char *buf = (unsigned char *) data;
        int x = 0, y = 0, comp = 0;
        retval = (uint8 *) stbi_load_from_memory(buf, siz, &x, &y, &comp, 4);
        *w = (uint32) x;
        *h = (uint32) y;
    } // if
    #endif

    if (retval == NULL)
        *w = *h = 0;

    return retval;
} // decodeImage


boolean isValidProductKey(const char *fmt, const char *key)
{
    if (fmt == NULL)
        return true;
    else if (key == NULL)
        return false;

    while (*fmt)
    {
        const char fmtch = *(fmt++);
        const char keych = *(key++);
        switch (fmtch)
        {
            case '-':
            case ' ':
                if ((keych == ' ') || (keych == '-'))
                    break;
                key--;  // user didn't type this, roll back.
                break;

            case '#':
                if ((keych >= '0') && (keych <= '9'))
                    break;
                return false;

            case 'X':
                if ( ((keych >= 'A') && (keych <= 'Z')) ||
                     ((keych >= 'a') && (keych <= 'z')) )
                    break;
                return false;

            case '?':
                if ( ((keych >= 'A') && (keych <= 'Z')) ||
                     ((keych >= 'a') && (keych <= 'z')) ||
                     ((keych >= '0') && (keych <= '9')) )
                    break;
                return false;

            case '*':
                break;

            default:
                // this should have been caught by schema sanitize.
                assert(false && "Invalid product key format.");
                return false;
        } // switch
    } // while

    return (*key == '\0');
} // isValidProductKey


// This is called from main()/WinMain()/whatever.
int MojoSetup_main(int argc, char **argv)
{
    GArgc = argc;
    GArgv = (const char **) argv;

    if (cmdline("buildver"))
    {
        printf("%s\n", GBuildVer);
        return 0;
    } // if

    #if TEST_LAUNCH_BROWSER_CODE
    return MojoSetup_testLaunchBrowserCode(argc, argv);
    #endif

    #if TEST_ARCHIVE_CODE
    return MojoSetup_testArchiveCode(argc, argv);
    #endif

    #if TEST_NETWORK_CODE
    return MojoSetup_testNetworkCode(argc, argv);
    #endif

    if (!initEverything())
        return 1;

    // Jump into Lua for the heavy lifting.
    MojoLua_runFile("mojosetup_mainline");
    deinitEverything();

    return 0;
} // MojoSetup_main



#if TEST_LAUNCH_BROWSER_CODE
int MojoSetup_testLaunchBrowserCode(int argc, char **argv)
{
    int i;

    if (!MojoArchive_initBaseArchive())  // Maybe need for xdg-open script.
        panic("Initial setup failed. Cannot continue.");

    printf("Testing browser launching code...\n\n");
    for (i = 1; i < argc; i++)
    {
        const boolean rc = MojoPlatform_launchBrowser(argv[i]);
        printf("Launch '%s': %s\n", argv[i], rc ? "success" : "failure");
    } // for

    MojoArchive_deinitBaseArchive();
    return 0;
} // MojoSetup_testLaunchBrowserCode
#endif


#if TEST_ARCHIVE_CODE
int MojoSetup_testArchiveCode(int argc, char **argv)
{
    int i;
    printf("Testing archiver code...\n\n");
    for (i = 1; i < argc; i++)
    {
        MojoArchive *archive = MojoArchive_newFromDirectory(argv[i]);
        if (archive != NULL)
            printf("directory '%s'...\n", argv[i]);
        else
        {
            MojoInput *io = MojoInput_newFromFile(argv[i]);
            if (io != NULL)
            {
                archive = MojoArchive_newFromInput(io, argv[i]);
                if (archive != NULL)
                    printf("archive '%s'...\n", argv[i]);
            } // if
        } // else

        if (archive == NULL)
            fprintf(stderr, "Couldn't handle '%s'\n", argv[i]);
        else
        {
            if (!archive->enumerate(archive))
                fprintf(stderr, "enumerate() failed.\n");
            else
            {
                const MojoArchiveEntry *ent;
                while ((ent = archive->enumNext(archive)) != NULL)
                {
                    printf("%s ", ent->filename);
                    if (ent->type == MOJOARCHIVE_ENTRY_FILE)
                    {
                        printf("(file, %d bytes, %o)\n",
                                (int) ent->filesize, ent->perms);

                        MojoInput *input = archive->openCurrentEntry(archive);

                        if(&archive->prevEnum != ent)
                        {
                            fprintf(stderr, "Address of MojoArchiveEntry pointer differs\n");
                            exit(EXIT_FAILURE);
                        } // if

                        if(!input)
                        {
                            fprintf(stderr, "Could not open current entry\n");
                            exit(EXIT_FAILURE);
                        } // if

                        if(!input->ready(input))
                        {
                            input->close(input);
                            continue;
                        } // if

                        uint64 pos = input->tell(input);
                        if(0 != pos)
                        {
                            fprintf(stderr, "position has to be 0 on start, is: %d\n", (int) pos);
                            exit(EXIT_FAILURE);
                        } // if

                        int64 filesize = input->length(input);
                        if(filesize != ent->filesize)
                        {
                            fprintf(stderr, "file size mismatch %d != %d\n",
                            		(int) filesize, (int) ent->filesize);
                            exit(EXIT_FAILURE);
                        } // if

                        boolean ret = input->seek(input, filesize - 1);
                        if(!ret)
                        {
                            fprintf(stderr, "seek() has to return 'true'.\n");
                            exit(EXIT_FAILURE);
                        } // if

                        ret = input->seek(input, filesize);
                        if(ret)
                        {
                            fprintf(stderr, "seek() has to return 'false'.\n");
                            exit(EXIT_FAILURE);
                        } // if

                        pos = input->tell(input);
                        if(filesize -1  != pos)
                        {
                            fprintf(stderr, "position should be %d after seek(), is: %d\n",
                            		(int) filesize - 1, (int) pos);
                            exit(EXIT_FAILURE);
                        } // if

                        input->close(input);
                    } // if
                    else if (ent->type == MOJOARCHIVE_ENTRY_DIR)
                        printf("(dir, %o)\n", ent->perms);
                    else if (ent->type == MOJOARCHIVE_ENTRY_SYMLINK)
                        printf("(symlink -> '%s')\n", ent->linkdest);
                    else
                    {
                        printf("(UNKNOWN?!, %d bytes, -> '%s', %o)\n",
                                (int) ent->filesize, ent->linkdest,
                                ent->perms);
                    } // else
                } // while
            } // else
            archive->close(archive);
            printf("\n\n");
        } // else
    } // for

    return 0;
} // MojoSetup_testArchiveCode
#endif


#if TEST_NETWORK_CODE
int MojoSetup_testNetworkCode(int argc, char **argv)
{
    int i;
    fprintf(stderr, "Testing networking code...\n\n");
    for (i = 1; i < argc; i++)
    {
        static char buf[64 * 1024];
        uint32 start = 0;
        const char *url = argv[i];
        int64 length = -1;
        int64 total_br = 0;
        int64 br = 0;
        printf("\n\nFetching '%s' ...\n", url);
        MojoInput *io = MojoInput_newFromURL(url);
        if (io == NULL)
        {
            fprintf(stderr, "failed!\n");
            continue;
        } // if

        start = MojoPlatform_ticks();
        while (!io->ready(io))
            MojoPlatform_sleep(10);
        fprintf(stderr, "took about %d ticks to get started\n",
                (int) (MojoPlatform_ticks() - start));

        length = io->length(io);
        fprintf(stderr, "Ready to read (%lld) bytes.\n",
                (long long) length);

        do
        {
            start = MojoPlatform_ticks();
            if (!io->ready(io))
            {
                fprintf(stderr, "Not ready!\n");
                while (!io->ready(io))
                    MojoPlatform_sleep(10);
                fprintf(stderr, "took about %d ticks to get ready\n",
                        (int) (MojoPlatform_ticks() - start));
            } // if

            start = MojoPlatform_ticks();
            br = io->read(io, buf, sizeof (buf));
            fprintf(stderr, "read blocked for about %d ticks\n",
                    (int) (MojoPlatform_ticks() - start));
            if (br > 0)
            {
                total_br += br;
                fprintf(stderr, "read %lld bytes\n", (long long) br);
                fwrite(buf, br, 1, stdout);
            } // if
        } while (br > 0);

        if (br < 0)
            fprintf(stderr, "ERROR IN TRANSMISSION.\n\n");
        else
        {
            fprintf(stderr, "TRANSMISSION COMPLETE!\n\n");
            fprintf(stderr, "(Read %lld bytes, expected %lld.)\n",
                    (long long) total_br, length);
        } // else
        io->close(io);
    } // for

    return 0;
} // MojoSetup_testNetworkCode
#endif

// end of mojosetup.c ...