view sidinfo.c @ 339:923e63b9653b

Cleanup.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 13 Jan 2020 15:14:16 +0200
parents 7ad937740139
children 6f8c431a3040
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 *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 (d_str != NULL && setChConv.enabled && convert)
    {
        char *tmp2 = sidutil_chconv_convert(&setChConv, d_str);
        tmp = sidutil_escape_string(tmp2, optEscapeChars);
        th_free(tmp2);
    }
    else
        tmp = sidutil_escape_string(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, 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(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, *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 *inFile = 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(&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
        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, 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(inFile);
        inFile = 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;
}