view sidlib.c @ 264:5afa6052f796

Change SIDLibSTILSubTune::fields to new SIDLibSTILField structure for fields, in order to allow multiple values for the same field, for example same (sub)tune might have multiple ARTIST fields designating different artists.
author Matti Hamalainen <ccr@tnsp.org>
date Sun, 05 Jan 2020 23:51:04 +0200
parents 0b66189c73d7
children 05bbe428a0ab
line wrap: on
line source

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


BOOL sidlib_fread_str(th_ioctx *ctx, char **str, const size_t len)
{
    char *tmp;

    if ((*str = tmp = th_malloc(len + 1)) == NULL)
    {
        th_io_error(ctx, THERR_MALLOC,
            "Could not allocate %" PRIu_SIZE_T " bytes for a string.",
            len);
        goto err;
    }

    if (!thfread_str(ctx, tmp, len))
    {
        th_io_error(ctx, THERR_FREAD,
            "Could not read %" PRIu_SIZE_T " bytes from file.",
            len);
        goto err;
    }

    tmp[len] = 0;
    return TRUE;

err:
    th_free(tmp);
    return FALSE;
}


static int sidlib_read_hash_data(th_ioctx *ctx, SIDLibPSIDHeader *psid,
    th_md5state_t *state, const BOOL newSLDB)
{
    int ret = THERR_OK;
    uint8_t *data = NULL;
    BOOL first = TRUE;
    size_t read;

    if ((data = (uint8_t *) th_malloc(SIDLIB_BUFFER_SIZE)) == NULL)
    {
        ret = th_io_error(ctx, THERR_MALLOC,
            "Error allocating temporary data buffer of %d bytes.",
            SIDLIB_BUFFER_SIZE);
        goto exit;
    }

    psid->dataSize = 0;
    do
    {
        read = thfread(data, sizeof(uint8_t), SIDLIB_BUFFER_SIZE, ctx);
        psid->dataSize += read;

        // If load address is 0 in header and we have the first block, grab it
        if (first && psid->loadAddress == 0)
        {
            if (read < 4)
            {
                ret = th_io_error(ctx, THERR_FREAD,
                    "Error reading song data, unexpectedly small file.");
                goto exit;
            }

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

            // .. do not include the load address to the hash if NEW SLDB format
            if (newSLDB)
                th_md5_append(state, &data[2], read - 2);
            else
                th_md5_append(state, data, read);

            first = FALSE;
        }
        else
        if (read > 0)
        {
            // Append data "as is"
            th_md5_append(state, data, read);
        }
    } while (read > 0 && !thfeof(ctx));

exit:
    th_free(data);
    return ret;
}


int sidlib_read_sid_file(th_ioctx *ctx, SIDLibPSIDHeader *psid, const BOOL newSLDB)
{
    int ret = THERR_OK;
    th_md5state_t state;
    off_t hdrStart, hdrEnd;

    hdrStart = thftell(ctx);

    // Read PSID header in
    if (!thfread_str(ctx, (uint8_t *) psid->magic, SIDLIB_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))
    {
        ret = th_io_error(ctx, ctx->status,
            "Could not read PSID/RSID header from '%s': %s.",
            ctx->filename, th_error_str(ctx->status));
        goto exit;
    }

    psid->magic[SIDLIB_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 > 4)
    {
        ret = th_io_error(ctx, THERR_NOT_SUPPORTED,
            "Not a supported PSID or RSID file: '%s'",
            ctx->filename);
        goto exit;
    }

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

    if (!sidlib_fread_str(ctx, &psid->sidName, SIDLIB_PSID_STR_LEN) ||
        !sidlib_fread_str(ctx, &psid->sidAuthor, SIDLIB_PSID_STR_LEN) ||
        !sidlib_fread_str(ctx, &psid->sidCopyright, SIDLIB_PSID_STR_LEN))
    {
        ret = th_io_error(ctx, ctx->status,
            "Error reading SID file header from '%s': %s.",
            ctx->filename, th_error_str(ctx->status));
        goto exit;
    }

    // Check if we need to load PSIDv2NG header ...
    if (psid->version >= 2)
    {
        // Yes, we need to
        if (!thfread_be16(ctx, &psid->flags) ||
            !thfread_u8(ctx, &psid->startPage) ||
            !thfread_u8(ctx, &psid->pageLength) ||
            !thfread_u8(ctx, &psid->sid2Addr) ||
            !thfread_u8(ctx, &psid->sid3Addr))
        {
            ret = th_io_error(ctx, ctx->status,
                "Error reading PSID/RSID v2+ extra header data from '%s': %s.",
                ctx->filename, th_error_str(ctx->status));
            goto exit;
        }
    }

    hdrEnd = thftell(ctx);

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

    if (newSLDB)
    {
        // New Songlengths.md5 style hash calculation:
        // We just hash the whole file, so seek back to beginning ..
        thfseek(ctx, hdrStart, SEEK_SET);

        if ((ret = sidlib_read_hash_data(ctx, psid, &state, FALSE)) != THERR_OK)
            goto exit;

        psid->dataSize -= hdrEnd - hdrStart;
    }
    else
    {
        // "Old" Songlengths.txt style MD5 hash calculation
        // We need to separately hash data etc.
        if ((ret = sidlib_read_hash_data(ctx, psid, &state, TRUE)) != THERR_OK)
            goto exit;

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

        // Append song speed data to hash
        uint8_t tmp8 = psid->isRSID ? 60 : 0;
        for (int 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 (int 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);

exit:
    return ret;
}


int sidlib_read_sid_file_alloc(th_ioctx *ctx, SIDLibPSIDHeader **ppsid, const BOOL newSLDB)
{
    if ((*ppsid = th_malloc0(sizeof(SIDLibPSIDHeader))) == NULL)
        return THERR_MALLOC;

    (*ppsid)->allocated = TRUE;

    return sidlib_read_sid_file(ctx, *ppsid, newSLDB);
}


void sidlib_free_sid_file(SIDLibPSIDHeader *psid)
{
    if (psid != NULL)
    {
        th_free(psid->sidName);
        th_free(psid->sidAuthor);
        th_free(psid->sidCopyright);

        if (psid->allocated)
            th_free(psid);
    }
}


const char *sidlib_get_sid_clock_str(const int flags)
{
    switch (flags)
    {
        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 *sidlib_get_sid_model_str(const int flags)
{
    switch (flags)
    {
        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 sidlib_sldb_node_free(SIDLibSLDBNode *node)
{
    if (node != NULL)
    {
        th_free(node->lengths);
        th_free(node);
    }
}


// Parse a time-entry in SLDB format
//
static int sidlib_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 sidlib_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 = sidlib_sldb_get_value(str, pos) * 60;

        // Check the field separator char
        if (str[*pos] == ':')
        {
            // Get seconds-field
            (*pos)++;
            result += sidlib_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
//
static int sidlib_sldb_parse_entry(th_ioctx *ctx, SIDLibSLDBNode **pnode, const char *line)
{
    int ret = THERR_OK;
    SIDLibSLDBNode *node;
    size_t pos, tmpLen, savePos;
    BOOL isOK;
    int i;

    // Allocate new node
    if ((*pnode = node = (SIDLibSLDBNode *) th_malloc0(sizeof(SIDLibSLDBNode))) == NULL)
    {
        ret = th_io_error(ctx, THERR_MALLOC,
            "Error allocating new SLDB node for SongLengthDB file '%s' line #%d:\n%s",
            ctx->filename, ctx->line, line);
        goto exit;
    }

    // Get hash value
    for (pos = 0, 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] != '=')
    {
        ret = th_io_error(ctx, THERR_INVALID_DATA,
            "'=' expected on column %d of SongLengthDB file '%s' line #%d:\n%s",
            pos, ctx->filename, ctx->line, line);
        goto exit;
    }

    // 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 (sidlib_sldb_gettime(line, &pos) >= 0)
            node->nlengths++;
        else
            isOK = FALSE;
    }

    // Allocate memory for lengths
    if (node->nlengths == 0)
    {
        ret = THERR_OK;
        goto exit;
    }

    if ((node->lengths = (int *) th_calloc(node->nlengths, sizeof(int))) == NULL)
    {
        ret = th_io_error(ctx, THERR_MALLOC,
            "Error allocating SLDB length data for SongLengthDB file '%s' line #%d:\n%s",
            ctx->filename, ctx->line, line);
        goto exit;
    }

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

        l = sidlib_sldb_gettime(line, &pos);

        if (l >= 0)
            node->lengths[i] = l;
        else
            isOK = FALSE;
    }

    return ret;

exit:
    sidlib_sldb_node_free(node);
    return ret;
}


int sidlib_sldb_new(SIDLibSLDB **pdbh)
{
    SIDLibSLDB *dbh;
    if ((dbh = *pdbh = (SIDLibSLDB *) th_malloc0(sizeof(SIDLibSLDB))) == NULL)
        return THERR_MALLOC;

    return THERR_OK;
}


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

    if ((line = th_malloc(SIDLIB_BUFFER_SIZE)) == NULL)
    {
        ret = th_io_error(ctx, THERR_MALLOC,
            "Error allocating temporary data buffer of %d bytes.",
            SIDLIB_BUFFER_SIZE);
        goto exit;
    }

    while (thfgets(line, SIDLIB_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)
            {
                ret = th_io_error(ctx, THERR_INVALID_DATA,
                    "Invalid MD5-hash in SongLengthDB file '%s' line #%d:\n%s",
                    ctx->filename, ctx->line, line);
                goto exit;
            }
            else
            {
                // Parse and add node to db
                if ((ret = sidlib_sldb_parse_entry(ctx, &tmnode, line)) != THERR_OK)
                    goto exit;

                th_llist_append_node((th_llist_t **) &dbh->nodes, (th_llist_t *) tmnode);
            }
        }
        else
        if (line[pos] != ';' && line[pos] != '[' && line[pos] != 0)
        {
            ret = th_io_error(ctx, THERR_INVALID_DATA,
                "Invalid line in SongLengthDB file '%s' line #%d:\n%s",
                ctx->filename, ctx->line, line);
            goto exit;
        }
    }

exit:
    th_free(line);
    return ret;
}


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

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

    return delta;
}


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


// (Re)create index
//
int sidlib_sldb_build_index(SIDLibSLDB *dbh)
{
    // Free old index
    th_free_r(&(dbh->pindex));

    // Get size of db
    dbh->nnodes = th_llist_length((th_llist_t *) dbh->nodes);

    // Check number of nodes
    if (dbh->nnodes > 0)
    {
        SIDLibSLDBNode *node;
        size_t i;

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

        // Get node-pointers to table
        for (i = 0, node = dbh->nodes; node != NULL && i < dbh->nnodes;
            node = (SIDLibSLDBNode *) node->node.next)
            dbh->pindex[i++] = node;

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

    return THERR_OK;
}


// Free a given song-length database
//
void sidlib_sldb_free(SIDLibSLDB *dbh)
{
    if (dbh != NULL)
    {
        th_llist_free_func_node((th_llist_t *) dbh->nodes,
            (void (*)(th_llist_t *)) sidlib_sldb_node_free);

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

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


SIDLibSLDBNode *sidlib_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->nnodes, sizeof(dbh->pindex[0]), sidlib_sldb_compare_nodes);

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


/* STIL database handling functions
 */
int sidlib_stildb_new(SIDLibSTILDB **pdbh)
{
    SIDLibSTILDB *dbh;
    if ((dbh = *pdbh = (SIDLibSTILDB *) th_malloc0(sizeof(SIDLibSTILDB))) == NULL)
        return THERR_MALLOC;

    return THERR_OK;
}


static int sidlib_stildb_entry_realloc(SIDLibSTILNode *node,
    const int nsubtune, const BOOL alloc,
    const int nfield, const char *fdata)
{
    if (node == NULL)
        return THERR_NULLPTR;

    // Re-allocate subtunes pointers structure if needed
    if (nsubtune > node->nsubtunes)
    {
        size_t clearIndex, clearLength;

        node->subtunes = (SIDLibSTILSubTune **) th_realloc(
            node->subtunes, (nsubtune + 1) * sizeof(SIDLibSTILSubTune *));

        if (node->subtunes == NULL)
            return THERR_MALLOC;

        // Clear the newly allocated memory
        if (node->nsubtunes == 0)
        {
            clearIndex = 0;
            clearLength = nsubtune + 1;
        }
        else
        {
            clearIndex = node->nsubtunes + 1;
            clearLength = nsubtune - clearIndex + 1;
        }

        memset(&(node->subtunes[clearIndex]), 0, clearLength * sizeof(SIDLibSTILSubTune *));

        node->nsubtunes = nsubtune;
    }

    // Allocate memory for the specified subtune
    if (alloc && node->subtunes[nsubtune] == NULL)
    {
        node->subtunes[nsubtune] = (SIDLibSTILSubTune *) th_malloc0(sizeof(SIDLibSTILSubTune));
        if (node->subtunes[nsubtune] == NULL)
            return THERR_MALLOC;

    }

    // Allocate field space, if needed
    if (alloc && nfield >= 0 && nfield < STF_LAST)
    {
        SIDLibSTILField *field = &node->subtunes[nsubtune]->fields[nfield];

        if ((field->data = th_realloc(field->data, (field->ndata + 1) * sizeof(char *))) == NULL)
            return THERR_MALLOC;

        field->data[field->ndata] = th_strdup(fdata);
        field->ndata++;
    }

    return THERR_OK;
}


static void sidlib_stildb_node_free(SIDLibSTILNode *node)
{
    if (node != NULL)
    {
        for (int nsubtune = 0; nsubtune <= node->nsubtunes; nsubtune++)
        {
            SIDLibSTILSubTune *subtune = node->subtunes[nsubtune];
            if (subtune != NULL)
            {
                for (int nfield = 0; nfield < STF_LAST; nfield++)
                {
                    SIDLibSTILField *field = &subtune->fields[nfield];
                    for (int n = 0; n < field->ndata; n++)
                        th_free(field->data[n]);

                    th_free(field->data);
                }

                th_free(subtune);
            }
        }

        th_free(node->subtunes);
        th_free(node->filename);
        th_free(node);
    }
}


static int sidlib_stildb_node_new(SIDLibSTILNode **pnode, const char *filename)
{
    // Allocate memory for new node
    SIDLibSTILNode *node;
    int res;

    if ((node = *pnode = (SIDLibSTILNode *) th_malloc0(sizeof(SIDLibSTILNode))) == NULL)
        return THERR_MALLOC;

    // Allocate filename and initial space for one subtune
    if ((node->filename = th_strdup(filename)) == NULL)
        return THERR_MALLOC;

    if ((res = sidlib_stildb_entry_realloc(node, 1, FALSE, -1, NULL)) != THERR_OK)
    {
        sidlib_stildb_node_free(node);
        return res;
    }

    return res;
}


/* Free a given STIL database
 */
void sidlib_stildb_free(SIDLibSTILDB *dbh)
{
    if (dbh != NULL)
    {
        th_llist_free_func_node((th_llist_t *) dbh->nodes,
            (void (*)(th_llist_t *)) sidlib_stildb_node_free);

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

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


/* Read database (additively) to given db-structure
 */
enum
{
    PM_EOF,
    PM_ERROR,
    PM_IDLE,
    PM_COMMENT,
    PM_NEXT,
    PM_ENTRY,
    PM_FIELD_NAME,
    PM_FIELD_DATA,
    PM_SUBTUNE,

    PM_LAST
};


static const char *sidlib_stil_fields[STF_LAST] =
{
    "NAME",
    "AUTHOR",
    "TITLE",
    "INFO",
    "ARTIST",
    "COMMENT",

    // STF_LAST
};


typedef struct
{
    size_t lineNum;
    ssize_t linePos;
    int ch, prevMode, nextMode, parseMode;
    BOOL lineStart;
} SIDLibSTILParserCtx;


static int sidlib_stildb_get_field(const char *name)
{
    for (int n = 0; n < STF_LAST; n++)
    {
        if (strcmp(sidlib_stil_fields[n], name) == 0)
            return n;
    }

    return -1;
}


static void sidlib_stildb_set_parsemode(SIDLibSTILParserCtx *ctx, const int mode)
{
    ctx->prevMode = ctx->parseMode;
    ctx->parseMode = mode;
}


static void sidlib_stildb_set_next_parsemode(SIDLibSTILParserCtx *ctx, const int mode)
{
    sidlib_stildb_set_parsemode(ctx, PM_NEXT);
    ctx->nextMode = mode;
}


#define VADDCH(ch) if (strPos < SIDLIB_BUFFER_SIZE) { tmpStr[strPos++] = ch; }


int sidlib_stildb_read(th_ioctx *fh, SIDLibSTILDB *dbh)
{
    int ret = THERR_OK, field = -1;
    SIDLibSTILParserCtx ctx;
    char *tmpStr = NULL, *fieldName = NULL;
    size_t strPos;
    SIDLibSTILNode *entry = NULL;
    int subtune;

    if (fh == NULL || dbh == NULL)
        return THERR_NULLPTR;

    if ((tmpStr = th_malloc(SIDLIB_BUFFER_SIZE + 1)) == NULL)
        return THERR_MALLOC;

    // Initialize values
    memset(&ctx, 0, sizeof(ctx));
    ctx.nextMode = ctx.prevMode = ctx.parseMode = PM_IDLE;
    ctx.ch = -1;
    ctx.lineStart = TRUE;
    strPos = 0;

    // Parse the STIL database
    while (ctx.parseMode != PM_EOF && ctx.parseMode != PM_ERROR)
    {
        if (ctx.ch == -1)
        {
            // Get next character
            switch (ctx.ch = thfgetc(fh))
            {
            case '\n':
                fh->line++;
                ctx.linePos = -1;
                break;

            default:
                if (ctx.linePos < 0)
                {
                    ctx.lineStart = TRUE;
                    ctx.linePos = 0;
                }
                else
                    ctx.linePos++;
            }
        }

        switch (ctx.parseMode)
        {
        case PM_COMMENT:
            // Comment parsing mode
            if (ctx.ch == '\n')
            {
                // End of line, end of comment
                sidlib_stildb_set_parsemode(&ctx, ctx.prevMode);
            }
            ctx.ch = -1;
            break;

        case PM_NEXT:
        case PM_IDLE:
            // Normal parsing mode
            if (ctx.ch == EOF)
            {
                sidlib_stildb_set_parsemode(&ctx, PM_EOF);
            }
            else
            if (ctx.ch == '#')
            {
                sidlib_stildb_set_parsemode(&ctx, PM_COMMENT);
                ctx.ch = -1;
            }
            else
            if (th_iscrlf(ctx.ch) || th_isspace(ctx.ch))
            {
                ctx.ch = -1;
            }
            else
            if (ctx.parseMode == PM_IDLE)
            {
                // PM_IDLE
                strPos = 0;
                if (ctx.ch == '/')
                {
                    sidlib_stildb_set_parsemode(&ctx, PM_ENTRY);
                }
                else
                if (ctx.ch == '(')
                {
                    sidlib_stildb_set_parsemode(&ctx, PM_SUBTUNE);
                    ctx.ch = -1;
                    ctx.lineStart = TRUE;
                }
                else
                if (th_isalpha(ctx.ch))
                {
                    sidlib_stildb_set_parsemode(&ctx, PM_FIELD_NAME);
                }
                else
                {
                    // Error! Invalid character found
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "Unexpected character '%c' on line #%d.",
                         ctx.ch, fh->line);
                    goto out;
                }
            }
            else
            {
                // PM_NEXT - Next item found
                sidlib_stildb_set_parsemode(&ctx, ctx.nextMode);
            }
            break;

        case PM_ENTRY:
            if (ctx.ch == EOF || th_iscrlf(ctx.ch) || th_isspace(ctx.ch))
            {
                // A new file / entry, allocate and append
                tmpStr[strPos] = 0;

                if ((ret = sidlib_stildb_node_new(&entry, tmpStr)) != THERR_OK)
                    goto out;

                th_llist_append_node((th_llist_t **) &dbh->nodes, (th_llist_t *) entry);

                // Default subtune is 0, for "main tune" information
                subtune = 0;

                sidlib_stildb_set_parsemode(&ctx, PM_IDLE);
            }
            else
            {
                VADDCH(ctx.ch)
                else
                {
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "Entry filename too long on line #%d.",
                        fh->line);
                    goto out;
                }
                ctx.ch = -1;
            }
            break;

        case PM_SUBTUNE:
            if (ctx.ch == EOF || ctx.ch == ')')
            {
                BOOL neg = FALSE;

                // Subtune indicator end
                tmpStr[strPos] = 0;

                if (!th_get_int(tmpStr, (unsigned int *) &subtune, &neg) || neg)
                {
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "Entry '%s' subtune indicator not a valid integer on line #%d: '%s'",
                        entry->filename, fh->line, tmpStr);
                    goto out;
                }

                if (subtune <= 0 || subtune > 128)
                {
                    th_io_error(fh, THERR_INVALID_DATA,
                        "Entry '%s' subtune number %d is invalid on line #%d.",
                        entry->filename, subtune, fh->line);

                    subtune = -1;
                }

                sidlib_stildb_set_parsemode(&ctx, PM_IDLE);
            }
            else
            if (th_isdigit(ctx.ch))
            {
                if (ctx.lineStart)
                    goto sub_unexpected;

                VADDCH(ctx.ch)
                else
                {
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "Subtune indicator too long on line #%d.",
                        fh->line);
                    goto out;
                }
            }
            else
            if ((ctx.ch == '#' && strPos > 0) || (ctx.ch != '#' && !th_isspace(ctx.ch)))
            {
sub_unexpected:
                ret = th_io_error(fh, THERR_INVALID_DATA,
                    "Unexpected character '%c' in subtune indicator on line #%d.",
                    ctx.ch, fh->line);
                goto out;
            }

            ctx.ch = -1;
            ctx.lineStart = FALSE;
            break;

        case PM_FIELD_NAME:
            if (ctx.ch == EOF || ctx.ch == ':' || th_iscrlf(ctx.ch) || th_isspace(ctx.ch))
            {
                tmpStr[strPos] = 0;
                th_pstr_cpy(&fieldName, tmpStr);

                field = sidlib_stildb_get_field(tmpStr);

                if (entry == NULL)
                {
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "No STIL entry allocated, but field '%s' found on line #%d.",
                        fieldName, fh->line);
                    goto out;
                }

                sidlib_stildb_set_next_parsemode(&ctx, PM_FIELD_DATA);
                ctx.lineStart = FALSE;
                strPos = 0;
            }
            else
            {
                VADDCH(ctx.ch)
                else
                {
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "Field name too long on line #%d.",
                        fieldName, fh->line);
                    goto out;
                }
            }

            ctx.ch = -1;
            break;

        case PM_FIELD_DATA:
            if (ctx.lineStart && th_isspace(ctx.ch) && ctx.linePos < 8)
            {
                ctx.ch = -1;
            }
            else
            if (th_iscrlf(ctx.ch))
            {
                ctx.lineStart = TRUE;
                ctx.ch = -1;
            }
            else
            if (ctx.ch == EOF || (ctx.lineStart && ctx.linePos < 8))
            {
                // Field done
                tmpStr[strPos] = 0;

                if (field >= 0 && subtune >= 0)
                {
                    // Supported field, add it
                    if ((ret = sidlib_stildb_entry_realloc(
                        entry, subtune, TRUE, field, tmpStr)) != THERR_OK)
                    {
                        ret = th_io_error(fh, THERR_MALLOC,
                            "Could not allocate memory for field '%s' on line #%d.",
                            fieldName, fh->line);
                        goto out;
                    }
                }

                sidlib_stildb_set_parsemode(&ctx, PM_IDLE);
            }
            else
            {
                ctx.lineStart = FALSE;
                VADDCH(ctx.ch)
                else
                {
                    ret = th_io_error(fh, THERR_INVALID_DATA,
                        "Field '%s' data too long on line #%d.",
                        fieldName, fh->line);
                    goto out;
                }
                ctx.ch = - 1;
            }
            break;
        }
    }

out:

    th_free(tmpStr);
    th_free(fieldName);

    if (ret == THERR_OK && ctx.parseMode == PM_ERROR)
        ret = THERR_INVALID_DATA;

    return ret;
}


/* Compare two nodes
 */
static int sidlib_stildb_cmp(const void *node1, const void *node2)
{
    // We assume here that we never ever get NULL-pointers or similar
    return strcmp(
        (*(SIDLibSTILNode * *) node1)->filename,
        (*(SIDLibSTILNode * *) node2)->filename);
}


/* (Re)create index
 */
int sidlib_stildb_build_index(SIDLibSTILDB *dbh)
{
    if (dbh == NULL)
        return THERR_NULLPTR;

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

    // Get size of db
    dbh->nnodes = th_llist_length((th_llist_t *) dbh->nodes);

    // Check number of nodes
    if (dbh->nnodes > 0)
    {
        SIDLibSTILNode *node;
        size_t i;

        // XXX TODO Check number of nodes?

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

        // Get node-pointers to table
        for (i = 0, node = dbh->nodes; node != NULL && i < dbh->nnodes;
            node = (SIDLibSTILNode *) node->node.next)
            dbh->pindex[i++] = node;

        // Sort the indexes
        qsort(dbh->pindex, dbh->nnodes, sizeof(SIDLibSTILNode *), sidlib_stildb_cmp);
    }

    return THERR_OK;
}


/* Get STIL information node from database
 */
SIDLibSTILNode *sidlib_stildb_get_node(SIDLibSTILDB *dbh, const char *filename)
{
    SIDLibSTILNode keyItem, *key, **item;

    keyItem.filename = (char *) filename;
    key = &keyItem;
    item = bsearch(&key, dbh->pindex, dbh->nnodes, sizeof(SIDLibSTILNode *), sidlib_stildb_cmp);

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