view sidinfo.c @ 311:ee56f1f2b528

Change how the STIL subtune are handled internally. Also fix a bug that caused the _last_ STIL field of each subtune not to be printed at all :S
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 11 Jan 2020 14:25:23 +0200
parents ff93c168c4aa
children b3d46806787d
line wrap: on
line source

/*
 * SIDInfo - PSID/RSID information displayer
 * Programmed and designed by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
 * (C) Copyright 2014-2020 Tecnic Software productions (TNSP)
 */
#include "th_args.h"
#include "th_string.h"
#include "th_file.h"
#include "th_datastruct.h"
#include "sidlib.h"
#include <sys/types.h>
#include <dirent.h>
#ifdef HAVE_ICONV
#    include <iconv.h>
#endif


//
// Some constants
//

// HVSC documents directory
#define SET_HVSC_DOCUMENTS   "DOCUMENTS"

// Songlengths database filename prefix (.md5|.txt appended)
#define SET_SLDB_FILEBASE    "Songlengths"

// STIL database file
#define SET_STILDB_FILENAME  "STIL.txt"


enum
{
    OFMT_QUOTED    = 0x0001,
    OFMT_FORMAT    = 0x0002,
};


enum
{
    OTYPE_OTHER    = 0,
    OTYPE_STR      = 1,
    OTYPE_INT      = 2,
};


enum
{
    TH_LANG_UTF8,
    TH_LANG_ISO88591,
    TH_LANG_CP850,
    TH_LANG_CP437,
};


typedef struct
{
    int cmd;
    char *str;
    char chr;
    int flags;
    char *fmt;
} PSFStackItem;


typedef struct
{
    int nitems, nallocated;
    PSFStackItem *items;
} PSFStack;


typedef struct
{
    char *name;
    char *lname;
    int type;
    char *dfmt;
} PSFOption;


#define HEX_ADDR "%d ($%04x)"

static const PSFOption optPSOptions[] =
{
    { "Filename"     , NULL                   , OTYPE_STR    , NULL },
    { "Type"         , NULL                   , OTYPE_STR    , NULL },
    { "Version"      , NULL                   , OTYPE_STR    , NULL },
    { "PlayerType"   , "Player type"          , OTYPE_STR    , NULL },
    { "PlayerCompat" , "Player compatibility" , OTYPE_STR    , NULL },
    { "VideoClock"   , "Video clock speed"    , OTYPE_STR    , NULL },
    { "SIDModel"     , "SID model"            , OTYPE_STR    , NULL },

    { "DataOffs"     , "Data offset"          , OTYPE_INT    , HEX_ADDR },
    { "DataSize"     , "Data size"            , OTYPE_INT    , HEX_ADDR },
    { "LoadAddr"     , "Load address"         , OTYPE_INT    , HEX_ADDR },
    { "InitAddr"     , "Init address"         , OTYPE_INT    , HEX_ADDR },
    { "PlayAddr"     , "Play address"         , OTYPE_INT    , HEX_ADDR },
    { "Songs"        , "Songs"                , OTYPE_INT    , "%d" },
    { "StartSong"    , "Start song"           , OTYPE_INT    , "%d" },

    { "SID2Model"    , "2nd SID model"        , OTYPE_STR    , NULL },
    { "SID3Model"    , "3rd SID model"        , OTYPE_STR    , NULL },
    { "SID2Addr"     , "2nd SID address"      , OTYPE_INT    , HEX_ADDR },
    { "SID3Addr"     , "3rd SID address"      , OTYPE_INT    , HEX_ADDR },

    { "Name"         , NULL                   , OTYPE_STR    , NULL },
    { "Author"       , NULL                   , OTYPE_STR    , NULL },
    { "Copyright"    , NULL                   , OTYPE_STR    , NULL },
    { "Hash"         , NULL                   , OTYPE_STR    , NULL },

    { "Songlengths"  , "Song lengths"         , OTYPE_OTHER  , NULL },
    { "STIL"         , "STIL information"     , OTYPE_OTHER  , NULL },
};

static const int noptPSOptions = sizeof(optPSOptions) / sizeof(optPSOptions[0]);


// Option variables
char   *setHVSCPath = NULL,
       *setSLDBPath = NULL,
       *setSTILDBPath = NULL;
BOOL	setSLDBNewFormat = FALSE,
        optParsable = FALSE,
        optFieldNamePrefix = TRUE,
        optHexadecimal = FALSE,
        optFieldOutput = TRUE,
        optRecurseDirs = FALSE;
char    *optOneLineFieldSep = NULL,
        *optEscapeChars = NULL;
int     optNFiles = 0;

PSFStack optFormat;

SIDLibSLDB *sidSLDB = NULL;
SIDLibSTILDB *sidSTILDB = NULL;


BOOL    setUseOutConv;
#ifdef HAVE_ICONV
iconv_t setIConvCtx;
#else
int     setOutLang;
#endif


// Define option arguments
static const th_optarg optList[] =
{
    { 0, '?', "help",       "Show this help", OPT_NONE },
    { 1, 'v', "verbose",    "Be more verbose", OPT_NONE },
    {10,   0, "license",    "Print out this program's license agreement", OPT_NONE },

    { 2, 'p', "parsable",   "Output in script-parsable format", OPT_NONE },
    { 5, 'n', "noprefix",   "Output without field name prefix", OPT_NONE },
    { 6, 'l', "line",       "Output in one line format, -l <field separator>", OPT_ARGREQ },
    {11, 'e', "escape",     "Escape these characters in fields (see note)", OPT_ARGREQ },
    { 3, 'f', "fields",     "Show only specified field(s)", OPT_ARGREQ },
    { 4, 'x', "hex",        "Use hexadecimal values", OPT_NONE },
    { 7, 'F', "format",     "Use given format string (see below)", OPT_ARGREQ },
    { 8, 'H', "hvsc",       "Specify path to HVSC root directory", OPT_ARGREQ },
    { 9, 'S', "sldb",       "Specify Songlengths.(txt|md5) file", OPT_ARGREQ },
    {13, 'T', "stildb",     "Specify STIL.txt file", OPT_ARGREQ },
    {12, 'R', "recurse",    "Recurse into sub-directories", OPT_NONE },
};

static const int optListN = sizeof(optList) / sizeof(optList[0]);


void argShowLicense(void)
{
    printf("%s - %s\n%s\n", th_prog_name, th_prog_desc, th_prog_author);
    printf(
    "\n"
    "Redistribution and use in source and binary forms, with or without\n"
    "modification, are permitted provided that the following conditions\n"
    "are met:\n"
    "\n"
    " 1. Redistributions of source code must retain the above copyright\n"
    "    notice, this list of conditions and the following disclaimer.\n"
    "\n"
    " 2. Redistributions in binary form must reproduce the above copyright\n"
    "    notice, this list of conditions and the following disclaimer in\n"
    "    the documentation and/or other materials provided with the\n"
    "    distribution.\n"
    "\n"
    " 3. The name of the author may not be used to endorse or promote\n"
    "    products derived from this software without specific prior written\n"
    "    permission.\n"
    "\n"
    "THIS SOFTWARE IS PROVIDED BY THE AUTHOR \"AS IS\" AND ANY EXPRESS OR\n"
    "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n"
    "WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n"
    "ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,\n"
    "INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n"
    "(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n"
    "SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n"
    "HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n"
    "STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING\n"
    "IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n"
    "POSSIBILITY OF SUCH DAMAGE.\n"
    );
}


void argShowHelp(void)
{
    int index, len;

    th_print_banner(stdout, th_prog_name, "[options] <sid filename> [sid filename #2 ..]");
    th_args_help(stdout, optList, optListN, 0);
    printf(
        "\n"
        "Available fields:\n");

    for (len = index = 0; index < noptPSOptions; index++)
    {
        const PSFOption *opt = &optPSOptions[index];
        len += strlen(opt->name) + 3;
        if (len >= 72)
        {
            printf("\n");
            len = 0;
        }
        printf("%s%s", opt->name, (index < noptPSOptions - 1) ? ", " : "\n\n");
    }

    printf(
        "Example: %s -x -p -f hash,copyright somesidfile.sid\n"
        "\n"
        "Format strings for '-F' option are composed of @fields@ that\n"
        "are expanded to their value. Also, escape sequences \\r, \\n and \\t\n"
        "can be used: -F \"hash=@hash@\\ncopy=@copyright@\\n\"\n"
        "\n"
        "The -F fields can be further formatted via printf-style specifiers:\n"
        "-F \"@copyright:'%%-30s'@\"\n"
        "\n"
        "NOTE: One line output (-l <field separator>) also sets escape characters\n"
        "(option -e <chars>), if escape characters have NOT been separately set.\n"
        "\n"
        "TIP: When specifying HVSC paths, it is preferable to use -H/--hvsc option,\n"
        "as STIL.txt and Songlengths.(txt|md5) will be automatically used from there.\n"
        "You can also set it via HVSC_BASE environment variable, see README.\n"
        "\n"
        , th_prog_name);
}


int argMatchPSField(const char *field)
{
    int index, found = -1;
    for (index = 0; index < noptPSOptions; index++)
    {
        const PSFOption *opt = &optPSOptions[index];
        if (th_strcasecmp(opt->name, field) == 0)
        {
            if (found >= 0)
                return -2;
            found = index;
        }
    }

    return found;
}


int argMatchPSFieldError(const char *field)
{
    int found = argMatchPSField(field);
    switch (found)
    {
        case -1:
            THERR("No such field '%s'.\n", field);
            break;

        case -2:
            THERR("Field '%s' is ambiguous.\n", field);
            break;
    }
    return found;
}


const char *siStripHVSCPath(const char *filename)
{
    if (setHVSCPath != NULL)
    {
        const char *hvsc = setHVSCPath, *fptr = filename;

        // Compare until end of string(s)
        for (; *hvsc != 0 && *fptr != 0 && *hvsc == *fptr; hvsc++, fptr++);

        // Full match?
        if (*hvsc == 0)
            return fptr - 1;
    }

    return filename;
}


char *siCheckHVSCFilePath(const char *filebase, const char *fext)
{
    th_stat_data sdata;
    char *npath = th_strdup_printf("%s%c%s%c%s%s",
        setHVSCPath, TH_DIR_SEPARATOR_CHR,
        SET_HVSC_DOCUMENTS, TH_DIR_SEPARATOR_CHR,
        filebase, fext != NULL ? fext : "");

    if (npath != NULL &&
        th_stat_path(npath, &sdata) &&
        (sdata.flags & TH_IS_READABLE) &&
        (sdata.flags & TH_IS_DIR) == 0)
        return npath;

    th_free(npath);
    return NULL;
}


BOOL siStackAddItem(PSFStack *stack, const PSFStackItem *item)
{
    if (stack->items == NULL || stack->nitems + 1 >= stack->nallocated)
    {
        stack->nallocated += 16;
        if ((stack->items = th_realloc(stack->items, stack->nallocated * sizeof(PSFStackItem))) == NULL)
        {
            THERR("Could not allocate memory for format item stack.\n");
            return FALSE;
        }
    }

    memcpy(stack->items + stack->nitems, item, sizeof(PSFStackItem));
    stack->nitems++;
    return TRUE;
}


void siClearStack(PSFStack *stack)
{
    if (stack != NULL)
    {
        if (stack->nitems > 0 && stack->items != NULL)
        {
            for (int n = 0; n < stack->nitems; n++)
            {
                PSFStackItem *item = &stack->items[n];
                th_free(item->str);
                th_free(item->fmt);
            }
            th_free(stack->items);
        }

        // Clear the stack data
        memset(stack, 0, sizeof(PSFStack));
    }
}


BOOL argParsePSFields(PSFStack *stack, const char *fmt)
{
    const char *start = fmt;
    siClearStack(stack);

    while (*start)
    {
        const char *end = strchr(start, ',');
        char *field = (end != NULL) ?
            th_strndup_trim(start, end - start, TH_TRIM_BOTH) :
            th_strdup_trim(start, TH_TRIM_BOTH);

        if (field != NULL)
        {
            PSFStackItem item;
            int found = argMatchPSFieldError(field);
            th_free(field);

            if (found < 0)
                return FALSE;

            memset(&item, 0, sizeof(item));
            item.cmd = found;
            item.fmt = th_strdup(optPSOptions[found].dfmt);

            if (!siStackAddItem(stack, &item))
                return FALSE;
        }

        if (!end)
            break;

        start = end + 1;
    }

    return TRUE;
}


#ifndef HAVE_ICONV

static const uint8_t si_lang_iso88591_to_cp850[16*6] = {
0xff, 0xad, 0xbd, 0x9c, 0xcf, 0xbe, 0xdd, 0xf5, 0xf9, 0xb8, 0xa6, 0xae, 0xaa, 0xf0, 0xa9, 0xee,
0xf8, 0xf1, 0xfd, 0xfc, 0xef, 0xe6, 0xf4, 0xfa, 0xf7, 0xfb, 0xa7, 0xaf, 0xac, 0xab, 0xf3, 0xa8,
0xb7, 0xb5, 0xb6, 0xc7, 0x8e, 0x8f, 0x92, 0x80, 0xd4, 0x90, 0xd2, 0xd3, 0xde, 0xd6, 0xd7, 0xd8,
0xd1, 0xa5, 0xe3, 0xe0, 0xe2, 0xe5, 0x99, 0x9e, 0x9d, 0xeb, 0xe9, 0xea, 0x9a, 0xed, 0xe8, 0xe1,
0x85, 0xa0, 0x83, 0xc6, 0x84, 0x86, 0x91, 0x87, 0x8a, 0x82, 0x88, 0x89, 0x8d, 0xa1, 0x8c, 0x8b,
0xd0, 0xa4, 0x95, 0xa2, 0x93, 0xe4, 0x94, 0xf6, 0x9b, 0x97, 0xa3, 0x96, 0x81, 0xec, 0xe7, 0x98,
};

static const uint8_t si_lang_iso88591_to_cp437[16*6] = {
0xff, 0xad, 0x9b, 0x9c, 0x00, 0x9d, 0x00, 0x00, 0x00, 0x00, 0xa6, 0xae, 0xaa, 0x00, 0x00, 0x00,
0xf8, 0xf1, 0xfd, 0x00, 0x00, 0xe6, 0x00, 0xfa, 0x00, 0x00, 0xa7, 0xaf, 0xac, 0xab, 0x00, 0xa8,
0x00, 0x00, 0x00, 0x00, 0x8e, 0x8f, 0x92, 0x80, 0x00, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xa5, 0x00, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0x00, 0x00, 0xe1,
0x85, 0xa0, 0x83, 0x00, 0x84, 0x86, 0x91, 0x87, 0x8a, 0x82, 0x88, 0x89, 0x8d, 0xa1, 0x8c, 0x8b,
0x00, 0xa4, 0x95, 0xa2, 0x93, 0x00, 0x94, 0xf6, 0x00, 0x97, 0xa3, 0x96, 0x81, 0x00, 0x00, 0x98,
};

#endif


char *siConvertCharset(const char *src)
{
#ifdef HAVE_ICONV
    size_t srcLeft = strlen(src) + 1;
    size_t outLeft = srcLeft * 2;
    char *srcPtr = (char *) src;
    char *outBuf, *outPtr;

    if ((outBuf = outPtr = th_malloc(outLeft + 1)) == NULL)
        return NULL;

    while (srcLeft > 0)
    {
        size_t ret = iconv(setIConvCtx, &srcPtr, &srcLeft, &outPtr, &outLeft);
        if (ret == (size_t) -1)
            break;
    }

#else
    // Fallback conversion of ISO-8859-1 to X
    size_t srcSize = strlen(src), outSize, minLeft;
    const uint8_t *srcPtr = (const uint8_t *) src;
    const uint8_t *tab;
    uint8_t *outBuf, *outPtr;

    switch (setOutLang)
    {
        case TH_LANG_UTF8:
            outSize = srcSize * 2;
            minLeft = 2;
            break;

        default:
            outSize = srcSize;
            minLeft = 1;
    }

    if ((outBuf = outPtr = th_malloc(outSize)) == NULL)
        return NULL;

    while (srcSize > 0 && outSize >= minLeft)
    {
        switch (setOutLang)
        {
            case TH_LANG_UTF8:
                // Not 100% correct really, but close enough
                if (*srcPtr < 0x80)
                {
                    *outPtr++ = *srcPtr;
                    outSize--;
                }
                else
                if (*srcPtr < 0xBF)
                {
                    *outPtr++ = 0xC2;
                    *outPtr++ = *srcPtr;
                    outSize -= 2;
                }
                else
                {
                    *outPtr++ = 0xC3;
                    *outPtr++ = *srcPtr - 0x40;
                    outSize -= 2;
                }
                break;

            case TH_LANG_ISO88591:
                *outPtr++ = *srcPtr;
                outSize--;
                break;

            case TH_LANG_CP850:
            case TH_LANG_CP437:
                // Not 100% correct either, but close enough
                tab = (setOutLang == TH_LANG_CP850) ? si_lang_iso88591_to_cp850 : si_lang_iso88591_to_cp437;

                if (*srcPtr < 0x7f)
                    *outPtr++ = *srcPtr;
                else
                if (*srcPtr >= 0xA0)
                    *outPtr++ = tab[*srcPtr - 0xA0];
                else
                    *outPtr++ = '?';

                outSize--;
                break;
        }

        srcPtr++;
        srcSize--;
    }

    *outPtr++ = 0;
#endif

    return (char *) outBuf;
}


static int siItemFormatStrPutInt(th_vprintf_ctx *ctx, th_vprintf_putch vputch,
    const int value, const int f_radix, int f_flags, int f_width, int f_prec,
    const BOOL f_unsig, th_vprintf_altfmt_func f_alt)
{
    char buf[64];
    int f_len = 0, vret;
    BOOL f_neg = FALSE;

    vret = th_vprintf_buf_int(buf, sizeof(buf), &f_len, value,
         f_radix, f_flags & TH_PF_UPCASE, f_unsig, &f_neg);

    if (vret == EOF)
        return 0;

    return th_vprintf_put_int_format(ctx, vputch, buf, f_flags, f_width, f_prec, f_len, vret, f_neg, f_unsig, f_alt);
}


static int siItemFormatStrPrintDo(th_vprintf_ctx *ctx,
    th_vprintf_putch vputch, const char *fmt,
    const int otype, const char *d_str, const int d_int)
{
    int ret = 0;

    while (*fmt)
    {
        if (*fmt != '%')
        {
            if ((ret = vputch(ctx, *fmt)) == EOF)
                goto out;
        }
        else
        {
            int f_width = -1, f_prec = -1, f_flags = 0;
            BOOL end = FALSE;

            fmt++;

            // Check for flags
            while (!end)
            {
                switch (*fmt)
                {
                    case '#':
                        f_flags |= TH_PF_ALT;
                        break;

                    case '+':
                        f_flags |= TH_PF_SIGN;
                        break;

                    case '0':
                        f_flags |= TH_PF_ZERO;
                        break;

                    case '-':
                        f_flags |= TH_PF_LEFT;
                        break;

                    case ' ':
                        f_flags |= TH_PF_SPACE;
                        break;

                    case '\'':
                        f_flags |= TH_PF_GROUP;
                        break;

                    default:
                        end = TRUE;
                        break;
                }
                if (!end) fmt++;
            }

            // Get field width
            if (*fmt == '*')
            {
                return -101;
            }
            else
            {
                f_width = 0;
                while (th_isdigit(*fmt))
                    f_width = f_width * 10 + (*fmt++ - '0');
            }

            // Check for field precision
            if (*fmt == '.')
            {
                fmt++;
                if (*fmt == '*')
                {
                    return -102;
                }
                else
                {
                    // If no digit after '.', precision is to be 0
                    f_prec = 0;
                    while (th_isdigit(*fmt))
                        f_prec = f_prec * 10 + (*fmt++ - '0');
                }
            }


            // Check for length modifiers (only some are supported currently)
            switch (*fmt)
            {
                case 0:
                    return -104;

                case 'o':
                    if (otype != OTYPE_INT) return -120;
                    if ((ret = siItemFormatStrPutInt(ctx, vputch, d_int, 8, f_flags, f_width, f_prec, TRUE, th_vprintf_altfmt_oct)) == EOF)
                        goto out;
                    break;

                case 'u':
                case 'i':
                case 'd':
                    if (otype != OTYPE_INT) return -120;
                    if ((ret = siItemFormatStrPutInt(ctx, vputch, d_int, 10, f_flags, f_width, f_prec, *fmt == 'u', NULL)) == EOF)
                        goto out;
                    break;

                case 'x':
                case 'X':
                    if (otype != OTYPE_INT) return -120;
                    if (*fmt == 'X')
                        f_flags |= TH_PF_UPCASE;
                    if ((ret = siItemFormatStrPutInt(ctx, vputch, d_int, 16, f_flags, f_width, f_prec, TRUE, th_vprintf_altfmt_hex)) == EOF)
                        goto out;
                    break;

                case 's':
                    if (otype != OTYPE_STR) return -121;
                    if ((ret = th_vprintf_put_str(ctx, vputch, d_str, f_flags, f_width, f_prec)) == EOF)
                        goto out;
                    break;

                //case '%':
                default:
                    if ((ret = vputch(ctx, *fmt)) == EOF)
                        goto out;
                    break;
            }
        }
        fmt++;
    }

out:
    return ret == EOF ? ret : ctx->ipos;
}


static int siItemFormatStrPutCH(th_vprintf_ctx *ctx, const char ch)
{
    if (ctx->pos + 1 >= ctx->size)
    {
        ctx->size += 64;
        if ((ctx->buf = th_realloc(ctx->buf, ctx->size)) == NULL)
            return EOF;
    }

    ctx->buf[ctx->pos] = ch;

    ctx->pos++;
    ctx->ipos++;
    return ch;
}


char * siItemFormatStrPrint(const char *fmt, const int otype, const char *d_str, const int d_int)
{
    th_vprintf_ctx ctx;

    ctx.size = 128;
    ctx.buf = th_malloc(ctx.size);
    ctx.pos = 0;
    ctx.ipos = 0;

    if (ctx.buf == NULL)
        return NULL;

    if (siItemFormatStrPrintDo(&ctx, siItemFormatStrPutCH, fmt, otype, d_str, d_int) <= 0)
        goto err;

    if (siItemFormatStrPutCH(&ctx, 0) < 0)
        goto err;

    return ctx.buf;

err:
    th_free(ctx.buf);
    return NULL;
}


static int siItemFormatStrPutCHNone(th_vprintf_ctx *ctx, const char ch)
{
    ctx->pos++;
    ctx->ipos++;
    return ch;
}


static BOOL siItemFormatStrCheck(const char *fmt, const PSFOption *opt)
{
    th_vprintf_ctx ctx;

    memset(&ctx, 0, sizeof(ctx));

    return siItemFormatStrPrintDo(&ctx, siItemFormatStrPutCHNone, fmt, opt->type, NULL, 0) >= 0;
}


//
// Parse a format string into a PSFStack structure
//
static BOOL argParsePSFormatStr(PSFStack *stack, const char *fmt)
{
    const char *start = NULL;
    int mode = 0;
    BOOL rval = TRUE;

    siClearStack(stack);

    while (mode != -1)
    switch (mode)
    {
        case 0:
            if (*fmt == '@')
            {
                start = fmt + 1;
                mode = 1;
            }
            else
            {
                start = fmt;
                mode = 2;
            }
            fmt++;
            break;

        case 1:
            if (*fmt != '@')
            {
                if (*fmt == 0)
                    mode = -1;
                fmt++;
                break;
            }

            if (fmt - start == 0)
            {
                // "@@" sequence, just print out @
                PSFStackItem item;
                memset(&item, 0, sizeof(item));
                item.cmd = -2;
                item.chr = '@';

                if (!siStackAddItem(stack, &item))
                    return FALSE;
            }
            else
            {
                char *fopt = NULL, *pfield, *field = th_strndup_trim(start, fmt - start, TH_TRIM_BOTH);
                if ((pfield = strchr(field, ':')) != NULL)
                {
                    *pfield = 0;
                    fopt = th_strdup_trim(pfield + 1, TH_TRIM_BOTH);
                }

                int ret = argMatchPSFieldError(field);
                if (ret >= 0)
                {
                    PSFStackItem item;
                    memset(&item, 0, sizeof(item));
                    item.cmd = ret;

                    if (fopt != NULL)
                    {
                        if (siItemFormatStrCheck(fopt, &optPSOptions[item.cmd]))
                        {
                            item.flags |= OFMT_FORMAT;
                            item.fmt = th_strdup(fopt);
                        }
                        else
                        {
                            THERR("Invalid field format specifier '%s' in '%s'.\n", fopt, field);
                            rval = FALSE;
                        }
                    }

                    if (!siStackAddItem(stack, &item))
                        rval = FALSE;
                }
                else
                    rval = FALSE;

                th_free(fopt);
                th_free(field);
            }

            mode = 0;
            fmt++;
            break;

        case 2:
            if (*fmt == 0 || *fmt == '@')
            {
                PSFStackItem item;
                memset(&item, 0, sizeof(item));
                item.cmd = -1;
                item.str = th_strndup(start, fmt - start);

                if (!siStackAddItem(stack, &item))
                    return FALSE;

                mode = (*fmt == 0) ? -1 : 0;
            }
            else
                fmt++;
            break;
    }

    return rval;
}


static BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    switch (optN)
    {
    case 0:
        argShowHelp();
        exit(0);
        break;

    case 10:
        argShowLicense();
        exit(0);
        break;

    case 1:
        th_verbosity++;
        break;

    case 2:
        optParsable = TRUE;
        break;

    case 3:
        if (!argParsePSFields(&optFormat, optArg))
            return FALSE;
        break;

    case 4:
        optHexadecimal = TRUE;
        break;

    case 5:
        optFieldNamePrefix = FALSE;
        break;

    case 6:
        optOneLineFieldSep = optArg;
        break;

    case 7:
        optFieldOutput = FALSE;
        optParsable = FALSE;
        if (!argParsePSFormatStr(&optFormat, optArg))
            return FALSE;
        break;

    case 8:
        th_pstr_cpy(&setHVSCPath, optArg);
        break;

    case 9:
        th_pstr_cpy(&setSLDBPath, optArg);
        break;

    case 13:
        th_pstr_cpy(&setSTILDBPath, optArg);
        break;

    case 11:
        optEscapeChars = optArg;
        break;

    case 12:
        optRecurseDirs = TRUE;
        break;

    default:
        THERR("Unknown option '%s'.\n", currArg);
        return FALSE;
    }

    return TRUE;
}


static char * siEscapeString(const char *str, const char *esc)
{
    if (str == NULL)
        return NULL;

    if (esc == NULL)
        return th_strdup(str);

    size_t len = 0, size = strlen(str) + 1;
    char *buf = th_malloc(size);
    if (buf == NULL)
        return NULL;

    while (*str)
    {
        if (strchr(esc, *str) != NULL || *str == '\\')
        {
            if (!th_strbuf_putch(&buf, &size, &len, '\\'))
                goto err;
        }
        if (!th_strbuf_putch(&buf, &size, &len, *str))
            goto err;

        str++;
    }

    if (!th_strbuf_putch(&buf, &size, &len, 0))
        goto err;

    return buf;

err:
    th_free(buf);
    return NULL;
}


static void siPrintStrEscapes(FILE *outFile, const char *str)
{
    while (*str)
    {
        if (*str == '\\')
        switch (*(++str))
        {
            case 'n': fputc('\n', outFile); break;
            case 'r': fputc('\r', outFile); break;
            case 't': fputc('\r', outFile); break;
            case '\\': fputc('\\', outFile); break;
            default: fputc(*str, outFile); break;
        }
        else
            fputc(*str, outFile);

        str++;
    }
}


static void siPrintFieldPrefixName(FILE *outFile, const char *name, const BOOL multifield)
{
    if (optFieldNamePrefix)
    {
        if (optFieldOutput && optOneLineFieldSep == NULL)
            fprintf(outFile, optParsable ? "%s=" : "%-20s : ", name);
        else
        if (multifield)
            fprintf(outFile, "[%s] ", name);
    }
}


static void siPrintFieldPrefix(FILE *outFile, const PSFOption *opt)
{
    siPrintFieldPrefixName(outFile,
        (optParsable || opt->lname == NULL) ? opt->name : opt->lname,
        FALSE);
}


static void siPrintFieldSeparator(FILE *outFile, const BOOL multifield, const BOOL last)
{
    if (optFieldOutput)
        fputs(optOneLineFieldSep != NULL ? optOneLineFieldSep : "\n", outFile);
    else
    if (multifield && !last)
        fputs(optOneLineFieldSep != NULL ? optOneLineFieldSep : ", ", outFile);
}


static const char *siGetInfoFormat(const PSFStackItem *item, const int otype)
{
    switch (otype)
    {
        case OTYPE_INT:
            if (!optParsable && item->fmt != NULL)
                return item->fmt;
            else
                return optHexadecimal ? "$%04x" : "%d";

        case OTYPE_STR:
            if (!optParsable && item->fmt != NULL)
                return item->fmt;
            else
                return "%s";

        default:
            return NULL;
    }
}


static void siPrintPSIDInfoLine(FILE *outFile, BOOL *shown,
    const char *fmt, const int otype,
    const char *d_str, const int d_int,
    const BOOL convert)
{
    char *str, *tmp;

    if (setUseOutConv && d_str != NULL && convert)
    {
        char *tmp2 = siConvertCharset(d_str);
        tmp = siEscapeString(tmp2, optEscapeChars);
        th_free(tmp2);
    }
    else
        tmp = siEscapeString(d_str, optEscapeChars);

    if ((str = siItemFormatStrPrint(fmt, otype, tmp, d_int)) != NULL)
        fputs(str, outFile);

    th_free(str);
    th_free(tmp);

    *shown = TRUE;
}


#define PRS(d_str, d_conv) do { \
        siPrintFieldPrefix(outFile, opt); \
        siPrintPSIDInfoLine(outFile, shown, siGetInfoFormat(item, opt->type), opt->type, d_str, -1, d_conv); \
        siPrintFieldSeparator(outFile, FALSE, TRUE); \
    } while (0)

#define PRI(d_int) do { \
        siPrintFieldPrefix(outFile, opt); \
        siPrintPSIDInfoLine(outFile, shown, siGetInfoFormat(item, opt->type), opt->type, NULL, d_int, FALSE); \
        siPrintFieldSeparator(outFile, FALSE, TRUE); \
    } while (0)


static void siPrintPSIDInformationField(FILE *outFile, const char *filename,
    const SIDLibPSIDHeader *psid, BOOL *shown, const PSFStackItem *item)
{
    const PSFOption *opt = &optPSOptions[item->cmd];
    char tmp[128];

    switch (item->cmd)
    {
        case  0: PRS(filename, FALSE); break;
        case  1: PRS(psid->magic, FALSE); break;
        case  2:
            snprintf(tmp, sizeof(tmp), "%d.%d", (psid->version & 0xff), (psid->version >> 8));
            PRS(tmp, FALSE);
            break;
        case  3:
            PRS((psid->flags & PSF_PLAYER_TYPE) ? "Compute! SIDPlayer MUS" : "Normal built-in", FALSE);
            break;
        case  4:
            if (psid->version >= 2)
                PRS((psid->flags & PSF_PLAYSID_TUNE) ? (psid->isRSID ? "C64 BASIC" : "PlaySID") : "C64 compatible", FALSE);
            break;
        case  5:
            if (psid->version >= 2)
                PRS(sidlib_get_sid_clock_str((psid->flags >> 2) & PSF_CLOCK_MASK), FALSE);
            break;
        case  6:
            if (psid->version >= 2)
                PRS(sidlib_get_sid_model_str((psid->flags >> 4) & PSF_MODEL_MASK), FALSE);
            break;

        case  7: PRI(psid->dataOffset); break;
        case  8: PRI(psid->dataSize); break;
        case  9: PRI(psid->loadAddress); break;
        case 10: PRI(psid->initAddress); break;
        case 11: PRI(psid->playAddress); break;
        case 12: PRI(psid->nSongs); break;
        case 13: PRI(psid->startSong); break;

        case 14:
            if (psid->version >= 3)
            {
                int flags = (psid->flags >> 6) & PSF_MODEL_MASK;
                if (flags == PSF_MODEL_UNKNOWN)
                    flags = (psid->flags >> 4) & PSF_MODEL_MASK;

                PRS(sidlib_get_sid_model_str(flags), FALSE);
            }
            break;
        case 15:
            if (psid->version >= 4)
            {
                int flags = (psid->flags >> 8) & PSF_MODEL_MASK;
                if (flags == PSF_MODEL_UNKNOWN)
                    flags = (psid->flags >> 4) & PSF_MODEL_MASK;

                PRS(sidlib_get_sid_model_str(flags), FALSE);
            }
            break;
        case 16:
            if (psid->version >= 3)
                PRI(0xD000 | (psid->sid2Addr << 4));
            break;
        case 17:
            if (psid->version >= 4)
                PRI(0xD000 | (psid->sid3Addr << 4));
            break;

        case 18: PRS(psid->sidName, TRUE); break;
        case 19: PRS(psid->sidAuthor, TRUE); break;
        case 20: PRS(psid->sidCopyright, TRUE); break;

        case 21:
            {
                size_t i, k;
                for (i = k = 0; i < TH_MD5HASH_LENGTH && k < sizeof(tmp) - 1; i++, k += 2)
                    sprintf(&tmp[k], "%02x", psid->hash[i]);

                PRS(tmp, FALSE);
            }
            break;

        case 22:
            if (psid->lengths != NULL && psid->lengths->nlengths > 0)
            {
                siPrintFieldPrefix(outFile, opt);
                for (int i = 0; i < psid->lengths->nlengths; i++)
                {
                    int len = psid->lengths->lengths[i];

                    snprintf(tmp, sizeof(tmp), "%d:%02d%s",
                        len / 60, len % 60,
                        (i < psid->lengths->nlengths - 1) ? ", " : "");

                    siPrintPSIDInfoLine(outFile, shown,
                        siGetInfoFormat(item, OTYPE_STR),
                        OTYPE_STR,
                        tmp,
                        -1, FALSE);
                }
                siPrintFieldSeparator(outFile, FALSE, TRUE);
            }
            break;

        case 23:
            if (psid->stil != NULL)
            {
                int nfieldn = 0, nfieldcount = 0;

                // We need to count the number of fields to be outputted
                // beforehand, so we can know when we are at the last one
                for (size_t nsubtune = 0; nsubtune < psid->stil->nsubtunes; nsubtune++)
                {
                    SIDLibSTILSubTune *subtune = &psid->stil->subtunes[nsubtune];
                    for (int nfield = 0; nfield < STF_LAST; nfield++)
                        nfieldcount += subtune->fields[nfield].ndata;
                }

                for (size_t nsubtune = 0; nsubtune < psid->stil->nsubtunes; nsubtune++)
                {
                    SIDLibSTILSubTune *subtune = &psid->stil->subtunes[nsubtune];
                    int maxdata = 0;

                    // For each subtune we need to check the max number of field data items
                    for (int nfield = 0; nfield < STF_LAST; nfield++)
                    {
                        SIDLibSTILField *fld = &subtune->fields[nfield];
                        if (fld->ndata > maxdata)
                            maxdata = fld->ndata;
                    }

                    // Now output the items in field order
                    for (int nitem = 0; nitem < maxdata; nitem++)
                    for (int nfield = 0; nfield < STF_LAST; nfield++)
                    {
                        SIDLibSTILField *fld = &subtune->fields[nfield];
                        if (nitem < fld->ndata)
                        {
                            if (nsubtune > 0)
                            {
                                snprintf(tmp, sizeof(tmp), "STIL#%d/%s",
                                    subtune->tune, sidlib_stil_fields[nfield]);
                            }
                            else
                            {
                                snprintf(tmp, sizeof(tmp), "STIL/%s",
                                    sidlib_stil_fields[nfield]);
                            }

                            siPrintFieldPrefixName(outFile, tmp, TRUE);
                            siPrintPSIDInfoLine(outFile, shown,
                                siGetInfoFormat(item, OTYPE_STR),
                                OTYPE_STR,
                                fld->data[nitem],
                                -1, TRUE);

                            siPrintFieldSeparator(outFile, TRUE,
                                ++nfieldn >= nfieldcount);
                        }
                    }
                }
            }
            break;
    }
}


void siPSIDError(th_ioctx *fh, const int err, const char *msg)
{
    (void) err;
    THERR("%s - %s\n", msg, fh->filename);
}


void siSTILError(th_ioctx *fh, const int err, const char *msg)
{
    (void) err;
    THERR("[%s:%" PRIu_SIZE_T "] %s\n", fh->filename, fh->line, msg);
}


BOOL siHandleSIDFile(const char *filename)
{
    SIDLibPSIDHeader *psid = NULL;
    th_ioctx *inFile = NULL;
    FILE *outFile;
    BOOL shown = FALSE;
    int res;

    outFile = stdout;

    if ((res = th_io_fopen(&inFile, &th_stdio_io_ops, filename, "rb")) != THERR_OK)
    {
        THERR("Could not open file '%s': %s\n",
            filename, th_error_str(res));
        goto error;
    }

    th_io_set_handlers(inFile, siPSIDError, NULL);

    // Read PSID data
    if ((res = sidlib_read_sid_file_alloc(inFile, &psid, setSLDBNewFormat)) != THERR_OK)
        goto error;

    // Get songlength information, if any
    if (sidSLDB != NULL)
        psid->lengths = sidlib_sldb_get_by_hash(sidSLDB, psid->hash);

    // Get STIL information, if any
    if (sidSTILDB != NULL)
    {
        psid->stil = sidlib_stildb_get_node(sidSTILDB, siStripHVSCPath(filename));
        if (psid->stil != NULL)
            psid->stil->lengths = psid->lengths;
    }

    // Output
    for (int index = 0; index < optFormat.nitems; index++)
    {
        PSFStackItem *item = &optFormat.items[index];
        switch (item->cmd)
        {
            case -1:
                siPrintStrEscapes(outFile, item->str);
                break;

            case -2:
                fputc(item->chr, outFile);
                break;

            default:
                siPrintPSIDInformationField(outFile, filename, psid, &shown, item);
                break;
        }
    }

    if (optFieldOutput && shown)
    {
        fprintf(outFile, "\n");
    }

    // Shutdown
error:
    sidlib_free_sid_file(psid);
    th_io_free(inFile);

    return TRUE;
}


BOOL argHandleFileDir(const char *path, const char *filename, const char *pattern)
{
    th_stat_data sdata;
    char *npath;
    BOOL ret = TRUE;

    if (filename != NULL)
        npath = th_strdup_printf("%s%c%s", path, TH_DIR_SEPARATOR_CHR, filename);
    else
        npath = th_strdup(path);

    if (!th_stat_path(npath, &sdata))
    {
        THERR("File or path '%s' does not exist.\n", npath);
        ret = FALSE;
        goto out;
    }

    optNFiles++;

    if (sdata.flags & TH_IS_DIR)
    {
        DIR *dirh;
        struct dirent *entry;

        // Check if recursion is disabled
        if (!optRecurseDirs && optNFiles > 1)
            goto out;

        if ((dirh = opendir(npath)) == NULL)
        {
            int err = th_get_error();
            THERR("Could not open directory '%s': %s\n",
                path, th_error_str(err));
            ret = FALSE;
            goto out;
        }

        while ((entry = readdir(dirh)) != NULL)
        if (entry->d_name[0] != '.')
        {
            if (!argHandleFileDir(npath, entry->d_name, pattern))
            {
                ret = FALSE;
                goto out;
            }
        }

        closedir(dirh);
    }
    else
    if (pattern == NULL || th_strmatch(filename, pattern))
    {
        siHandleSIDFile(npath);
    }

out:
    th_free(npath);
    return ret;
}


BOOL argHandleFile(char *path)
{
    char *pattern = NULL, *filename = NULL, *pt, *npath;
    BOOL ret;

    if ((npath = th_strdup(path)) == NULL)
        return FALSE;

    // Check if we have path separators
    if ((pt = strrchr(npath, '/')) != NULL ||
        (pt = strrchr(npath, '\\')) != NULL)
    {
        *pt++ = 0;
    }
    else
    {
        th_free(npath);
        npath = th_strdup(".");
        pt = strcmp(path, npath) != 0 ? path : NULL;
    }

    // Check if we have glob pattern chars
    if (pt != NULL && *pt != 0)
    {
        if (strchr(pt, '*') || strchr(pt, '?'))
            pattern = th_strdup(pt);
        else
            filename = th_strdup(pt);
    }

    ret = argHandleFileDir(npath, filename, pattern);

    th_free(pattern);
    th_free(npath);
    th_free(filename);
    return ret;
}


int main(int argc, char *argv[])
{
    char *setLang = th_strdup(getenv("LANG"));
    th_ioctx *inFile = NULL;

    // Get HVSC_BASE env variable if it is set
    th_pstr_cpy(&setHVSCPath, getenv("HVSC_BASE"));

    // Initialize
    th_init("SIDInfo", "PSID/RSID information displayer", "0.9.2",
        "By Matti 'ccr' Hamalainen (C) Copyright 2014-2020 TNSP",
        "This program is distributed under a 3-clause BSD -style license.");

    th_verbosity = 0;

    memset(&optFormat, 0, sizeof(optFormat));

    // Get environment language
    if (setLang != NULL)
    {
        // Get the character encoding part (e.g. "UTF-8" etc.) and
        // strip out and lowercase everything (e.g. "utf8")
        size_t i;
        char *ptr = strchr(setLang, '.');
        ptr = (ptr == NULL) ? setLang : ptr + 1;

        for (i = 0; *ptr; ptr++)
        {
            if (*ptr != '-')
                setLang[i++] = th_tolower(*ptr);
        }
        setLang[i] = 0;

#ifdef HAVE_ICONV
        // Initialize iconv, check if we have language/charset
        setIConvCtx = iconv_open("utf8", "iso88591");
        setUseOutConv = setIConvCtx != (iconv_t) -1;
#else
        // Check if we can use our fallback converter
        if (strcmp(setLang, "utf8") == 0)
            setOutLang = TH_LANG_UTF8;
        else
        if (strcmp(setLang, "iso88591") == 0 ||
            strcmp(setLang, "cp819") == 0 ||
            strcmp(setLang, "latin1") == 0 ||
            strcmp(setLang, "cp28591") == 0)
            setOutLang = TH_LANG_ISO88591;
        else
        if (strcmp(setLang, "cp850") == 0)
            setOutLang = TH_LANG_CP850;
        else
        if (strcmp(setLang, "cp437") == 0)
            setOutLang = TH_LANG_CP437;
        else
            setOutLang = TH_LANG_ISO88591;

        setUseOutConv = setOutLang != TH_LANG_ISO88591;
#endif
    }

    // Parse command line arguments
    if (!th_args_process(argc, argv, optList, optListN,
        argHandleOpt, NULL, OPTH_ONLY_OPTS))
        goto out;

    THMSG(2, "Requested output LANG='%s', use charset conversion=%s\n",
        setLang, setUseOutConv ? "yes" : "no");

    if (optOneLineFieldSep != NULL ||
        (!optFieldOutput && optFormat.nitems > 0))
    {
        // For one-line format and formatted output (-F), disable parsable
        optParsable = FALSE;

        // If no escape chars have been set, use the field separator(s)
        if (optEscapeChars == NULL)
            optEscapeChars = optOneLineFieldSep;
    }

    if (optFieldOutput && optFormat.nitems == 0)
    {
        // For standard field output, push standard items to format stack
        siClearStack(&optFormat);

        for (int i = 0; i < noptPSOptions; i++)
        {
            PSFStackItem item;
            memset(&item, 0, sizeof(item));
            item.cmd = i;
            item.fmt = th_strdup(optPSOptions[i].dfmt);
            siStackAddItem(&optFormat, &item);
        }
    }

    // Check if HVSC path is set
    if (setHVSCPath != NULL)
    {
        // Ensure that there is a path separator at the end
        if (th_strrcasecmp(setHVSCPath, TH_DIR_SEPARATOR_STR) == NULL)
            th_pstr_printf(&setHVSCPath, "%s%c", setHVSCPath, TH_DIR_SEPARATOR_CHR);

        // If SLDB path is not set, autocheck for .md5 and .txt
        if (setSLDBPath == NULL)
            setSLDBPath = siCheckHVSCFilePath(SET_SLDB_FILEBASE, ".md5");

        if (setSLDBPath == NULL)
            setSLDBPath = siCheckHVSCFilePath(SET_SLDB_FILEBASE, ".txt");

        if (setSTILDBPath == NULL)
            setSTILDBPath = siCheckHVSCFilePath(SET_STILDB_FILENAME, NULL);
    }

    if (setSLDBPath != NULL)
    {
        // Initialize SLDB
        int ret = THERR_OK;

        setSLDBNewFormat = th_strrcasecmp(setSLDBPath, ".md5") != NULL;

        if ((ret = th_io_fopen(&inFile, &th_stdio_io_ops, setSLDBPath, "r")) != THERR_OK)
        {
            THERR("Could not open SLDB '%s': %s\n",
                setSLDBPath, th_error_str(ret));
            goto err1;
        }

        th_io_set_handlers(inFile, siSTILError, NULL);

        THMSG(1, "Reading SLDB (%s format [%s]): %s\n",
            setSLDBNewFormat ? "new" : "old",
            setSLDBNewFormat ? ".md5" : ".txt",
            setSLDBPath);

        if ((ret = sidlib_sldb_new(&sidSLDB)) != THERR_OK)
        {
            THERR("Could not allocate SLDB database structure: %s\n",
                th_error_str(ret));
            goto err1;
        }

        if ((ret = sidlib_sldb_read(inFile, sidSLDB)) != THERR_OK)
        {
            THERR("Error parsing SLDB: %s\n",
                th_error_str(ret));
            goto err1;
        }

        if ((ret = sidlib_sldb_build_index(sidSLDB)) != THERR_OK)
        {
            THERR("Error building SLDB index: %s\n",
                th_error_str(ret));
            goto err1;
        }

err1:
        th_io_free(inFile);
        inFile = NULL;
    }

    if (setSTILDBPath != NULL)
    {
        // Initialize STILDB
        int ret = THERR_OK;

        if ((ret = th_io_fopen(&inFile, &th_stdio_io_ops, setSTILDBPath, "r")) != THERR_OK)
        {
            THERR("Could not open STIL database '%s': %s\n",
                setSTILDBPath, th_error_str(ret));
            goto err2;
        }

        th_io_set_handlers(inFile, siSTILError, NULL);

        THMSG(1, "Reading STIL database: %s\n",
            setSTILDBPath);

        if ((ret = sidlib_stildb_new(&sidSTILDB)) != THERR_OK)
        {
            THERR("Could not allocate STIL database structure: %s\n",
                th_error_str(ret));
            goto err2;
        }

        if ((ret = sidlib_stildb_read(inFile, sidSTILDB)) != THERR_OK)
        {
            THERR("Error parsing STIL: %s\n",
                th_error_str(ret));
            goto err2;
        }

        if ((ret = sidlib_stildb_build_index(sidSTILDB)) != THERR_OK)
        {
            THERR("Error building STIL index: %s\n",
                th_error_str(ret));
            goto err2;
        }

err2:
        th_io_free(inFile);
        inFile = NULL;
    }

    // Process files
    if (!th_args_process(argc, argv, optList, optListN,
        NULL, argHandleFile, OPTH_ONLY_OTHER))
        goto out;

    if (optNFiles == 0)
    {
        THERR("No filename(s) specified. Try --help.\n");
    }

out:

#ifdef HAVE_ICONV
    if (setUseOutConv)
        iconv_close(setIConvCtx);
#endif

    th_free(setLang);

    siClearStack(&optFormat);
    th_free(setHVSCPath);
    th_free(setSLDBPath);
    th_free(setSTILDBPath);

    sidlib_sldb_free(sidSLDB);
    sidlib_stildb_free(sidSTILDB);

    return 0;
}