view sidinfo.c @ 55:54b48086a1d0

Begin slight refactoring of how output is done.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 26 Dec 2015 19:37:04 +0200
parents d989cf142f8c
children d2148880c71e
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"

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


// Flags for various information fields
enum
{
    SIF_NONE           = 0,

    SIF_TYPE           = 0x00000001,
    SIF_VERSION        = 0x00000002,
    SIF_DATA_OFFS      = 0x00000004,
    SIF_LOAD_ADDR      = 0x00000008,
    SIF_INIT_ADDR      = 0x00000010,
    SIF_PLAY_ADDR      = 0x00000020,
    SIF_SONGS          = 0x00000040,
    SIF_START_SONG     = 0x00000080,
    SIF_SPEEDS         = 0x00000100,
    SIF_SID_NAME       = 0x00000200,
    SIF_SID_AUTHOR     = 0x00000400,
    SIF_SID_COPYRIGHT  = 0x00000800,
    SIF_PLAYER_TYPE    = 0x00001000,
    SIF_PLAYSID_TUNE   = 0x00002000,
    SIF_VIDEO_CLOCK    = 0x00004000,
    SIF_SID_MODEL      = 0x00008000,

    SIF_DATA_SIZE      = 0x00100000,
    SIF_HASH           = 0x00200000,
    SIF_FILENAME       = 0x01000000,

    SIF_ALL            = 0x0fffffff,
};


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


static const PSFOption optPSFlags[] =
{
    { SIF_FILENAME       , "Filename"   , NULL },
    { SIF_TYPE           , "Type"       , NULL },
    { SIF_VERSION        , "Version"    , NULL },
    { SIF_PLAYER_TYPE    , "PlayerType" , "Player type" },
    { SIF_PLAYSID_TUNE   , "PlayerCompat", "Player compatibility" },
    { SIF_VIDEO_CLOCK    , "VideoClock" , "Video clock speed" },
    { SIF_SID_MODEL      , "SIDModel"   , "SID model" },

    { SIF_DATA_OFFS      , "DataOffs"   , "Data offset" },
    { SIF_DATA_SIZE      , "DataSize"   , "Data size" },
    { SIF_LOAD_ADDR      , "LoadAddr"   , "Load address" },
    { SIF_INIT_ADDR      , "InitAddr"   , "Init address" },
    { SIF_PLAY_ADDR      , "PlayAddr"   , "Play address" },
    { SIF_SONGS          , "Songs"      , "Songs" },
    { SIF_START_SONG     , "StartSong"  , "Start song" },
    { SIF_SID_NAME       , "Name"       , NULL },
    { SIF_SID_AUTHOR     , "Author"     , NULL },
    { SIF_SID_COPYRIGHT  , "Copyright"  , NULL },
    { SIF_HASH           , "Hash"       , NULL },

    { SIF_ALL            , "All"        , NULL },
};

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


// Option variables
BOOL	optParsable = FALSE,
        optNoNamePrefix = FALSE,
        optHexadecimal = FALSE,
        optOneLine = FALSE;
char    *optFieldSep = NULL;
uint32_t optFields = SIF_ALL;
int     optNFiles = 0;


// Define option arguments
static const th_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 },
    { 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 },
};

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, 0);
    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 (th_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 field '%s'.\n", opt);
            return FALSE;

        case -2:
            THERR("Field '%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 = SIF_NONE;

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

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

                if (!end)
                    break;

                start = end + 1;
            }
        }
        break;

    case 4:
        optHexadecimal = TRUE;
        break;

    case 5:
        optNoNamePrefix = TRUE;
        break;

    case 6:
        optOneLine = TRUE;
        optFieldSep = optArg;
        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;


enum
{
    PSF_PLAYER_TYPE   = 0x0001, // 0 = built-in, 1 = Compute! SIDPlayer MUS
    PSF_PLAYSID_TUNE  = 0x0002, // 0 = Real C64-compatible, 1 = PlaySID specific (v2NG)

    PSF_CLOCK_UNKNOWN = 0x0000, // Video standard used (v2NG)
    PSF_CLOCK_PAL     = 0x0004,
    PSF_CLOCK_NTSC    = 0x0008,
    PSF_CLOCK_ANY     = 0x000c,
    PSF_CLOCK_MASK    = 0x000c,
    
    PSF_MODEL_UNKNOWN = 0x0000, // SID model (v2NG)
    PSF_MODEL_MOS6581 = 0x0010,
    PSF_MODEL_MOS8580 = 0x0020,
    PSF_MODEL_ANY     = 0x0030,
    PSF_MODEL_MASK    = 0x0030,
};


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;
            }
            
            // Grab the actual load address
            psid->loadAddress = TH_LE16_TO_NATIVE(*(uint16_t *) fileData);

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


const char *siGetSIDClockStr(int flags)
{
    switch (flags & PSF_CLOCK_MASK)
    {
        case PSF_CLOCK_UNKNOWN : return "Unknown";
        case PSF_CLOCK_PAL     : return "PAL 50Hz";
        case PSF_CLOCK_NTSC    : return "NTSC 60Hz";
        case PSF_CLOCK_ANY     : return "PAL / NTSC";
        default                : return "?";
    }
}


const char *siGetSIDModelStr(int flags)
{
    switch (flags & PSF_MODEL_MASK)
    {
        case PSF_MODEL_UNKNOWN : return "Unknown";
        case PSF_MODEL_MOS6581 : return "MOS6581";
        case PSF_MODEL_MOS8580 : return "MOS8580";
        case PSF_MODEL_ANY     : return "MOS6581 / MOS8580";
        default                : return "?";
    }
}


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


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


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

        siPrintFieldPrefix(outFile, (optParsable || opt->lname == NULL) ? opt->name : opt->lname);

        va_start(ap, xaltfmt);

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

        va_end(ap);

        siPrintFieldSeparator(outFile);
        *shown = TRUE;
    }
}

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

void siPrintPSIDInformationField(FILE *outFile, const char *filename, const PSIDHeader *psid, BOOL *shown, const int xindex)
{
    switch (xindex)
    {
        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, siGetSIDClockStr(psid->flags));
            break;
        case  6:
            if (psid->version >= 2)
                PR("%s", NULL, siGetSIDModelStr(psid->flags));
            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: PR("%s", NULL, psid->sidName); break;
        case 15: PR("%s", NULL, psid->sidAuthor); break;
        case 16: PR("%s", NULL, psid->sidCopyright); break;

        case 17:
            {
                const PSFOption *opt = &optPSFlags[xindex];
                if (optFormatStack || (optFields & opt->flag))
                {
                    siPrintFieldPrefix(outFile, "Hash");
                    th_md5_print(outFile, psid->hash);
                    siPrintFieldSeparator(outFile);
                }
            }
            break;
    }
}


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

    optNFiles++;
    outFile = stdout;

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

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

    // Output
    {
        for (index = 0; index < noptPSFlags - 1; index++)
            siPrintPSIDInformationField(outFile, filename, &psid, &shown, index);
        if (shown && optOneLine)
            fprintf(outFile, "\n");
    }

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

    return TRUE;
}


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

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

    // Process files
    if (!th_args_process(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, OPTH_ONLY_OTHER))
        return -2;

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

    return 0;
}