view src/xs_length.c @ 969:b9e6f929a617

Fix a silly, but meaningless bug in reading PSIDv2/3 extra header (the value is not actually used for anything here.)
author Matti Hamalainen <ccr@tnsp.org>
date Wed, 21 Nov 2012 00:15:43 +0200
parents 5e0a05c84694
children d90bca05521e
line wrap: on
line source

/*  
   XMMS-SID - SIDPlay input plugin for X MultiMedia System (XMMS)

   Get song length from SLDB for PSID/RSID files
   
   Programmed and designed by Matti 'ccr' Hamalainen <ccr@tnsp.org>
   (C) Copyright 1999-2009 Tecnic Software productions (TNSP)

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License along
   with this program; if not, write to the Free Software Foundation, Inc.,
   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "xs_length.h"
#include "xs_support.h"


/* Free memory allocated for given SLDB node
 */
static void xs_sldb_node_free(XSSLDBNode *node)
{
    if (node)
    {
        g_free(node->lengths);
        g_free(node);
    }
}


/* Insert given node to db linked list
 */
static void xs_sldb_node_insert(XSSLDB *db, XSSLDBNode *node)
{
    assert(db != NULL);

    if (db->nodes)
    {
        node->prev = db->nodes->prev;
        db->nodes->prev->next = node;
        db->nodes->prev = node;
    }
    else
    {
        db->nodes = node;
        node->prev = node;
    }
    node->next = NULL;
}


/* Parse a time-entry in SLDB format
 */
static gint xs_sldb_gettime(gchar *str, size_t *pos)
{
    gint result, tmp;

    /* Check if it starts with a digit */
    if (isdigit(str[*pos]))
    {
        /* Get minutes-field */
        result = 0;
        while (isdigit(str[*pos]))
            result = (result * 10) + (str[(*pos)++] - '0');

        result *= 60;

        /* Check the field separator char */
        if (str[*pos] == ':')
        {
            /* Get seconds-field */
            (*pos)++;
            tmp = 0;
            while (isdigit(str[*pos]))
                tmp = (tmp * 10) + (str[(*pos)++] - '0');

            result += tmp;
        }
        else
            result = -2;
    }
    else
        result = -1;

    /* Ignore and skip the possible attributes */
    while (str[*pos] && !isspace(str[*pos]))
        (*pos)++;

    return result;
}


/* Parse one SLDB definition line, return SLDB node
 */
XSSLDBNode * xs_sldb_read_entry(gchar *inLine)
{
    XSSLDBNode *tmnode = NULL;
    size_t linePos, tmpLen, savePos;
    gboolean isOK;
    gint i;

    /* Allocate new node */
    tmnode = (XSSLDBNode *) g_malloc0(sizeof(XSSLDBNode));
    if (tmnode == NULL)
    {
        xs_error("Error allocating new node. Fatal error.\n");
        return NULL;
    }

    /* Get hash value */
    linePos = 0;
    for (i = 0; i < XS_MD5HASH_LENGTH; i++, linePos += 2)
    {
        gint tmpu;
        sscanf(&inLine[linePos], "%2x", &tmpu);
        tmnode->md5Hash[i] = tmpu;
    }
        
    /* Get playtimes */
    xs_findnext(inLine, &linePos);
    if (inLine[linePos] != '=')
    {
        xs_error("'=' expected on column #%d.\n", linePos);
        goto error;
    }

    /* First playtime is after '=' */
    savePos = ++linePos;
    tmpLen = strlen(inLine);

    /* Get number of sub-tune lengths */
    isOK = TRUE;
    while (linePos < tmpLen && isOK)
    {
        xs_findnext(inLine, &linePos);

        if (xs_sldb_gettime(inLine, &linePos) >= 0)
            tmnode->nlengths++;
        else
            isOK = FALSE;
    }

    /* Allocate memory for lengths */
    if (tmnode->nlengths == 0)
        goto error;

    tmnode->lengths = (gint *) g_malloc0(tmnode->nlengths * sizeof(gint));
    if (tmnode->lengths == NULL)
    {
        xs_error("Could not allocate memory for node.\n");
        goto error;
    }

    /* Read lengths in */
    for (i = 0, linePos = savePos, isOK = TRUE; 
        linePos < tmpLen && i < tmnode->nlengths && isOK; i++)
    {
        gint l;
        xs_findnext(inLine, &linePos);

        l = xs_sldb_gettime(inLine, &linePos);
        if (l >= 0)
            tmnode->lengths[i] = l;
        else
            isOK = FALSE;
    }

    return tmnode;

error:
    xs_sldb_node_free(tmnode);
    return NULL;
}


/* Read database to memory
 */
gint xs_sldb_read(XSSLDB *db, const gchar *dbFilename)
{
    FILE *inFile;
    gchar inLine[XS_BUF2_SIZE];
    size_t lineNum;
    XSSLDBNode *tmnode;
    assert(db);

    /* Try to open the file */
    if ((inFile = fopen(dbFilename, "ra")) == NULL)
    {
        xs_error("Could not open SongLengthDB '%s'\n", dbFilename);
        return -1;
    }

    /* Read and parse the data */
    lineNum = 0;

    while (fgets(inLine, XS_BUF2_SIZE, inFile) != NULL)
    {
        size_t linePos = 0;
        lineNum++;
        
        xs_findnext(inLine, &linePos);

        /* Check if it is datafield */
        if (isxdigit(inLine[linePos]))
        {
            /* Check the length of the hash */
            gint hashLen;
            for (hashLen = 0; inLine[linePos] && isxdigit(inLine[linePos]); hashLen++, linePos++);

            if (hashLen != XS_MD5HASH_LENGTH_CH)
            {
                xs_error("Invalid MD5-hash in SongLengthDB file '%s' line #%d:\n%s\n",
                    dbFilename, lineNum, inLine);
            }
            else
            {
                /* Parse and add node to db */
                if ((tmnode = xs_sldb_read_entry(inLine)) != NULL)
                {
                    xs_sldb_node_insert(db, tmnode);
                }
                else
                {
                    xs_error("Invalid entry in SongLengthDB file '%s' line #%d:\n%s\n",
                        dbFilename, lineNum, inLine);
                }
            }
        }
        else
        if (inLine[linePos] != ';' && inLine[linePos] != '[' && inLine[linePos] != 0)
        {
            xs_error("Invalid line in SongLengthDB file '%s' line #%d:\n%s\n",
                dbFilename, lineNum, inLine);
        }
    }

    fclose(inFile);
    return 0;
}


/* Compare two given MD5-hashes.
 * Return: 0 if equal
 *         negative if testHash1 < testHash2
 *         positive if testHash1 > testHash2
 */
static gint xs_sldb_cmphash(xs_md5hash_t testHash1, xs_md5hash_t testHash2)
{
    gint i, d;

    /* Compute difference of hashes */
    for (i = 0, d = 0; (i < XS_MD5HASH_LENGTH) && !d; i++)
        d = (testHash1[i] - testHash2[i]);

    return d;
}


/* Compare two nodes
 */
static gint xs_sldb_cmp(const void *node1, const void *node2)
{
    /* We assume here that we never ever get NULL-pointers or similar */
    return xs_sldb_cmphash(
        (*(XSSLDBNode **) node1)->md5Hash,
        (*(XSSLDBNode **) node2)->md5Hash);
}


/* (Re)create index
 */
gint xs_sldb_index(XSSLDB * db)
{
    XSSLDBNode *node;
    size_t i;
    assert(db);

    /* Free old index */
    g_free(db->pindex);
    db->pindex = NULL;

    /* Get size of db */
    for (node = db->nodes, db->n = 0; node != NULL; node = node->next)
        db->n++;

    /* Check number of nodes */
    if (db->n > 0)
    {
        /* Allocate memory for index-table */
        db->pindex = (XSSLDBNode **) g_malloc(sizeof(XSSLDBNode *) * db->n);
        if (!db->pindex)
            return -1;

        /* Get node-pointers to table */
        for (i = 0, node = db->nodes; node && i < db->n; node = node->next)
            db->pindex[i++] = node;

        /* Sort the indexes */
        qsort(db->pindex, db->n, sizeof(XSSLDBNode *), xs_sldb_cmp);
    }

    return 0;
}


/* Free a given song-length database
 */
void xs_sldb_free(XSSLDB * db)
{
    XSSLDBNode *node, *next;

    if (!db)
        return;

    /* Free the memory allocated for nodes */
    node = db->nodes;
    while (node != NULL)
    {
        next = node->next;
        xs_sldb_node_free(node);
        node = next;
    }

    db->nodes = NULL;

    /* Free memory allocated for index */
    g_free(db->pindex);
    db->pindex = NULL;

    /* Free structure */
    db->n = 0;
    g_free(db);
}


/* Compute md5hash of given SID-file
 */
typedef struct
{
    gchar magic[4];    /* "PSID" / "RSID" magic identifier */
    guint16 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 */
    guint32 speed;       /* Speed */
    gchar sidName[32];   /* Descriptive text-fields, ASCIIZ */
    gchar sidAuthor[32];
    gchar sidCopyright[32];
} psidv1_header_t;


typedef struct
{
    guint16 flags;        /* Flags */
    guint8 startPage, pageLength;
    guint16 reserved;
} psidv2_header_t;


static gint xs_get_sid_hash(const gchar *filename, xs_md5hash_t hash)
{
    XSFile *inFile = NULL;
    xs_md5state_t inState;
    psidv1_header_t psidH;
    psidv2_header_t psidH2;
    guint8 *songData = NULL;
    guint8 ib8[2], i8;
    gint index, result;
    gboolean isRSID;

    /* Try to open the file */
    if ((inFile = xs_fopen(filename, "rb")) == NULL)
        goto error;

    /* Read PSID header in */
    if (!xs_fread_str(inFile, psidH.magic, sizeof(psidH.magic)) ||
        !xs_fread_be16(inFile, &psidH.version) ||
        !xs_fread_be16(inFile, &psidH.dataOffset) ||
        !xs_fread_be16(inFile, &psidH.loadAddress) ||
        !xs_fread_be16(inFile, &psidH.initAddress) ||
        !xs_fread_be16(inFile, &psidH.playAddress) ||
        !xs_fread_be16(inFile, &psidH.nSongs) ||
        !xs_fread_be16(inFile, &psidH.startSong) ||
        !xs_fread_be32(inFile, &psidH.speed))
    {
        xs_error("Could not read PSID/RSID header from '%s'\n", filename);
        goto error;
    }

    if ((strncmp(psidH.magicID, "PSID", 4) &&
        strncmp(psidH.magicID, "RSID", 4)) ||
        psidH.version < 1 || psidH.version > 3)
    {
        xs_error("Not a supported PSID or RSID file '%s'\n", filename);
        goto error;
    }

    isRSID = psidH.magic[0] == 'R';

    if (!xs_fread_str(inFile, psidH.sidName, sizeof(psidH.sidName)) ||
        !xs_fread_str(inFile, psidH.sidAuthor, sizeof(psidH.sidAuthor)) ||
        !xs_fread_str(inFile, psidH.sidCopyright, sizeof(psidH.sidCopyright)))
    {
        xs_error("Error reading SID file header from '%s'\n", filename);
        goto error;
    }
    
    /* Check if we need to load PSIDv2NG header ... */
    psidH2.flags = 0;    /* Just silence a stupid gcc warning */
    
    if (psidH.version == 2 || psidH.version == 3)
    {
        /* Yes, we need to */
        if (!xs_fread_be16(inFile, &psidH2.flags) ||
            !xs_fread_byte(inFile, &psidH2.startPage) ||
            !xs_fread_byte(inFile, &psidH2.pageLength) ||
            !xs_fread_be16(inFile, &psidH2.reserved))
        {
            xs_error("Error reading PSID/RSID v2+ extra header data from '%s'\n",
                filename);
            goto error;
        }
    }

    /* Allocate buffer */
    if ((songData = (guint8 *) g_malloc(XS_SIDBUF_SIZE)) == NULL)
    {
        xs_error("Error allocating temp data buffer for file '%s'\n", filename);
        goto error;
    }

    /* Read data to buffer */
    result = xs_fread(songData, sizeof(guint8), XS_SIDBUF_SIZE, inFile);
    xs_fclose(inFile);

    /* Initialize and start MD5-hash calculation */
    xs_md5_init(&inState);

    if (psidH.loadAddress == 0)
    {
        /* Strip load address (2 first bytes) */
        xs_md5_append(&inState, &songData[2], result - 2);
    }
    else
    {
        /* Append "as is" */
        xs_md5_append(&inState, songData, result);
    }

    /* Free buffer */
    g_free(songData);

    /* Append header data to hash */
#define XSADDHASH(QDATAB) do {			\
    ib8[0] = (QDATAB & 0xff);			\
    ib8[1] = (QDATAB >> 8);			\
    xs_md5_append(&inState, (guint8 *) &ib8, sizeof(ib8));    \
    } while (0)

    XSADDHASH(psidH.initAddress);
    XSADDHASH(psidH.playAddress);
    XSADDHASH(psidH.nSongs);
#undef XSADDHASH

    /* Append song speed data to hash */
    i8 = isRSID ? 60 : 0;
    for (index = 0; index < psidH.nSongs && index < 32; index++)
    {
        if (isRSID)
            i8 = 60;
        else
            i8 = (psidH.speed & (1 << index)) ? 60 : 0;

        xs_md5_append(&inState, &i8, sizeof(i8));
    }

    /* Rest of songs (more than 32) */
    for (index = 32; index < psidH.nSongs; index++)
        xs_md5_append(&inState, &i8, sizeof(i8));

    /* PSIDv2NG specific */
    if (psidH.version == 2 || psidH.version == 3)
    {
        /* SEE SIDPLAY HEADERS FOR INFO */
        i8 = (psidH2.flags >> 2) & 3;
        if (i8 == 2)
            xs_md5_append(&inState, &i8, sizeof(i8));
    }

    /* Calculate the hash */
    xs_md5_finish(&inState, hash);

    return 0;

error:
    if (inFile != NULL)
        xs_fclose(inFile);
    g_free(songData);
    return -1;
}


/* Get node from db index via binary search
 */
XSSLDBNode *xs_sldb_get(XSSLDB *db, const gchar *filename)
{
    XSSLDBNode keyItem, *key, **item;

    /* Check the database pointers */
    if (!db || !db->nodes || !db->pindex)
        return NULL;

    /* Get the hash and then look up from db */
    if (xs_get_sid_hash(filename, keyItem.md5Hash) == 0)
    {
        key = &keyItem;
        item = bsearch(&key, db->pindex, db->n,
            sizeof(db->pindex[0]), xs_sldb_cmp);
        
        if (item == NULL)
        {
            gint i;
            xs_error("No matching hash in SLDB: %s\n", filename);
            for (i = 0; i < XS_MD5HASH_LENGTH; i++)
                fprintf(stderr, "%02x", keyItem.md5Hash[i]);
            fprintf(stderr, "\n");
        }
        return (item != NULL) ? *item : NULL;
    }
    else
        return NULL;
}