view sidinfo.c @ 138:d40c371ce1f9 rel-0_6_2

Bump version.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 02 Jun 2017 04:39:46 +0300
parents b4b1aac8761c
children 9f96b37c4f75
line wrap: on
line source

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


// Some constants
#define SET_DEF_CHARSET "utf-8"
#define SET_SLDB_FILENAME "Songlengths.txt"


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


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


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


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


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

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

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

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

    { "Songlengths"  , "Song lengths"         , OTYPE_OTHER },
};

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


// Option variables
char   *setHVSCPath = NULL,
       *setSLDBPath = NULL;
BOOL	optParsable = FALSE,
        optNoNamePrefix = FALSE,
        optHexadecimal = FALSE,
        optOneLine = FALSE,
        optFieldOutput = TRUE;
char    *optFieldSep = NULL;
int     optNFiles = 0;

PSFStack optFormat;

SIDLibSLDB *sidSLDB = NULL;

#ifdef HAVE_ICONV
BOOL    setUseChConv;
iconv_t setChConv;
#endif


// Define option arguments
static const th_optarg optList[] =
{
    { 0, '?', "help",       "Show this help", OPT_NONE },
//    { 1, 'v', "verbose",    "Be more verbose", 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 },
    { 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 documents directory", OPT_ARGREQ },
    { 9, 'S', "sldb",       "Specify Songlengths.txt file (use -H if possible)", OPT_ARGREQ },
};

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


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"
        "When specifying HVSC path, it is preferable to use -H/--hvsc option,\n"
        "as STIL.txt and Songlengths.txt will be automatically used from there.\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;
}


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)
        {
            int n;
            for (n = 0; n < stack->nitems; n++)
            {
                if (stack->items[n].cmd == -1)
                    th_free(stack->items[n].str);
            }
            th_free(stack->items);
        }
        memset(stack, 0, sizeof(PSFStack));
    }
}


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

    while (*start)
    {
        PSFStackItem item;
        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)
        {
            int found = argMatchPSFieldError(field);
            th_free(field);

            if (found < 0)
                return FALSE;

            item.cmd = found;
            item.str = NULL;
            if (!siStackAddItem(stack, &item))
                return FALSE;
        }

        if (!end)
            break;

        start = end + 1;
    }

    return TRUE;
}


//
// Parse a format string into a PSFStack structure
//
BOOL argParsePSFormatStr(PSFStack *stack, const char *fmt)
{
    PSFStackItem item;
    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 - start == 0)
                {
                    item.cmd = -2;
                    item.str = NULL;
                    item.chr = '@';
                    if (!siStackAddItem(stack, &item))
                        return FALSE;
                }
                else
                {
                    char *field = th_strndup_trim(start, fmt - start, TH_TRIM_BOTH);
                    int ret = argMatchPSFieldError(field);
                    th_free(field);
                    if (ret >= 0)
                    {
                        item.cmd = ret;
                        item.str = NULL;
                        if (!siStackAddItem(stack, &item))
                            return FALSE;
                    }
                    else
                        rval = FALSE;
                }
                mode = 0;
            }
            else
            if (*fmt == 0)
                mode = -1;
            fmt++;
            break;

        case 2:
            if (*fmt == 0 || *fmt == '@')
            {
                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;
}


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

    case 1:
        th_verbosityLevel++;
        break;

    case 2:
        optParsable = TRUE;
        break;

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

    case 4:
        optHexadecimal = TRUE;
        break;

    case 5:
        optNoNamePrefix = TRUE;
        break;

    case 6:
        optOneLine = TRUE;
        optFieldSep = optArg;
        break;

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

    case 8:
        setHVSCPath = th_strdup(optArg);
        break;

    case 9:
        setSLDBPath = th_strdup(optArg);
        break;

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

    return TRUE;
}


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 siPrintFieldPrefix(FILE *outFile, const PSFOption *opt)
{
    const char *name = (optParsable || opt->lname == NULL) ? opt->name : opt->lname;
    if (!optNoNamePrefix && optFieldOutput)
        fprintf(outFile, optParsable ? "%s=" : "%-20s : ", name);
}


static void siPrintFieldSeparator(FILE *outFile)
{
    if (optFieldOutput)
        fputs(optOneLine ? optFieldSep : "\n", outFile);
}


static void siPrintPSIDInfoLine(FILE *outFile, BOOL *shown, const PSFOption *opt, const char *xfmt, const char *xaltfmt, ...)
{
    va_list ap;
    const char *fmt = optHexadecimal ? (xaltfmt != NULL ? xaltfmt : xfmt) : xfmt;

    siPrintFieldPrefix(outFile, opt);

    va_start(ap, xaltfmt);
    vfprintf(outFile, fmt, ap);
    va_end(ap);

    siPrintFieldSeparator(outFile);
    *shown = TRUE;
}


#define PR(xfmt, xaltfmt, ...) siPrintPSIDInfoLine(outFile, shown, opt, xfmt, xaltfmt, __VA_ARGS__ )


static void siPrintPSIDInformationField(FILE *outFile, const char *filename, const PSIDHeader *psid, BOOL *shown, const PSFStackItem *item)
{
    const PSFOption *opt = &optPSOptions[item->cmd];
    switch (item->cmd)
    {
        case  0: PR("%s", NULL, filename); break;
        case  1: PR("%s", NULL, psid->magic); break;
        case  2: PR("%d.%d", NULL, (psid->version & 0xff), (psid->version >> 8)); break;
        case  3: PR("%s", NULL, (psid->flags & PSF_PLAYER_TYPE) ? "Compute! SIDPlayer MUS" : "Normal built-in"); break;
        case  4:
            if (psid->version >= 2)
                PR("%s", NULL, (psid->flags & PSF_PLAYSID_TUNE) ? "PlaySID" : "C64 compatible");
            break;
        case  5:
            if (psid->version >= 2)
                PR("%s", NULL, si_get_sid_clock_str((psid->flags >> 2) & PSF_CLOCK_MASK));
            break;
        case  6:
            if (psid->version >= 2)
                PR("%s", NULL, si_get_sid_model_str((psid->flags >> 4) & PSF_MODEL_MASK));
            break;

        case  7: PR("%d", "$%08x", psid->dataOffset); break;
        case  8: PR("%d", "$%08x", psid->dataSize); break;
        case  9: PR("%d", "$%04x", psid->loadAddress); break;
        case 10: PR("%d", "$%04x", psid->initAddress); break;
        case 11: PR("%d", "$%04x", psid->playAddress); break;
        case 12: PR("%d", "$%04x", psid->nSongs); break;
        case 13: PR("%d", "$%04x", 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;

                PR("%s", NULL, si_get_sid_model_str(flags));
            }
            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;

                PR("%s", NULL, si_get_sid_model_str(flags));
            }
            break;
        case 16:
            if (psid->version >= 3)
                PR("%d", "$%04x", 0xD000 | (psid->sid2Addr << 4));
            break;
        case 17:
            if (psid->version >= 4)
                PR("%d", "$%04x", 0xD000 | (psid->sid3Addr << 4));
            break;

        case 18: PR("%s", NULL, psid->sidName); break;
        case 19: PR("%s", NULL, psid->sidAuthor); break;
        case 20: PR("%s", NULL, psid->sidCopyright); break;

        case 21:
            siPrintFieldPrefix(outFile, opt);
            th_md5_print(outFile, psid->hash);
            siPrintFieldSeparator(outFile);
            break;

        case 22:
            siPrintFieldPrefix(outFile, opt);
            if (psid->lengths != NULL)
            {
                int i;
                for (i = 0; i < psid->lengths->nlengths; i++)
                {
                    int len = psid->lengths->lengths[i];
                    fprintf(outFile, "%d:%d%s", len / 60, len % 60,
                        (i < psid->lengths->nlengths - 1) ? " " : "");
                }
            }
            siPrintFieldSeparator(outFile);
            break;
    }
}


BOOL argHandleFile(char *filename)
{
    PSIDHeader *psid = NULL;
    th_ioctx *inFile = NULL;
    FILE *outFile;
    int index;
    BOOL shown = FALSE;

    optNFiles++;
    outFile = stdout;

    if ((inFile = th_io_fopen(&th_stdio_io_ops, filename, "rb")) == NULL)
    {
        THERR("Could not open file '%s'.\n", filename);
        goto error;
    }

    // Read PSID data
    if (si_read_sid_file(inFile, &psid) != 0)
    {
        THERR("Error reading %s\n", filename);
        goto error;
    }

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

    // Output
    for (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)
    {
        if (shown && optOneLine)
            fprintf(outFile, "\n");
    }

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

    return TRUE;
}


int main(int argc, char *argv[])
{
    // Initialize
    th_init("SIDInfo", "PSID/RSID information displayer", "0.6.2", NULL, NULL);
    th_verbosityLevel = 0;

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

    // Initialize iconv, check if we have language/charset
#ifdef HAVE_ICONV
    char *setLang = th_strdup(getenv("LANG"));
    if (setLang != NULL)
    {
        char *ptr = strchr(setLang, '.');
        if (ptr != NULL) strcpy(setLang, ptr + 1);
    }

    setChConv = iconv_open(setLang != NULL ? setLang : SET_DEF_CHARSET, "iso88591");
    setUseChConv = setChConv != (iconv_t) -1;
    th_free(setLang);
#endif

    // Parse command line arguments
    if (!th_args_process(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, OPTH_ONLY_OPTS))
        return -1;

    if (optOneLine)
    {
        optParsable = FALSE;
        optNoNamePrefix = TRUE;
    }
    
    if (optFieldOutput && !optFormat.nitems)
    {
        PSFStackItem item;
        int i;

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

        for (i = 0; i < noptPSOptions; i++)
        {
            item.cmd = i;
            siStackAddItem(&optFormat, &item);
        }
    }

    // Check paths
    if (setHVSCPath != NULL)
    {
        if (setSLDBPath == NULL)
            setSLDBPath = th_strdup_printf("%s%c%s", setHVSCPath, TH_DIR_SEPARATOR, SET_SLDB_FILENAME);
    }

    if (setSLDBPath != NULL)
    {
        // Initialize SLDB
        int ret = THERR_OK;
        th_ioctx *inFile = NULL;
        if ((inFile = th_io_fopen(&th_stdio_io_ops, setSLDBPath, "r")) == NULL)
        {
            THERR("Could not open SLDB '%s'.\n",
                setSLDBPath);
            goto err;
        }

        THMSG(1, "Reading SLDB.\n");
        if ((sidSLDB = si_sldb_new()) == NULL)
        {
            THERR("Could not allocate SLDB structure!\n");
            goto err;
        }

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

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

err:
        th_io_close(inFile);
    }

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

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

out:

#ifdef HAVE_ICONV
    if (setUseChConv)
        iconv_close(setChConv);
#endif

    th_free(setHVSCPath);
    th_free(setSLDBPath);
    si_sldb_free(sidSLDB);
    return 0;
}