view sidlib.c @ 341:fe061ead51cc

Perform character set conversion after item formatting step instead of before it. This should remedy the potential issue of formatting not taking UTF8 multibyte into account, as our formatting unfortunately does not support multibyte encoding. This way the data should stay in ISO-8859-1 format just up to outputting it.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 13 Jan 2020 16:29:47 +0200
parents 629876cc0540
children c2ebcb0f0d62
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"


static char *sidlib_strdup_convert(SIDLibChConvCtx *chconv, const char *str)
{
    if (chconv != NULL)
        return chconv->convert(chconv, str);
    else
        return th_strdup(str);
}


static BOOL sidlib_fread_str(th_ioctx *ctx, SIDLibChConvCtx *chconv, 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;

    if (chconv != NULL)
    {
        *str = chconv->convert(chconv, tmp);
        th_free(tmp);
    }
    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, SIDLibChConvCtx *chconv)
{
    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.");
        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.");
        goto exit;
    }

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

    if (!sidlib_fread_str(ctx, chconv, &psid->sidName, SIDLIB_PSID_STR_LEN) ||
        !sidlib_fread_str(ctx, chconv, &psid->sidAuthor, SIDLIB_PSID_STR_LEN) ||
        !sidlib_fread_str(ctx, chconv, &psid->sidCopyright, SIDLIB_PSID_STR_LEN))
    {
        ret = th_io_error(ctx, ctx->status,
            "Error reading SID file header data.");
        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.");
            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, SIDLibChConvCtx *chconv)
{
    if ((*ppsid = th_malloc0(sizeof(SIDLibPSIDHeader))) == NULL)
        return THERR_MALLOC;

    (*ppsid)->allocated = TRUE;

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


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.");
        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 #%" PRIu_SIZE_T ".",
            pos);
        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 entry length data.");
        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.");
                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 / unexpected data at column #%" PRIu_SIZE_T ".",
                pos);
            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)
{
    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)
    {
        SIDLibSLDBNode *node;
        size_t i;

        // Check number of nodes against overflow
        if (dbh->nnodes > UINTPTR_MAX / sizeof(SIDLibSTILNode *))
            return THERR_BOUNDS;

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


SIDLibSTILSubTune *sidlib_stildb_find_subtune(SIDLibSTILNode *node, const int nsubtune)
{
    for (size_t n = 0; n < node->nsubtunes; n++)
    {
        SIDLibSTILSubTune *subtune = &node->subtunes[n];
        if (subtune->tune == nsubtune)
            return subtune;
    }
    return NULL;
}


static int sidlib_stildb_entry_realloc(
    SIDLibSTILNode *node,
    const int nsubtune, const BOOL alloc,
    const int nfield, const char *fdata,
    SIDLibChConvCtx *chconv)
{
    SIDLibSTILSubTune *subtune;

    if (node == NULL)
        return THERR_NULLPTR;

    // Get subtune or allocate new
    if ((subtune = sidlib_stildb_find_subtune(node, nsubtune)) == NULL)
    {
        node->subtunes = (SIDLibSTILSubTune *) th_realloc(
            node->subtunes, (node->nsubtunes + 1) * sizeof(SIDLibSTILSubTune));

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

        subtune = &node->subtunes[node->nsubtunes];
        memset(subtune, 0, sizeof(SIDLibSTILSubTune));
        subtune->tune = nsubtune;
        node->nsubtunes++;
    }

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

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

        field->data[field->ndata] = sidlib_strdup_convert(chconv, fdata);
        field->ndata++;
    }

    return THERR_OK;
}


static void sidlib_stildb_node_free(SIDLibSTILNode *node)
{
    if (node != NULL)
    {
        for (size_t nsubtune = 0; nsubtune < node->nsubtunes; nsubtune++)
        {
            SIDLibSTILSubTune *subtune = &node->subtunes[nsubtune];
            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(node->subtunes);
        th_free(node->filename);
        th_free(node);
    }
}


static int sidlib_stildb_node_new(SIDLibSTILNode **pnode, const char *filename,
    SIDLibChConvCtx *chconv)
{
    // 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
    // NOTE! Filename is not converted even if chconv != NULL
    if ((node->filename = th_strdup(filename)) == NULL)
        return THERR_MALLOC;

    if ((res = sidlib_stildb_entry_realloc(node, 1, FALSE, -1, NULL, chconv)) != 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
};


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, SIDLibChConvCtx *chconv)
{
    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'.",
                         ctx.ch);
                    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, chconv)) != 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,
                        "STIL entry filename too long.");
                    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 '%s' is not a valid integer.",
                        entry->filename, tmpStr);
                    goto out;
                }

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

                    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.");
                    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.",
                    ctx.ch);
                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.",
                        fieldName);
                    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.");
                    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, chconv)) != THERR_OK)
                    {
                        ret = th_io_error(fh, THERR_MALLOC,
                            "Could not allocate memory for field '%s'.",
                            fieldName);
                        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.",
                        fieldName);
                    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_compare_subtunes(const void *node1, const void *node2)
{
    return
        ((SIDLibSTILSubTune *) node1)->tune - ((SIDLibSTILSubTune *) node2)->tune;
}


static int sidlib_stildb_compare_nodes(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;

        // Check number of nodes against overflow
        if (dbh->nnodes > UINTPTR_MAX / sizeof(SIDLibSTILNode *))
            return THERR_BOUNDS;

        // 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 subtunes
            qsort(node->subtunes, node->nsubtunes,
                sizeof(SIDLibSTILSubTune), sidlib_stildb_compare_subtunes);
        }

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

    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_compare_nodes);

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