view sidlib.c @ 93:681dfc3b006e

UTF-8.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 12 Feb 2016 18:10:25 +0200
parents c5ff71d64e53
children 833bb78eb658
line wrap: on
line source

/*
 * SIDInfoLib - Way too simplistic PSID/RSID file library
 * Written by Matti 'ccr' Hämäläinen <ccr@tnsp.org>
 * (C) Copyright 2014-2016 Tecnic Software productions (TNSP)
 */
#include "sidlib.h"
#include "th_endian.h"
#include "th_string.h"


static void si_append_hash16(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 si_read_sid_file(th_ioctx *ctx, PSIDHeader *psid)
{
    th_md5state_t state;
    uint8_t tmp8, *data = NULL;
    int index, ret = -1;
    size_t read;
    BOOL first;

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

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

    // Read PSID header in
    if (!thfread_str(ctx, (uint8_t *) psid->magic, PSID_MAGIC_LEN) ||
        !thfread_be16(ctx, &psid->version) ||
        !thfread_be16(ctx, &psid->dataOffset) ||
        !thfread_be16(ctx, &psid->loadAddress) ||
        !thfread_be16(ctx, &psid->initAddress) ||
        !thfread_be16(ctx, &psid->playAddress) ||
        !thfread_be16(ctx, &psid->nSongs) ||
        !thfread_be16(ctx, &psid->startSong) ||
        !thfread_be32(ctx, &psid->speed))
    {
        th_io_error(ctx, ctx->errno,
            "Could not read PSID/RSID header: %s.\n",
            th_error_str(ctx->errno));
        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)
    {
        th_io_error(ctx, THERR_NOT_SUPPORTED,
            "Not a supported PSID or RSID file.\n");
        goto error;
    }

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

    if (!thfread_str(ctx, (uint8_t *) psid->sidName, PSID_STR_LEN) ||
        !thfread_str(ctx, (uint8_t *) psid->sidAuthor, PSID_STR_LEN) ||
        !thfread_str(ctx, (uint8_t *) psid->sidCopyright, PSID_STR_LEN))
    {
        th_io_error(ctx, ctx->errno,
            "Error reading SID file header: %s.\n",
            th_error_str(ctx->errno));
        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 (!thfread_be16(ctx, &psid->flags) ||
            !thfread_byte(ctx, &psid->startPage) ||
            !thfread_byte(ctx, &psid->pageLength) ||
            !thfread_be16(ctx, &psid->reserved))
        {
            th_io_error(ctx, ctx->errno,
                "Error reading PSID/RSID v2+ extra header data: %s.\n",
                th_error_str(ctx->errno));
            goto error;
        }
    }

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

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

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

            // Grab the actual load address
            psid->loadAddress = TH_LE16_TO_NATIVE(*(uint16_t *) data);

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

    // Append header data to hash
    si_append_hash16(&state, psid->initAddress);
    si_append_hash16(&state, psid->playAddress);
    si_append_hash16(&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(data);
    return ret;
}


const char *si_get_sid_clock_str(const 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 *si_get_sid_model_str(const 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 "?";
    }
}


// Free memory allocated for given SLDB node
//
static void si_sldb_node_free(SIDLibSLDBNode *node)
{
    if (node)
    {
        th_free_r(&node->lengths);
        th_free_r(&node);
    }
}


// Insert given node to db linked list
//
static void si_sldb_node_insert(SIDLibSLDB *dbh, SIDLibSLDBNode *node)
{
    if (dbh->nodes)
    {
        node->prev = dbh->nodes->prev;
        dbh->nodes->prev->next = node;
        dbh->nodes->prev = node;
    }
    else
    {
        dbh->nodes = node;
        node->prev = node;
    }
    node->next = NULL;
}


// Parse a time-entry in SLDB format
//
static int si_sldb_get_value(const char *str, size_t *pos)
{
    int result = 0;

    while (isdigit(str[*pos]))
        result = (result * 10) + (str[(*pos)++] - '0');

    return result;
}


static int si_sldb_gettime(const char *str, size_t *pos)
{
    int result;

    // Check if it starts with a digit
    if (th_isdigit(str[*pos]))
    {
        // Get minutes-field
        result = si_sldb_get_value(str, pos) * 60;

        // Check the field separator char
        if (str[*pos] == ':')
        {
            // Get seconds-field
            (*pos)++;
            result += si_sldb_get_value(str, pos);
        }
        else
            result = -2;
    }
    else
        result = -1;

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

    return result;
}


// Parse one SLDB definition line, return SLDB node
//
SIDLibSLDBNode *si_sldb_parse_entry(th_ioctx *ctx, const char *line)
{
    SIDLibSLDBNode *node = NULL;
    size_t pos, tmpLen, savePos;
    BOOL isOK;
    int i;

    // Allocate new node
    node = (SIDLibSLDBNode *) th_malloc0(sizeof(SIDLibSLDBNode));
    if (node == NULL)
    {
        th_io_error(ctx, THERR_MALLOC,
            "Error allocating new node.\n");
        return NULL;
    }

    // Get hash value
    pos = 0;
    for (i = 0; i < TH_MD5HASH_LENGTH; i++, pos += 2)
    {
        unsigned int tmpu;
        sscanf(&line[pos], "%2x", &tmpu);
        node->hash[i] = tmpu;
    }

    // Get playtimes
    th_findnext(line, &pos);
    if (line[pos] != '=')
    {
        th_io_error(ctx, THERR_INVALID_DATA,
            "'=' expected on column #%d.\n", pos);
        goto error;
    }

    // First playtime is after '='
    savePos = ++pos;
    tmpLen = strlen(line);

    // Get number of sub-tune lengths
    isOK = TRUE;
    while (pos < tmpLen && isOK)
    {
        th_findnext(line, &pos);

        if (si_sldb_gettime(line, &pos) >= 0)
            node->nlengths++;
        else
            isOK = FALSE;
    }

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

    node->lengths = (int *) th_malloc0(node->nlengths * sizeof(int));
    if (node->lengths == NULL)
    {
        th_io_error(ctx, THERR_MALLOC,
            "Could not allocate memory for node.\n");
        goto error;
    }

    // Read lengths in
    for (i = 0, pos = savePos, isOK = TRUE;
         pos < tmpLen && i < node->nlengths && isOK; i++)
    {
        int l;
        th_findnext(line, &pos);

        l = si_sldb_gettime(line, &pos);
        if (l >= 0)
            node->lengths[i] = l;
        else
            isOK = FALSE;
    }

    return node;

error:
    si_sldb_node_free(node);
    return NULL;
}


SIDLibSLDB * si_sldb_new(void)
{
    return (SIDLibSLDB *) th_malloc0(sizeof(SIDLibSLDB));
}


// Read SLDB database to memory
//
int si_sldb_read(th_ioctx *ctx, SIDLibSLDB *dbh)
{
    char *line = NULL;

    if ((line = th_malloc(PSID_BUFFER_SIZE)) == NULL)
    {
        th_io_error(ctx, THERR_MALLOC,
            "Error allocating temporary data buffer of %d bytes.\n",
            PSID_BUFFER_SIZE);
        return ctx->errno;
    }

    while (thfgets(line, PSID_BUFFER_SIZE, ctx) != NULL)
    {
        SIDLibSLDBNode *tmnode;
        size_t pos = 0;
        ctx->line++;

        th_findnext(line, &pos);

        // Check if it is datafield
        if (th_isxdigit(line[pos]))
        {
            // Check the length of the hash
            int hashLen;
            for (hashLen = 0; line[pos] && th_isxdigit(line[pos]); hashLen++, pos++);

            if (hashLen != TH_MD5HASH_LENGTH_CH)
            {
                th_io_error(ctx, THERR_INVALID_DATA,
                    "Invalid MD5-hash in SongLengthDB file '%s' line #%d:\n%s\n",
                    ctx->filename, ctx->line, line);
            }
            else
            {
                // Parse and add node to db
                if ((tmnode = si_sldb_parse_entry(ctx, line)) != NULL)
                {
                    si_sldb_node_insert(dbh, tmnode);
                }
                else
                {
                    th_io_error(ctx, THERR_INVALID_DATA,
                        "Invalid entry in SongLengthDB file '%s' line #%d:\n%s\n",
                        ctx->filename, ctx->line, line);
                }
            }
        }
        else
        if (line[pos] != ';' && line[pos] != '[' && line[pos] != 0)
        {
            th_io_error(ctx, THERR_INVALID_DATA,
                "Invalid line in SongLengthDB file '%s' line #%d:\n%s\n",
                ctx->filename, ctx->line, line);
        }
    }

    th_free(line);
    return THERR_OK;
}


// Compare two given MD5-hashes.
// Return: 0 if equal
//         negative if testHash1 < testHash2
//         positive if testHash1 > testHash2
//
static int si_sldb_compare_hash(th_md5hash_t testHash1, th_md5hash_t testHash2)
{
    int i, delta;

    for (i = delta = 0; i < TH_MD5HASH_LENGTH && !delta; i++)
        delta = testHash1[i] - testHash2[i];

    return delta;
}


// Compare two nodes.
// We assume here that we never ever get NULL-pointers.
static int si_sldb_compare_nodes(const void *node1, const void *node2)
{
    return si_sldb_compare_hash(
        (*(SIDLibSLDBNode **) node1)->hash,
        (*(SIDLibSLDBNode **) node2)->hash);
}


// (Re)create index
//
int si_sldb_build_index(SIDLibSLDB * dbh)
{
    SIDLibSLDBNode *node;

    // Free old index
    th_free_r(&dbh->pindex);

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

    // Check number of nodes
    if (dbh->n > 0)
    {
        size_t i;

        // Allocate memory for index-table
        dbh->pindex = (SIDLibSLDBNode **) th_malloc(sizeof(SIDLibSLDBNode *) * dbh->n);
        if (dbh->pindex == NULL)
            return THERR_MALLOC;

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

        // Sort the indexes
        qsort(dbh->pindex, dbh->n, sizeof(SIDLibSLDBNode *), si_sldb_compare_nodes);
    }

    return THERR_OK;
}


// Free a given song-length database
//
void si_sldb_free(SIDLibSLDB *dbh)
{
    if (dbh != NULL)
    {
        SIDLibSLDBNode *node = dbh->nodes;
        while (node != NULL)
        {
            SIDLibSLDBNode *next = node->next;
            si_sldb_node_free(node);
            node = next;
        }

        dbh->nodes = NULL;
        dbh->n = 0;

        th_free_r(&dbh->pindex);
        th_free(dbh);
    }
}


SIDLibSLDBNode *si_sldb_get_by_hash(SIDLibSLDB *dbh, th_md5hash_t hash)
{
    SIDLibSLDBNode keyItem, *key, **item;

    memcpy(&keyItem.hash, hash, sizeof(th_md5hash_t));
    key = &keyItem;
    item = bsearch(&key, dbh->pindex, dbh->n, sizeof(dbh->pindex[0]), si_sldb_compare_nodes);

    return (item != NULL) ? *item : NULL;
}