MOBI_RET mobi_build_ncx(MOBIRawml *rawml, const OPF *opf) {
    /* parse ncx data */
    if (rawml == NULL) {
        debug_print("%s\n", "Initialization failed");
        return MOBI_INIT_FAILED;
    }
    if (rawml->ncx && rawml->ncx->cncx_record) {
        size_t i = 0;
        uint32_t maxlevel = 0;
        MOBI_RET ret;
        const size_t count = rawml->ncx->entries_count;
        if (count == 0) {
            return MOBI_SUCCESS;
        }
        NCX *ncx = malloc(count * sizeof(NCX));
        if (ncx == NULL) {
            debug_print("%s\n", "Memory allocation failed");
            return MOBI_MALLOC_FAILED;
        }
        MOBIAttrType pref_attr = ATTR_ID;
        while (i < count) {
            const MOBIIndexEntry *ncx_entry = &rawml->ncx->entries[i];
            const char *label = ncx_entry->label;
            const size_t id = strtoul(label, NULL, 16);
            uint32_t cncx_offset;
            ret = mobi_get_indxentry_tagvalue(&cncx_offset, ncx_entry, INDX_TAG_NCX_TEXT_CNCX);
            if (ret != MOBI_SUCCESS) {
                mobi_free_ncx(ncx, i);
                return ret;
            }
            const MOBIPdbRecord *cncx_record = rawml->ncx->cncx_record;
            char *text = mobi_get_cncx_string_utf8(cncx_record, cncx_offset, rawml->ncx->encoding);
            if (text == NULL) {
                mobi_free_ncx(ncx, i);
                debug_print("%s\n", "Memory allocation failed");
                return MOBI_MALLOC_FAILED;
            }
            char *target = malloc(MOBI_ATTRNAME_MAXSIZE + 1);
            if (target == NULL) {
                free(text);
                mobi_free_ncx(ncx, i);
                debug_print("%s\n", "Memory allocation failed");
                return MOBI_MALLOC_FAILED;
            }
            if (mobi_is_rawml_kf8(rawml)) {
                uint32_t posfid;
                ret = mobi_get_indxentry_tagvalue(&posfid, ncx_entry, INDX_TAG_NCX_POSFID);
                if (ret != MOBI_SUCCESS) {
                    free(text);
                    free(target);
                    mobi_free_ncx(ncx, i);
                    return ret;
                }
                uint32_t posoff;
                ret = mobi_get_indxentry_tagvalue(&posoff, ncx_entry, INDX_TAG_NCX_POSOFF);
                if (ret != MOBI_SUCCESS) {
                    free(text);
                    free(target);
                    mobi_free_ncx(ncx, i);
                    return ret;
                }
                uint32_t filenumber;
                char targetid[MOBI_ATTRNAME_MAXSIZE + 1];
                ret = mobi_get_id_by_posoff(&filenumber, targetid, rawml, posfid, posoff, &pref_attr);
                if (ret != MOBI_SUCCESS) {
                    free(text);
                    free(target);
                    mobi_free_ncx(ncx, i);
                    return ret;
                }
                /* FIXME: posoff == 0 means top of file? */
                if (posoff) {
                    int n = snprintf(target, MOBI_ATTRNAME_MAXSIZE + 1, "part%05u.html#%s", filenumber, targetid);
                    if (n > MOBI_ATTRVALUE_MAXSIZE + 1) {
                        debug_print("Warning: truncated target: %s\n", target);
                        snprintf(target, MOBI_ATTRNAME_MAXSIZE + 1, "part%05u.html", filenumber);
                    }
                } else {
                    snprintf(target, MOBI_ATTRNAME_MAXSIZE + 1, "part%05u.html", filenumber);
                }
                
            } else {
                uint32_t filepos;
                ret = mobi_get_indxentry_tagvalue(&filepos, ncx_entry, INDX_TAG_NCX_FILEPOS);
                if (ret != MOBI_SUCCESS) {
                    free(text);
                    free(target);
                    mobi_free_ncx(ncx, i);
                    return ret;
                }
                snprintf(target, MOBI_ATTRNAME_MAXSIZE + 1, "part00000.html#%010u", filepos);
            }
            uint32_t level;
            ret = mobi_get_indxentry_tagvalue(&level, ncx_entry, INDX_TAG_NCX_LEVEL);
            if (ret != MOBI_SUCCESS) {
                free(text);
                free(target);
                mobi_free_ncx(ncx, i);
                return ret;
            }
            if (level > maxlevel) {
                maxlevel = level;
            }
            uint32_t parent = MOBI_NOTSET;
            ret = mobi_get_indxentry_tagvalue(&parent, ncx_entry, INDX_TAG_NCX_PARENT);
            if (ret == MOBI_INIT_FAILED) {
                free(text);
                free(target);
                mobi_free_ncx(ncx, i);
                return ret;
            }
            uint32_t first_child = MOBI_NOTSET;
            ret = mobi_get_indxentry_tagvalue(&first_child, ncx_entry, INDX_TAG_NCX_CHILD_START);
            if (ret == MOBI_INIT_FAILED) {
                free(text);
                free(target);
                mobi_free_ncx(ncx, i);
                return ret;
            }
            uint32_t last_child = MOBI_NOTSET;
            ret = mobi_get_indxentry_tagvalue(&last_child, ncx_entry, INDX_TAG_NCX_CHILD_END);
            if (ret == MOBI_INIT_FAILED) {
                free(text);
                free(target);
                mobi_free_ncx(ncx, i);
                return ret;
            }
            if ((first_child != MOBI_NOTSET && first_child >= rawml->ncx->entries_count) ||
                (last_child != MOBI_NOTSET && last_child >= rawml->ncx->entries_count) ||
                (parent != MOBI_NOTSET && parent >= rawml->ncx->entries_count)) {
                free(text);
                free(target);
                mobi_free_ncx(ncx, i);
                return MOBI_DATA_CORRUPT;
            }
            debug_print("seq=%zu, id=%zu, text='%s', target='%s', level=%u, parent=%u, fchild=%u, lchild=%u\n", i, id, text, target, level, parent, first_child, last_child);
            ncx[i++] = (NCX) {id, text, target, level, parent, first_child, last_child};
        }
        mobi_write_ncx(rawml, ncx, opf, maxlevel);
        mobi_free_ncx(ncx, count);
    } else {
        mobi_write_ncx(rawml, NULL, opf, 1);
    }
    return MOBI_SUCCESS;
}