view sidinfo.c @ 341:fe061ead51cc

Perform character set conversion after item formatting step instead of before it. This should remedy the potential issue of formatting not taking UTF8 multibyte into account, as our formatting unfortunately does not support multibyte encoding. This way the data should stay in ISO-8859-1 format just up to outputting it.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 13 Jan 2020 16:29:47 +0200
parents 6f8c431a3040
children 020c4f21e861
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 "sidutil.h"
#include <sys/types.h>
#include <dirent.h>


//
// Some constants
//
enum
{
    OFMT_QUOTED    = 0x0001,
    OFMT_FORMAT    = 0x0002,
};


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


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 SET_FMT_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    , SET_FMT_HEX_ADDR },
    { "DataSize"     , "Data size"            , OTYPE_INT    , SET_FMT_HEX_ADDR },
    { "LoadAddr"     , "Load address"         , OTYPE_INT    , SET_FMT_HEX_ADDR },
    { "InitAddr"     , "Init address"         , OTYPE_INT    , SET_FMT_HEX_ADDR },
    { "PlayAddr"     , "Play address"         , OTYPE_INT    , SET_FMT_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    , SET_FMT_HEX_ADDR },
    { "SID3Addr"     , "3rd SID address"      , OTYPE_INT    , SET_FMT_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,
        optShowHelp = FALSE;
char    *optOneLineFieldSep = NULL,
        *optEscapeChars = NULL;
int     optNFiles = 0;

PSFStack optFormat;

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

SIDUtilChConvCtx setChConv;


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

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

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


void argShowHelp(void)
{
    int index, width;

    th_print_banner(stdout, th_prog_name, "[options] <sid file|path> [file|path #2 ..]");
    th_args_help(stdout, optList, optListN, 0, 80 - 2);

    printf(
        "\n"
        "Available fields:\n");

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

    printf(
        "\n"
        "Example: %s -x -p -f hash,copyright somesidfile.sid\n"
        "\n"
        "Format strings for '-F' option are composed of @fields@ that get expanded\n"
        "to their value. Also, escape sequences \\r, \\n and \\t can be used:\n"
        "  -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"
        "HVSC path   : %s\n"
        "SLDB file   : %s\n"
        "STIL file   : %s\n"
        "\n",
        th_prog_name,
        setHVSCPath != NULL ? setHVSCPath : "[not set]",
        setSLDBPath != NULL ? setSLDBPath : "[not set]",
        setSTILDBPath != NULL ? setSTILDBPath : "[not set]"
        );
}


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;
}


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;
}


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.pos  = 0;
    ctx.ipos = 0;

    if ((ctx.buf = th_malloc(ctx.size)) == 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:
        optShowHelp = TRUE;
        break;

    case 1:
        sidutil_print_license();
        exit(0);
        break;

    case 2:
        th_verbosity++;
        break;

    case 3:
        optParsable = TRUE;
        break;

    case 4:
        optHexadecimal = TRUE;
        break;

    case 5:
        optFieldNamePrefix = FALSE;
        break;

    case 6:
        optOneLineFieldSep = optArg;
        break;

    case 7:
        optEscapeChars = optArg;
        break;

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

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

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

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

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

    case 13:
        optRecurseDirs = TRUE;
        break;

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

    return TRUE;
}


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


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


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


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 *outfh, BOOL *shown,
    const char *fmt, const int otype,
    const char *d_str, const int d_int,
    const BOOL convert)
{
    char *formatted, *escaped;

    escaped = sidutil_escape_string(d_str, optEscapeChars);

    if ((formatted = siItemFormatStrPrint(fmt, otype, escaped, d_int)) != NULL)
    {
        char *converted;
        if (setChConv.enabled && convert &&
            (converted = sidutil_chconv_convert(&setChConv, formatted)) != NULL)
        {
            fputs(converted, outfh);
            th_free(converted);
        }
        else
            fputs(formatted, outfh);
    }

    th_free(formatted);
    th_free(escaped);

    *shown = TRUE;
}


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

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


static void siPrintPSIDInformationField(FILE *outfh, 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(outfh, 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(outfh, shown,
                        siGetInfoFormat(item, OTYPE_STR),
                        OTYPE_STR,
                        tmp,
                        -1, FALSE);
                }
                siPrintFieldSeparator(outfh, 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(outfh, tmp, TRUE);
                            siPrintPSIDInfoLine(outfh, shown,
                                siGetInfoFormat(item, OTYPE_STR),
                                OTYPE_STR,
                                fld->data[nitem],
                                -1, TRUE);

                            siPrintFieldSeparator(outfh, 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 *infh = NULL;
    FILE *outfh;
    BOOL shown = FALSE;
    int res;

    outfh = stdout;

    if ((res = th_io_fopen(&infh, &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(infh, siPSIDError, NULL);

    // Read PSID data
    if ((res = sidlib_read_sid_file_alloc(infh, &psid, setSLDBNewFormat, NULL)) != 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,
            sidutil_strip_hvsc_path(setHVSCPath, 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:
                sidutil_print_string_escaped(outfh, item->str);
                break;

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

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

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

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

    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, *filename, *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
    pattern = filename = NULL;
    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[])
{
    th_ioctx *infh = NULL;
    char *setLang = getenv("LANG");
    int ret;

    // 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));
    memset(&setChConv, 0, sizeof(setChConv));

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

    // 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 = sidutil_check_hvsc_file(setHVSCPath, SIDUTIL_SLDB_FILEBASE, ".md5");

        if (setSLDBPath == NULL)
            setSLDBPath = sidutil_check_hvsc_file(setHVSCPath, SIDUTIL_SLDB_FILEBASE, ".txt");

        if (setSTILDBPath == NULL)
            setSTILDBPath = sidutil_check_hvsc_file(setHVSCPath, SIDUTIL_STILDB_FILENAME, NULL);
    }

    // Check if help is requested
    if (optShowHelp)
    {
        argShowHelp();
        goto exit;
    }

    // Initialize character set conversion
    if ((ret = sidutil_chconv_init(&setChConv, setLang)) != THERR_OK)
    {
        THERR("Could not initialize character set conversion (LANG='%s'): %s\n",
            setLang, th_error_str(ret));
    }

    THMSG(2, "Requested output LANG='%s', use charset conversion=%s\n",
        setChConv.outLang, setChConv.enabled ? "yes" : "no");

    // Check operation mode
    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);
        }
    }

    // Read SLDB and STILDB
    if (setSLDBPath != NULL)
    {
        // Initialize SLDB
        setSLDBNewFormat = th_strrcasecmp(setSLDBPath, ".md5") != NULL;

        if ((ret = th_io_fopen(&infh, &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(infh, 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(infh, 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(infh);
        infh = NULL;
    }

    if (setSTILDBPath != NULL)
    {
        // Initialize STILDB
        if ((ret = th_io_fopen(&infh, &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(infh, 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(infh, sidSTILDB, NULL)) != 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(infh);
        infh = NULL;
    }

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

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

exit:

    sidutil_chconv_close(&setChConv);

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

    sidlib_sldb_free(sidSLDB);
    sidlib_stildb_free(sidSTILDB);

    return 0;
}