view sidlib.c @ 367:f73270cabde2

Bump copyright years.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 02 Jan 2021 11:37:13 +0200
parents 4978ff445572
children 22e8ee2df9ac
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-2021 Tecnic Software productions (TNSP)
 */
#include "sidlib.h"
#include "th_endian.h"
#include "th_string.h"


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

    // STF_LAST
};


const char *sidlib_stil_fields_lc[STF_LAST] =
{
    "name",
    "author",
    "title",
    "info",
    "artist",
    "comment",

    // STF_LAST
};


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


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


static int sidlib_stildb_get_field(const char *field)
{
    for (int n = 0; n < STF_LAST; n++)
    {
        if (strcmp(field, sidlib_stil_fields_uc[n]) == 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);
                    entry = NULL;
                }
                else
                if (ctx.ch == '(')
                {
                    if (entry == NULL)
                    {
                        ret = th_io_error(fh, THERR_INVALID_DATA,
                            "Unexpected start of subtune indicator without entry.");
                        goto out;
                    }

                    sidlib_stildb_set_parsemode(&ctx, PM_SUBTUNE);
                    ctx.ch = -1;
                    ctx.lineStart = TRUE;
                }
                else
                if (th_isalpha(ctx.ch))
                {
                    if (entry == NULL)
                    {
                        ret = th_io_error(fh, THERR_INVALID_DATA,
                            "Unexpected start of field name without entry.");
                        goto out;
                    }

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