view sidinfo.c @ 20:6058339ffe0e

Update usage.
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 25 Sep 2014 02:49:51 +0300
parents 16cfbdf20eaf
children 6ba8403930ab
line wrap: on
line source

/*
 * SIDInfo - PSID/RSID information displayer
 * Written by Matti 'ccr' Hämäläinen
 * (C) Copyright 2014 Tecnic Software productions (TNSP)
 */
#include "th_args.h"
#include "th_endian.h"
#include "th_string.h"
#include "th_crypto.h"

#define PSID_MAGIC_LEN    4
#define PSID_STR_LEN      32
#define PSID_BUFFER_SIZE  (1024 * 16)


enum
{
    PSF_NONE	       = 0,

    PSF_TYPE	       = 0x00000001,
    PSF_VERSION        = 0x00000002,
    PSF_DATA_OFFS      = 0x00000004,
    PSF_LOAD_ADDR      = 0x00000008,
    PSF_INIT_ADDR      = 0x00000010,
    PSF_PLAY_ADDR      = 0x00000020,
    PSF_SONGS          = 0x00000040,
    PSF_START_SONG     = 0x00000080,
    PSF_SPEEDS         = 0x00000100,
    PSF_SID_NAME       = 0x00000200,
    PSF_SID_AUTHOR     = 0x00000400,
    PSF_SID_COPYRIGHT  = 0x00000800,

    PSF_DATA_SIZE      = 0x00100000,
    PSF_HASH           = 0x00200000,
    PSF_FILENAME       = 0x10000000,

    PSF_ALL            = 0xffffffff,
};


typedef struct
{
    uint32_t flag;
    char *name;
    char *lname;
} PSFOption;


const PSFOption optPSFlags[] =
{
    { PSF_TYPE           , "Type"       , NULL },
    { PSF_VERSION        , "Version"    , NULL },
    { PSF_DATA_OFFS      , "DataOffs"   , "Data offset" },
    { PSF_DATA_SIZE      , "DataSize"   , "Data size" },
    { PSF_LOAD_ADDR      , "LoadAddr"   , "Load address" },
    { PSF_INIT_ADDR      , "InitAddr"   , "Init address" },
    { PSF_PLAY_ADDR      , "PlayAddr"   , "Play address" },
    { PSF_SONGS          , "Songs"      , "Songs" },
    { PSF_START_SONG     , "StartSong"  , "Start song" },
    { PSF_SID_NAME       , "Name"       , NULL },
    { PSF_SID_AUTHOR     , "Author"     , NULL },
    { PSF_SID_COPYRIGHT  , "Copyright"  , NULL },
    { PSF_HASH           , "Hash"       , NULL },
    { PSF_FILENAME       , "Filename"   , NULL },
    { PSF_ALL            , "All"        , NULL },
};

const int noptPSFlags = sizeof(optPSFlags) / sizeof(optPSFlags[0]);


/* Options
 */
BOOL	optParsable = FALSE,
        optNoNamePrefix = FALSE,
        optHexadecimal = FALSE;
uint32_t optFields = PSF_ALL;
int     optNFiles = 0;


/* Arguments
 */
static optarg_t 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 },
    { 3, 'f', "fields",     "Show only specified field(s)", OPT_ARGREQ },
    { 4, 'x', "hex",        "Use hexadecimal values", OPT_NONE },
};

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


void argShowHelp(void)
{
    int index, n;

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

    for (index = n = 0; index < noptPSFlags; index++)
    {
        const PSFOption *opt = &optPSFlags[index];
        printf("%s%s", opt->name, (index < noptPSFlags - 1) ? ", " : "\n\n");
        if (++n > 5)
        {
            printf("\n");
            n = 0;
        }
    }
    
    printf("Example: %s -x -p -f hash,copyright somesidfile.sid\n", th_prog_name);
}


int argMatchPSField(const char *field)
{
    int index, found = -1;
    size_t len = strlen(field);
    for (index = 0; index < noptPSFlags; index++)
    {
        const PSFOption *opt = &optPSFlags[index];
        if (strncasecmp(opt->name, field, len) == 0)
        {
            if (found >= 0)
                return -2;
            found = index;
        }
    }

    return found;
}


BOOL argParsePSField(char *opt, char *end, uint32_t *fields)
{
    // Trim whitespace
    if (end != NULL)
    {
        *end = 0;
        for (end--; end > opt && *end && th_isspace(*end); end--)
            *end = 0;
    }
    while (*opt && th_isspace(*opt)) opt++;

    // Match field name
    int found = argMatchPSField(opt);
    switch (found)
    {
        case -1:
            THERR("No such flag '%s'.\n", opt);
            return FALSE;

        case -2:
            THERR("Flag '%s' is ambiguous.\n", opt);
            return FALSE;

        default:
            *fields |= optPSFlags[found].flag;
            return TRUE;
    }
}


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:
        {
            char *start = optArg;
            optFields = PSF_NONE;

            while (*start)
            {
                char *end = strchr(start, ',');

                if (!argParsePSField(start, end, &optFields))
                    return FALSE;

                if (!end)
                    break;

                start = end + 1;
            }

            //fprintf(stderr, "%08x\n", optFields);
        }
        break;

    case 4:
        optHexadecimal = TRUE;
        break;

    case 5:
        optNoNamePrefix = TRUE;
        break;

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

    return TRUE;
}


typedef struct
{
    char magic[PSID_MAGIC_LEN + 1]; // "PSID" / "RSID" magic identifier
    uint16_t
        version,         // Version number
        dataOffset,      // Start of actual c64 data in file
        loadAddress,     // Loading address
        initAddress,     // Initialization address
        playAddress,     // Play one frame
        nSongs,          // Number of subsongs
        startSong;       // Default starting song
    uint32_t speed;      // Speed
    char sidName[PSID_STR_LEN + 1];    // Descriptive text-fields, ASCIIZ
    char sidAuthor[PSID_STR_LEN + 1];
    char sidCopyright[PSID_STR_LEN + 1];

    // PSIDv2 data
    uint16_t flags;      // Flags
    uint8_t  startPage, pageLength;
    uint16_t reserved;

    // Extra data
    BOOL isRSID;
    size_t dataSize;     // Total size of data - header
    th_md5hash_t hash;   // Songlength database hash

} PSIDHeader;


static void siAppendHash16(th_md5state_t *state, uint16_t data)
{
    uint8_t ib8[2];
    ib8[0] = data & 0xff;
    ib8[1] = data >> 8;
    th_md5_append(state, (uint8_t *) &ib8, sizeof(ib8));
}


int siReadPSIDFile(FILE *inFile, PSIDHeader *psid)
{
    th_md5state_t state;
    uint8_t tmp8, *fileData = NULL;
    int index, ret = -1;
    size_t read;
    BOOL first;

    memset(psid, 0, sizeof(*psid));

    if ((fileData = (uint8_t *) th_malloc(PSID_BUFFER_SIZE)) == NULL)
    {
        THERR("Error allocating temporary data buffer of %d bytes.\n", PSID_BUFFER_SIZE);
        goto error;
    }

    // Read PSID header in
    if (!th_fread_str(inFile, (uint8_t *) psid->magic, PSID_MAGIC_LEN) ||
        !th_fread_be16(inFile, &psid->version) ||
        !th_fread_be16(inFile, &psid->dataOffset) ||
        !th_fread_be16(inFile, &psid->loadAddress) ||
        !th_fread_be16(inFile, &psid->initAddress) ||
        !th_fread_be16(inFile, &psid->playAddress) ||
        !th_fread_be16(inFile, &psid->nSongs) ||
        !th_fread_be16(inFile, &psid->startSong) ||
        !th_fread_be32(inFile, &psid->speed))
    {
        THERR("Could not read PSID/RSID header.\n");
        goto error;
    }

    psid->magic[PSID_MAGIC_LEN] = 0;

    if ((psid->magic[0] != 'R' && psid->magic[0] != 'P') ||
        psid->magic[1] != 'S' || psid->magic[2] != 'I' || psid->magic[3] != 'D' ||
        psid->version < 1 || psid->version > 3)
    {
        THERR("Not a supported PSID or RSID file.\n");
        goto error;
    }

    psid->isRSID = psid->magic[0] == 'R';

    if (!th_fread_str(inFile, (uint8_t *)psid->sidName, PSID_STR_LEN) ||
        !th_fread_str(inFile, (uint8_t *)psid->sidAuthor, PSID_STR_LEN) ||
        !th_fread_str(inFile, (uint8_t *)psid->sidCopyright, PSID_STR_LEN))
    {
        THERR("Error reading SID file header.\n");
        goto error;
    }

    psid->sidName[PSID_STR_LEN] = 0;
    psid->sidAuthor[PSID_STR_LEN] = 0;
    psid->sidCopyright[PSID_STR_LEN] = 0;

    // Check if we need to load PSIDv2NG header ...
    if (psid->version >= 2)
    {
        // Yes, we need to
        if (!th_fread_be16(inFile, &psid->flags) ||
            !th_fread_byte(inFile, &psid->startPage) ||
            !th_fread_byte(inFile, &psid->pageLength) ||
            !th_fread_be16(inFile, &psid->reserved))
        {
            THERR("Error reading PSID/RSID v2+ extra header data.\n");
            goto error;
        }
    }

    // Initialize MD5-hash calculation
    th_md5_init(&state);

    // Process actual data
    psid->dataSize = 0;
    first = TRUE;
    do
    {
        read = fread(fileData, sizeof(uint8_t), PSID_BUFFER_SIZE, inFile);
        psid->dataSize += read;

        if (first && psid->loadAddress == 0)
        {
            if (read < 4)
            {
                THERR("Error reading song data, unexpectedly small file.\n");
                goto error;
            }

            // Strip load address (2 first bytes)
            th_md5_append(&state, &fileData[2], read - 2);
            first = FALSE;
        }
        else
        if (read > 0)
        {
            // Append "as is"
            th_md5_append(&state, fileData, read);
        }
    } while (read > 0 && !feof(inFile));

    // Append header data to hash
    siAppendHash16(&state, psid->initAddress);
    siAppendHash16(&state, psid->playAddress);
    siAppendHash16(&state, psid->nSongs);

    // Append song speed data to hash
    tmp8 = psid->isRSID ? 60 : 0;
    for (index = 0; index < psid->nSongs && index < 32; index++)
    {
        if (psid->isRSID)
            tmp8 = 60;
        else
            tmp8 = (psid->speed & (1 << index)) ? 60 : 0;

        th_md5_append(&state, &tmp8, sizeof(tmp8));
    }

    // Rest of songs (more than 32)
    for (index = 32; index < psid->nSongs; index++)
        th_md5_append(&state, &tmp8, sizeof(tmp8));

    // PSIDv2NG specific
    if (psid->version >= 2)
    {
        // REFER TO SIDPLAY HEADERS FOR MORE INFORMATION
        tmp8 = (psid->flags >> 2) & 3;
        if (tmp8 == 2)
            th_md5_append(&state, &tmp8, sizeof(tmp8));
    }

    // Calculate the hash
    th_md5_finish(&state, psid->hash);
    ret = 0;

error:
    // Free buffer
    th_free(fileData);
    return ret;
}


static void siPrintLinePrefix(FILE *outFile, const BOOL parsable, const char *name)
{
    if (!optNoNamePrefix)
        fprintf(outFile, parsable ? "%s=" : "%-20s : ", name);
}


static void siPrintPSIDInfoLine(FILE *outFile, const BOOL parsable, const BOOL hex,
    const uint32_t flags, const int xindex, const char *xfmt, const char *xaltfmt, ...)
{
    const PSFOption *opt = &optPSFlags[xindex];
    if (flags & opt->flag)
    {
        va_list ap;
        const char *fmt = hex ? (xaltfmt != NULL ? xaltfmt : xfmt) : xfmt;

        siPrintLinePrefix(outFile, parsable, (parsable || opt->lname == NULL) ? opt->name : opt->lname);

        va_start(ap, xaltfmt);

        if (parsable)
            vfprintf(outFile, fmt, ap);
        else
            vfprintf(outFile, fmt, ap);

        va_end(ap);

        fprintf(outFile, "\n");
    }
}

#define PR(xindex, xfmt, xaltfmt, ...) siPrintPSIDInfoLine(outFile, parsable, hex, flags, xindex, xfmt, xaltfmt, __VA_ARGS__ )


void siPrintPSIDInformation(FILE *outFile, const BOOL parsable, const BOOL hex,
    const uint32_t flags, const char *filename, const PSIDHeader *psid)
{
    PR(13, "%s", NULL, filename);

    PR( 0, "%s", NULL, psid->magic);
    PR( 1, "%d.%d", NULL, (psid->version & 0xff), (psid->version >> 8));
    PR( 2, "%d", "$%08x", psid->dataOffset);
    PR( 3, "%d", "$%08x", psid->dataSize);
    PR( 4, "%d", "$%04x", psid->loadAddress);
    PR( 5, "%d", "$%04x", psid->initAddress);
    PR( 6, "%d", "$%04x", psid->playAddress);
    PR( 7, "%d", "$%04x", psid->nSongs);
    PR( 8, "%d", "$%04x", psid->startSong);

    PR( 9, "%s", NULL, psid->sidName);
    PR(10, "%s", NULL, psid->sidAuthor);
    PR(11, "%s", NULL, psid->sidCopyright);

    if (flags & PSF_HASH)
    {
        siPrintLinePrefix(outFile, parsable, "Hash");
        th_md5_print(outFile, psid->hash);
        fprintf(outFile, "\n");
    }
}


BOOL argHandleFile(char *filename)
{
    static PSIDHeader psid;
    static FILE *inFile = NULL;
    optNFiles++;

    if ((inFile = fopen(filename, "rb")) == NULL)
    {
        THERR("Could not open file '%s'.\n", filename);
        return FALSE;
    }

    // Read PSID data
    if (siReadPSIDFile(inFile, &psid) != 0)
        goto error;

    // Output
    siPrintPSIDInformation(stdout, optParsable, optHexadecimal, optFields, filename, &psid);

    // Shutdown
error:
    if (inFile != NULL)
        fclose(inFile);

    return TRUE;
}


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

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

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

    return 0;
}