/*
 * Copyright (C) 2007-2020 S[&]T, The Netherlands.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "coda-cdf-internal.h"
#include "coda-mem-internal.h"
#include "coda-read-bytes.h"
#include "coda-swap2.h"
#include "coda-swap4.h"
#include "coda-swap8.h"

#ifdef HAVE_SYS_MMAN_H
#include <sys/mman.h>
#endif
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include "zlib.h"

static void rtrim(char *str)
{
    long length;

    length = (long)strlen(str);
    while (length > 0 && str[length - 1] == ' ')
    {
        str[length - 1] = '\0';
        length--;
    }
}

static int read_attribute_sub(coda_cdf_product *product_file, int64_t offset, int32_t byte_size, coda_type *definition,
                              coda_dynamic_type **attribute)
{
    if (definition->type_class == coda_text_class)
    {
        char *buffer;

        buffer = malloc(byte_size + 1);
        if (buffer == NULL)
        {
            coda_set_error(CODA_ERROR_OUT_OF_MEMORY, "out of memory (could not allocate %lu bytes) (%s:%u)",
                           (long)byte_size + 1, __FILE__, __LINE__);
            return -1;
        }
        if (read_bytes(product_file->raw_product, offset, byte_size, buffer) != 0)
        {
            free(buffer);
            return -1;
        }
        buffer[byte_size] = '\0';
        *attribute = (coda_dynamic_type *)coda_mem_string_new((coda_type_text *)definition, NULL,
                                                              (coda_product *)product_file, buffer);
        free(buffer);
    }
    else
    {
#ifdef WORDS_BIGENDIAN
        coda_endianness other_endianness = coda_little_endian;
#else
        coda_endianness other_endianness = coda_big_endian;
#endif
        union
        {
            int8_t as_int8[8];
            uint8_t as_uint8[8];
            int16_t as_int16[4];
            uint16_t as_uint16[4];
            int32_t as_int32[2];
            uint32_t as_uint32[2];
            int64_t as_int64[1];
            float as_float[2];
            double as_double[1];
        } buffer;

        if (read_bytes(product_file->raw_product, offset, byte_size, buffer.as_int8) != 0)
        {
            return -1;
        }
        if (product_file->endianness == other_endianness)
        {
            switch (byte_size)
            {
                case 1:
                    break;
                case 2:
                    swap2(buffer.as_int16);
                    break;
                case 4:
                    swap4(buffer.as_int32);
                    break;
                case 8:
                    swap8(buffer.as_int64);
                    break;
                default:
                    assert(0);
                    exit(1);
            }
        }
        switch (definition->read_type)
        {
            case coda_native_type_int8:
                *attribute = (coda_dynamic_type *)coda_mem_int8_new((coda_type_number *)definition, NULL,
                                                                    (coda_product *)product_file, buffer.as_int8[0]);
                break;
            case coda_native_type_uint8:
                *attribute = (coda_dynamic_type *)coda_mem_uint8_new((coda_type_number *)definition, NULL,
                                                                     (coda_product *)product_file, buffer.as_uint8[0]);
                break;
            case coda_native_type_int16:
                *attribute = (coda_dynamic_type *)coda_mem_int16_new((coda_type_number *)definition, NULL,
                                                                     (coda_product *)product_file, buffer.as_int16[0]);
                break;
            case coda_native_type_uint16:
                *attribute = (coda_dynamic_type *)coda_mem_uint16_new((coda_type_number *)definition, NULL,
                                                                      (coda_product *)product_file,
                                                                      buffer.as_uint16[0]);
                break;
            case coda_native_type_int32:
                *attribute = (coda_dynamic_type *)coda_mem_int32_new((coda_type_number *)definition, NULL,
                                                                     (coda_product *)product_file, buffer.as_int32[0]);
                break;
            case coda_native_type_uint32:
                *attribute = (coda_dynamic_type *)coda_mem_uint32_new((coda_type_number *)definition, NULL,
                                                                      (coda_product *)product_file,
                                                                      buffer.as_uint32[0]);
                break;
            case coda_native_type_int64:
                *attribute = (coda_dynamic_type *)coda_mem_int64_new((coda_type_number *)definition, NULL,
                                                                     (coda_product *)product_file, buffer.as_int64[0]);
                break;
            case coda_native_type_float:
                *attribute = (coda_dynamic_type *)coda_mem_float_new((coda_type_number *)definition, NULL,
                                                                     (coda_product *)product_file, buffer.as_float[0]);
                break;
            case coda_native_type_double:
                *attribute = (coda_dynamic_type *)coda_mem_double_new((coda_type_number *)definition, NULL,
                                                                      (coda_product *)product_file,
                                                                      buffer.as_double[0]);
                break;
            default:
                assert(0);
                exit(1);
        }
    }
    if (*attribute == NULL)
    {
        return -1;
    }
    return 0;
}

static int read_attribute(coda_cdf_product *product_file, int64_t offset, int32_t data_type,
                          int32_t num_elems, coda_dynamic_type **attribute)
{
    coda_type_class type_class;
    coda_native_type native_type;
    coda_type *definition;
    int32_t byte_size;

    switch (data_type)
    {
        case 1:        /* INT1 */
        case 41:       /* BYTE */
            type_class = coda_integer_class;
            native_type = coda_native_type_int8;
            byte_size = 1;
            break;
        case 2:        /* INT2 */
            type_class = coda_integer_class;
            native_type = coda_native_type_int16;
            byte_size = 2;
            break;
        case 4:        /* INT4 */
            type_class = coda_integer_class;
            native_type = coda_native_type_int32;
            byte_size = 4;
            break;
        case 8:        /* INT8 */
        case 33:       /* CDF_TIME_TT2000 */
            type_class = coda_integer_class;
            native_type = coda_native_type_int64;
            byte_size = 8;
            break;
        case 11:       /* UINT1 */
            type_class = coda_integer_class;
            native_type = coda_native_type_uint8;
            byte_size = 1;
            break;
        case 12:       /* UINT2 */
            type_class = coda_integer_class;
            native_type = coda_native_type_uint16;
            byte_size = 2;
            break;
        case 14:       /* UINT4 */
            type_class = coda_integer_class;
            native_type = coda_native_type_uint32;
            byte_size = 4;
            break;
        case 21:       /* REAL4 */
        case 44:       /* FLOAT */
            type_class = coda_real_class;
            native_type = coda_native_type_float;
            byte_size = 4;
            break;
        case 22:       /* REAL8 */
        case 31:       /* CDF_EPOCH */
        case 45:       /* DOUBLE */
            type_class = coda_real_class;
            native_type = coda_native_type_double;
            byte_size = 8;
            break;
        case 51:       /* CHAR */
        case 52:       /* UCHAR */
            type_class = coda_text_class;
            native_type = coda_native_type_string;
            byte_size = num_elems;
            break;
        default:
            coda_set_error(CODA_ERROR_INVALID_ARGUMENT, "invalid CDF data type (%d)", data_type);
            return -1;
    }
    switch (type_class)
    {
        case coda_integer_class:
        case coda_real_class:
            definition = (coda_type *)coda_type_number_new(coda_format_cdf, type_class);
            break;
        case coda_text_class:
            definition = (coda_type *)coda_type_text_new(coda_format_cdf);
            break;
        default:
            assert(0);
            exit(1);
    }
    if (definition == NULL)
    {
        return -1;
    }
    if (coda_type_set_read_type(definition, native_type) != 0)
    {
        coda_type_release(definition);
        return -1;
    }
    if (type_class != coda_text_class)
    {
        if (coda_type_set_byte_size(definition, byte_size) != 0)
        {
            coda_type_release(definition);
            return -1;
        }
#ifndef WORDS_BIGENDIAN
        if (coda_type_number_set_endianness((coda_type_number *)definition, coda_little_endian) != 0)
        {
            coda_type_release(definition);
            return -1;
        }
#endif
    }

    if (num_elems != 1 && data_type != 51 && data_type != 52)
    {
        coda_type_array *array_definition;
        coda_mem_array *array = NULL;
        int i;

        /* create an array */
        array_definition = coda_type_array_new(coda_format_cdf);
        if (array_definition == NULL)
        {
            coda_type_release(definition);
            return -1;
        }
        if (coda_type_array_set_base_type(array_definition, definition) != 0)
        {
            coda_type_release((coda_type *)array_definition);
            coda_type_release(definition);
            return -1;
        }
        coda_type_release(definition);
        if (coda_type_array_add_variable_dimension(array_definition, NULL) != 0)
        {
            coda_type_release((coda_type *)array_definition);
            return -1;
        }
        array = coda_mem_array_new(array_definition, NULL);
        if (array == NULL)
        {
            coda_type_release((coda_type *)array_definition);
            return -1;
        }
        coda_type_release((coda_type *)array_definition);
        for (i = 0; i < num_elems; i++)
        {
            if (read_attribute_sub(product_file, offset + i * byte_size, byte_size, definition, attribute) != 0)
            {
                coda_dynamic_type_delete((coda_dynamic_type *)array);
                return -1;
            }
            if (coda_mem_array_add_element(array, *attribute) != 0)
            {
                coda_dynamic_type_delete((coda_dynamic_type *)array);
                coda_dynamic_type_delete(*attribute);
                return -1;
            }
        }
    }
    else
    {
        if (read_attribute_sub(product_file, offset, byte_size, definition, attribute) != 0)
        {
            coda_type_release(definition);
            return -1;
        }
        coda_type_release(definition);
    }

    return 0;
}

static int read_AEDR(coda_cdf_product *product_file, int64_t offset, const char *name, int32_t scope)
{
    coda_dynamic_type *attribute;
    int32_t record_type;
    int64_t aedr_next;
    int32_t attr_num;
    int32_t data_type;
    int32_t num;
    int32_t num_elems;

    if (offset == 0)
    {
        return 0;
    }

    if (read_bytes(product_file->raw_product, offset + 8, 4, &record_type) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&record_type);
#endif
    if (record_type != 5 && record_type != 9)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for AEDR record", record_type);
        return -1;
    }

    if (read_bytes(product_file->raw_product, offset + 12, 8, &aedr_next) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 20, 4, &attr_num) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 24, 4, &data_type) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 28, 4, &num) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 32, 4, &num_elems) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int64(&aedr_next);
    swap_int32(&attr_num);
    swap_int32(&data_type);
    swap_int32(&num);
    swap_int32(&num_elems);
#endif
    offset += 56;
    if (aedr_next < 0 || aedr_next >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for AEDR record");
        return -1;
    }
    if (attr_num < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid attribute number in AEDR record");
        return -1;
    }
    if (num < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid entry number in AEDR record");
        return -1;
    }
    if (num_elems < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of entries in AEDR record");
        return -1;
    }

    if (data_type == 32)
    {
        coda_set_error(CODA_ERROR_UNSUPPORTED_PRODUCT, "CDF EPOCH16 data type is not supported");
        return -1;
    }

    if (read_attribute(product_file, offset, data_type, num_elems, &attribute) != 0)
    {
        return -1;
    }
    if (record_type == 5 && (scope & 1))
    {
        if (coda_mem_type_add_attribute((coda_mem_type *)product_file->root_type, name, attribute, 1) != 0)
        {
            coda_dynamic_type_delete(attribute);
            return -1;
        }
    }
    else
    {
        if (num < 0 || num >= product_file->root_type->num_fields)
        {
            coda_set_error(CODA_ERROR_PRODUCT,
                           "CDF Attribute entry number (%d) is outside range of available variables " "[0,%ld]", num,
                           product_file->root_type->num_fields - 1);
            return -1;
        }

        assert(((coda_cdf_variable *)product_file->root_type->field_type[num])->backend == coda_backend_cdf);
        if (coda_cdf_variable_add_attribute((coda_cdf_variable *)product_file->root_type->field_type[num], name,
                                            attribute, 1) != 0)
        {
            coda_dynamic_type_delete(attribute);
            return -1;
        }
    }

    if (read_AEDR(product_file, aedr_next, name, scope) != 0)
    {
        return -1;
    }

    return 0;
}

static int read_ADR(coda_cdf_product *product_file, int64_t offset)
{
    int32_t record_type;
    int64_t adr_next;
    int64_t agredr_head;
    int32_t scope;
    int32_t num;
    int32_t ngr_entries;
    int32_t maxgr_entry;
    int64_t azedr_head;
    int32_t nz_entries;
    int32_t maxz_entry;
    int64_t aedr_head;
    char name[257];

    if (offset == 0)
    {
        return 0;
    }

    if (read_bytes(product_file->raw_product, offset + 8, 4, &record_type) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&record_type);
#endif
    if (record_type != 4)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for ADR record", record_type);
        return -1;
    }

    if (read_bytes(product_file->raw_product, offset + 12, 8, &adr_next) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 20, 8, &agredr_head) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 28, 4, &scope) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 32, 4, &num) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 36, 4, &ngr_entries) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 40, 4, &maxgr_entry) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 48, 8, &azedr_head) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 56, 4, &nz_entries) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 60, 4, &maxz_entry) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 68, 256, name) < 0)
    {
        return -1;
    }
    name[256] = '\0';
    rtrim(name);
#ifndef WORDS_BIGENDIAN
    swap_int64(&adr_next);
    swap_int64(&agredr_head);
    swap_int32(&scope);
    swap_int32(&num);
    swap_int32(&ngr_entries);
    swap_int32(&maxgr_entry);
    swap_int64(&azedr_head);
    swap_int32(&nz_entries);
    swap_int32(&maxz_entry);
#endif
    if (adr_next < 0 || adr_next >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for ADR record");
        return -1;
    }
    if (agredr_head < 0 || agredr_head >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for AgrEDR record");
        return -1;
    }
    if (num < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid attribute number in ADR record");
        return -1;
    }
    if (ngr_entries < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of g/rEntries in ADR record");
        return -1;
    }
    if (azedr_head < 0 || azedr_head >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for AzEDR record");
        return -1;
    }
    if (nz_entries < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of zEntries in ADR record");
        return -1;
    }

    aedr_head = agredr_head;
    if (scope & 1)
    {
        if (nz_entries != 0)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "gADR record has non-zero NzEntries (%d)", nz_entries);
            return -1;
        }
    }
    else if (ngr_entries == 0)
    {
        aedr_head = azedr_head;
    }

    if (read_AEDR(product_file, aedr_head, name, scope) != 0)
    {
        return -1;
    }

    if (read_ADR(product_file, adr_next) != 0)
    {
        return -1;
    }

    return 0;
}

static int read_VXR(coda_cdf_product *product_file, coda_cdf_variable *variable, int64_t offset, int32_t first,
                    int32_t last);

static int read_VR(coda_cdf_product *product_file, coda_cdf_variable *variable, int64_t offset, int32_t first,
                   int32_t last)
{
    int32_t record_type;

    if (offset == 0)
    {
        return 0;
    }

    if (read_bytes(product_file->raw_product, offset + 8, 4, &record_type) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&record_type);
#endif
    if (record_type == 6)
    {
        return read_VXR(product_file, variable, offset, first, last);
    }
    if (record_type == 7)
    {
        int i;

        if (last >= variable->num_records)
        {
            last = variable->num_records - 1;
        }
        for (i = first; i <= last; i++)
        {
            variable->offset[i] = offset + 12 + (i - first) * variable->num_values_per_record * variable->value_size;
        }
    }
    else if (record_type == 13)
    {
        int64_t csize;
        Bytef *buffer;
        z_stream zs;
        int partial_read = 0;
        int result;
        int i;

        if (first >= variable->num_records)
        {
            /* completely skip this record */
            return 0;
        }

        if (variable->data == NULL)
        {
            variable->data = malloc(variable->num_records * variable->num_values_per_record * variable->value_size);
            if (variable->data == NULL)
            {
                coda_set_error(CODA_ERROR_OUT_OF_MEMORY, "out of memory (could not allocate %lu bytes) (%s:%u)",
                               (long)variable->num_records * variable->num_values_per_record * variable->value_size,
                               __FILE__, __LINE__);
                return -1;
            }
        }

        if (read_bytes(product_file->raw_product, offset + 16, 8, &csize) < 0)
        {
            return -1;
        }
#ifndef WORDS_BIGENDIAN
        swap_int64(&csize);
#endif
        offset += 24;

        if (csize < 20)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "Invalid compressed data block for CDF variable");
            return -1;
        }
        buffer = malloc((size_t)csize);
        if (buffer == NULL)
        {
            coda_set_error(CODA_ERROR_OUT_OF_MEMORY, "out of memory (could not allocate %lu bytes) (%s:%u)",
                           (long)csize, __FILE__, __LINE__);
            return -1;
        }
        if (read_bytes(product_file->raw_product, offset, csize, buffer) < 0)
        {
            free(buffer);
            return -1;
        }
        zs.next_in = Z_NULL;
        zs.avail_in = 0;
        zs.zalloc = Z_NULL;
        zs.zfree = Z_NULL;
        zs.opaque = Z_NULL;
        zs.msg = NULL;
        /* windowBits is 15 + 16 (adding 16 means that gzip headers are parsed automatically) */
        if (inflateInit2(&zs, 31) != Z_OK)
        {
            coda_set_error(CODA_ERROR_FILE_READ, "could not intialize zip decompression");
            if (zs.msg != NULL)
            {
                coda_add_error_message(" (%s)", zs.msg);
            }
            free(buffer);
            return -1;
        }
        zs.next_in = buffer;
        zs.avail_in = (int)csize;
        if (last >= variable->num_records)
        {
            last = variable->num_records - 1;
            partial_read = 1;
        }
        zs.next_out = (Bytef *)&variable->data[first * variable->num_values_per_record * variable->value_size];
        zs.avail_out = (last - first + 1) * variable->num_values_per_record * variable->value_size;
        result = inflate(&zs, Z_FINISH);
        assert(result != Z_STREAM_ERROR);
        if (result < 0 && !(result == Z_BUF_ERROR && partial_read))
        {
            switch (result)
            {
                case Z_NEED_DICT:
                case Z_DATA_ERROR:
                    coda_set_error(CODA_ERROR_FILE_READ, "invalid or incomplete compressed data for CDF variable");
                    if (zs.msg != NULL)
                    {
                        coda_add_error_message(" (%s)", zs.msg);
                    }
                    break;
                case Z_MEM_ERROR:
                    coda_set_error(CODA_ERROR_OUT_OF_MEMORY, NULL);
                    break;
                default:
                    coda_set_error(CODA_ERROR_FILE_READ, "error during decompression of CDF variable");
                    if (zs.msg != NULL)
                    {
                        coda_add_error_message(" (%s)", zs.msg);
                    }
            }
            inflateEnd(&zs);
            free(buffer);
            return -1;
        }
        free(buffer);

        if (inflateEnd(&zs) != Z_OK)
        {
            coda_set_error(CODA_ERROR_FILE_READ, "zlib error");
            if (zs.msg != NULL)
            {
                coda_add_error_message(" (%s)", zs.msg);
            }
            return -1;
        }
        for (i = first; i <= last; i++)
        {
            variable->offset[i] = i * variable->num_values_per_record * variable->value_size;
        }
    }
    else
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for VVR record", record_type);
        return -1;
    }

    return 0;
}

static int read_VXR(coda_cdf_product *product_file, coda_cdf_variable *variable, int64_t offset, int32_t first,
                    int32_t last)
{
    int32_t record_type;
    int64_t vxr_next;
    int32_t n_entries;
    int32_t nused_entries;
    int i;

    if (offset == 0)
    {
        return 0;
    }

    if (read_bytes(product_file->raw_product, offset + 8, 4, &record_type) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&record_type);
#endif
    if (record_type != 6)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for VXR record", record_type);
        return -1;
    }

    if (read_bytes(product_file->raw_product, offset + 12, 8, &vxr_next) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 20, 4, &n_entries) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 24, 4, &nused_entries) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int64(&vxr_next);
    swap_int32(&n_entries);
    swap_int32(&nused_entries);
#endif
    offset += 28;
    if (vxr_next < 0 || vxr_next >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for VXR record");
        return -1;
    }
    if (n_entries < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of entries for VXR record");
        return -1;
    }

    for (i = 0; i < nused_entries; i++)
    {
        int32_t vr_first;
        int32_t vr_last;
        int64_t vr_offset;

        if (read_bytes(product_file->raw_product, offset + i * 4, 4, &vr_first) < 0)
        {
            return -1;
        }
        if (read_bytes(product_file->raw_product, offset + (i + n_entries) * 4, 4, &vr_last) < 0)
        {
            return -1;
        }
        if (read_bytes(product_file->raw_product, offset + (i + n_entries) * 8, 8, &vr_offset) < 0)
        {
            return -1;
        }
#ifndef WORDS_BIGENDIAN
        swap_int32(&vr_first);
        swap_int32(&vr_last);
        swap_int64(&vr_offset);
#endif
        if (vr_first < 0)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid first record number in VXR record");
            return -1;
        }
        if (vr_last < 0)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid last record number in VXR record");
            return -1;
        }
        if (vr_offset < 0 || vr_offset >= product_file->file_size)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for VR record");
            return -1;
        }
        if (read_VR(product_file, variable, vr_offset, vr_first, vr_last) != 0)
        {
            return -1;
        }
    }

    if (read_VXR(product_file, variable, vxr_next, first, last) != 0)
    {
        return -1;
    }

    return 0;
}

static int read_CPR(coda_cdf_product *product_file, int64_t offset)
{
    int32_t record_type;
    int32_t ctype;

    if (offset == 0)
    {
        return 0;
    }

    if (read_bytes(product_file->raw_product, offset + 8, 4, &record_type) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&record_type);
#endif
    if (record_type != 11)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for CPR record", record_type);
        return -1;
    }

    if (read_bytes(product_file->raw_product, offset + 12, 4, &ctype) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&ctype);
#endif
    if (ctype != 5)
    {
        coda_set_error(CODA_ERROR_UNSUPPORTED_PRODUCT, "Unsupported compression method (%d) for CDF variable", ctype);
        return -1;
    }

    return 0;
}

static int read_VDR(coda_cdf_product *product_file, int64_t offset, int is_zvar)
{
    coda_dynamic_type *variable_type;
    coda_cdf_variable *variable;
    int32_t record_type;
    int64_t vdr_next;
    int32_t data_type;
    int32_t max_rec;
    int64_t vxr_head;
    int64_t vxr_tail;
    int32_t flags;
    int32_t srecords;
    int32_t num_elems;
    int32_t num;
    int64_t cpr_spr_offset;
    int32_t blocking_factor;
    char name[257];
    int32_t num_dims;
    int32_t zdim_sizes[CODA_MAX_NUM_DIMS];
    int32_t dim_varys[CODA_MAX_NUM_DIMS];
    int record_varys;
    int has_compression;
    int i;

    if (offset == 0)
    {
        return 0;
    }

    if (read_bytes(product_file->raw_product, offset + 8, 4, &record_type) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&record_type);
#endif
    if (is_zvar)
    {
        if (record_type != 8)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for zVDR record", record_type);
            return -1;
        }
    }
    else
    {
        if (record_type != 3)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for rVDR record", record_type);
            return -1;
        }
    }

    if (read_bytes(product_file->raw_product, offset + 12, 8, &vdr_next) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 20, 4, &data_type) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 24, 4, &max_rec) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 28, 8, &vxr_head) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 36, 8, &vxr_tail) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 44, 4, &flags) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 48, 4, &srecords) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 64, 4, &num_elems) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 68, 4, &num) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 72, 8, &cpr_spr_offset) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 80, 4, &blocking_factor) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 84, 256, name) < 0)
    {
        return -1;
    }
    name[256] = '\0';
    rtrim(name);
#ifndef WORDS_BIGENDIAN
    swap_int64(&vdr_next);
    swap_int32(&data_type);
    swap_int32(&max_rec);
    swap_int64(&vxr_head);
    swap_int64(&vxr_tail);
    swap_int32(&flags);
    swap_int32(&srecords);
    swap_int32(&num_elems);
    swap_int32(&num);
    swap_int64(&cpr_spr_offset);
    swap_int32(&blocking_factor);
#endif
    if (vdr_next < 0 || vdr_next >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for VDR record");
        return -1;
    }
    if (vxr_head < 0 || vxr_head >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for VXR record");
        return -1;
    }
    if (vxr_tail < 0 || vxr_tail >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for VXR record");
        return -1;
    }
    if (num_elems < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of elements in VDR record");
        return -1;
    }
    if (num < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid variable number in VDR record");
        return -1;
    }
    if (cpr_spr_offset < -1 || cpr_spr_offset >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for CPR/SPR record");
        return -1;
    }

    if (data_type == 32)
    {
        coda_set_error(CODA_ERROR_UNSUPPORTED_PRODUCT, "CDF EPOCH16 data type is not supported");
        return -1;
    }
    if (is_zvar)
    {
        if (read_bytes(product_file->raw_product, offset + 340, 4, &num_dims) < 0)
        {
            return -1;
        }
#ifndef WORDS_BIGENDIAN
        swap_int32(&num_dims);
#endif
        if (num_dims < 0)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "CDF variable '%s' has invalid number of dimensions (%d)", name,
                           num_dims);
            return -1;
        }
        if (num_dims > CODA_MAX_NUM_DIMS)
        {
            coda_set_error(CODA_ERROR_PRODUCT, "CDF variable '%s' has too many dimensions (%d)", name, num_dims);
            return -1;
        }
        offset += 344;
        if (num_dims > 0)
        {
            for (i = 0; i < num_dims; i++)
            {
                if (read_bytes(product_file->raw_product, offset + i * 4, 4, &zdim_sizes[i]) < 0)
                {
                    return -1;
                }
#ifndef WORDS_BIGENDIAN
                swap_int32(&zdim_sizes[i]);
#endif
            }
            offset += num_dims * 4;
        }
    }
    else
    {
        num_dims = product_file->rnum_dims;
    }

    if (num_dims > 0)
    {
        for (i = 0; i < num_dims; i++)
        {
            if (read_bytes(product_file->raw_product, offset + i * 4, 4, &dim_varys[i]) < 0)
            {
                return -1;
            }
#ifndef WORDS_BIGENDIAN
            swap_int32(&dim_varys[i]);
#endif
        }
        offset += num_dims * 4;
    }
    record_varys = flags & 1;
    /* int has_pad_value = flags & 2; */
    has_compression = flags & 4;
    /* int64_t pad_value_offset = offset; */
    if (!record_varys && max_rec != 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF variable '%s' has non-varying record dimension but number of records "
                       "(%d) is not equal to 1", name, max_rec + 1);
        return -1;
    }

    if (has_compression && cpr_spr_offset != -1)
    {
        if (read_CPR(product_file, cpr_spr_offset) != 0)
        {
            return -1;
        }
    }
    if (product_file->root_type->num_fields != num)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF variable has invalid number '%d', expected '%ld'", num,
                       product_file->root_type->num_fields);
        return -1;
    }

    if (is_zvar)
    {
        variable_type = coda_cdf_variable_new(data_type, max_rec, record_varys, num_dims, zdim_sizes, dim_varys,
                                              product_file->array_ordering, num_elems, srecords, &variable);
    }
    else
    {
        variable_type = coda_cdf_variable_new(data_type, max_rec, record_varys, num_dims, product_file->rdim_sizes,
                                              dim_varys, product_file->array_ordering, num_elems, srecords, &variable);
    }
    if (variable_type == NULL)
    {
        return -1;
    }
    if (coda_mem_record_add_field(product_file->root_type, name, variable_type, 1) != 0)
    {
        coda_cdf_type_delete((coda_dynamic_type *)variable_type);
        return -1;
    }

    if (read_VXR(product_file, variable, vxr_head, 0, -1) != 0)
    {
        return -1;
    }

    if (read_VDR(product_file, vdr_next, is_zvar) != 0)
    {
        return -1;
    }

    return 0;
}

static int read_GDR(coda_cdf_product *product_file, int64_t offset)
{
    int32_t record_type;
    int64_t rvdr_head;
    int64_t zvdr_head;
    int64_t adr_head;
    int64_t eof;
    int32_t nr_vars;
    int32_t num_attr;
    int32_t nz_vars;

    if (read_bytes(product_file->raw_product, offset + 8, 4, &record_type) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&record_type);
#endif
    if (record_type != 2)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for GDR record", record_type);
        return -1;
    }

    if (read_bytes(product_file->raw_product, offset + 12, 8, &rvdr_head) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 20, 8, &zvdr_head) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 28, 8, &adr_head) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 36, 8, &eof) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 44, 4, &nr_vars) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 48, 4, &num_attr) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 56, 4, &product_file->rnum_dims) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, offset + 60, 4, &nz_vars) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int64(&rvdr_head);
    swap_int64(&zvdr_head);
    swap_int64(&adr_head);
    swap_int64(&eof);
    swap_int32(&nr_vars);
    swap_int32(&num_attr);
    swap_int32(&product_file->rnum_dims);
    swap_int32(&nz_vars);
#endif
    if (rvdr_head < 0 || rvdr_head >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for rVDR record");
        return -1;
    }
    if (zvdr_head < 0 || zvdr_head >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for zVDR record");
        return -1;
    }
    if (adr_head < 0 || adr_head >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for ADR record");
        return -1;
    }
    if (nr_vars < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of variables in GDR record");
        return -1;
    }
    if (num_attr < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of attributes in GDR record");
        return -1;
    }
    if (product_file->rnum_dims < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of dimensions for rVariables in GDR record");
        return -1;
    }
    if (nz_vars < 0)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid number of zVariables in GDR record");
        return -1;
    }

    if (eof != product_file->file_size)
    {
        char s1[21];
        char s2[21];

        coda_str64(eof, s1);
        coda_str64(product_file->file_size, s2);
        coda_set_error(CODA_ERROR_PRODUCT, "CDF end of file position (%s) does not match file size (%s)", s1, s2);
        return -1;
    }

    if (rvdr_head != 0)
    {
        if (read_VDR(product_file, rvdr_head, 0) != 0)
        {
            return -1;
        }
    }

    if (zvdr_head != 0)
    {
        if (read_VDR(product_file, zvdr_head, 1) != 0)
        {
            return -1;
        }
    }

    if (read_ADR(product_file, adr_head) != 0)
    {
        return -1;
    }

    return 0;
}

static int read_file(coda_cdf_product *product_file)
{
    int32_t record_type;
    int64_t gdr_offset;
    int32_t encoding;
    int32_t flags;

    /* CDF Descriptor Record */
    if (read_bytes(product_file->raw_product, 16, 4, &record_type) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int32(&record_type);
#endif
    if (record_type != 1)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid record type (%d) for CDR record", record_type);
        return -1;
    }

    if (read_bytes(product_file->raw_product, 20, 8, &gdr_offset) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, 28, 4, &product_file->cdf_version) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, 32, 4, &product_file->cdf_release) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, 36, 4, &encoding) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, 40, 4, &flags) < 0)
    {
        return -1;
    }
    if (read_bytes(product_file->raw_product, 52, 4, &product_file->cdf_increment) < 0)
    {
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_int64(&gdr_offset);
    swap_int32(&product_file->cdf_version);
    swap_int32(&product_file->cdf_release);
    swap_int32(&encoding);
    swap_int32(&flags);
    swap_int32(&product_file->cdf_increment);
#endif
    if (gdr_offset < 0 || gdr_offset >= product_file->file_size)
    {
        coda_set_error(CODA_ERROR_PRODUCT, "CDF file has invalid offset for GDR record");
        return -1;
    }

    switch (encoding)
    {
        case 1:        /* NETWORK */
        case 2:        /* SUN */
        case 5:        /* SGi */
        case 7:        /* IBMRS */
        case 9:        /* MAC */
        case 11:       /* HP */
        case 12:       /* NeXT */
            product_file->endianness = coda_big_endian;
            break;
        case 4:        /* DECSTATION */
        case 6:        /* IBMPC */
        case 13:       /* ALPHAOSF1 */
        case 16:       /* ALPHAVMSi */
            product_file->endianness = coda_little_endian;
            break;
        case 3:        /* VAX */
        case 14:       /* ALPHAVMSd */
        case 15:       /* ALPHAVMSg */
            coda_set_error(CODA_ERROR_UNSUPPORTED_PRODUCT, "CDF file contains unsupported floating point format "
                           "(only IEEE 754 floating point format is supported)");
            return -1;
        default:
            coda_set_error(CODA_ERROR_UNSUPPORTED_PRODUCT, "CDF file has unsupported encoding %d", encoding);
            return -1;
    }
    if (flags & 1)
    {
        /* row-major ordering */
        product_file->array_ordering = coda_array_ordering_c;
    }
    else
    {
        /* column-major ordering */
        product_file->array_ordering = coda_array_ordering_fortran;
    }
    if (!(flags & 2))
    {
        coda_set_error(CODA_ERROR_UNSUPPORTED_PRODUCT, "multi-file CDF is not supported");
        return -1;
    }
    product_file->has_md5_chksum = ((flags & 4) && (flags & 8));

    /* Global Descriptor Record */
    if (read_GDR(product_file, gdr_offset) != 0)
    {
        return -1;
    }

    return 0;
}

int coda_cdf_reopen(coda_product **product)
{
    coda_cdf_product *product_file;
    coda_type_record *root_definition;
    uint32_t magic[2];

    product_file = (coda_cdf_product *)malloc(sizeof(coda_cdf_product));
    if (product_file == NULL)
    {
        coda_set_error(CODA_ERROR_OUT_OF_MEMORY, "out of memory (could not allocate %lu bytes) (%s:%u)",
                       sizeof(coda_cdf_product), __FILE__, __LINE__);
        coda_close(*product);
        return -1;
    }
    product_file->filename = NULL;
    product_file->file_size = (*product)->file_size;
    product_file->format = coda_format_cdf;
    product_file->root_type = NULL;
    product_file->product_definition = NULL;
    product_file->product_variable_size = NULL;
    product_file->product_variable = NULL;
    product_file->mem_size = 0;
    product_file->mem_ptr = NULL;

    product_file->raw_product = *product;

    product_file->filename = strdup((*product)->filename);
    if (product_file->filename == NULL)
    {
        coda_set_error(CODA_ERROR_OUT_OF_MEMORY, "out of memory (could not duplicate filename string) (%s:%u)",
                       __FILE__, __LINE__);
        coda_cdf_close((coda_product *)product_file);
        return -1;
    }

    /* magic */
    if (read_bytes(product_file->raw_product, 0, 8, magic) < 0)
    {
        coda_cdf_close((coda_product *)product_file);
        return -1;
    }
#ifndef WORDS_BIGENDIAN
    swap_uint32(&magic[0]);
    swap_uint32(&magic[1]);
#endif
    if (magic[0] == 0x0000FFFF || magic[0] == 0xCDF26002)
    {
        coda_set_error(CODA_ERROR_UNSUPPORTED_PRODUCT, "CDF format version older than 3.0 is not supported");
        coda_cdf_close((coda_product *)product_file);
        return -1;
    }
    if (magic[1] == 0xCCCC0001)
    {
        coda_set_error(CODA_ERROR_UNSUPPORTED_PRODUCT, "full file compression not supported for CDF files");
        coda_cdf_close((coda_product *)product_file);
        return -1;
    }
    assert(magic[0] == 0xCDF30001 && magic[1] == 0x0000FFFF);

    /* create root type */
    root_definition = coda_type_record_new(coda_format_cdf);
    if (root_definition == NULL)
    {
        coda_cdf_close((coda_product *)product_file);
        return -1;
    }
    product_file->root_type = coda_mem_record_new(root_definition, NULL);
    if (product_file->root_type == NULL)
    {
        coda_cdf_close((coda_product *)product_file);
        coda_type_release((coda_type *)root_definition);
        return -1;
    }
    coda_type_release((coda_type *)root_definition);

    if (read_file(product_file) != 0)
    {
        coda_cdf_close((coda_product *)product_file);
        return -1;
    }

    *product = (coda_product *)product_file;

    return 0;
}

int coda_cdf_close(coda_product *product)
{
    coda_cdf_product *product_file = (coda_cdf_product *)product;

    if (product_file->filename != NULL)
    {
        free(product_file->filename);
    }
    if (product_file->root_type != NULL)
    {
        coda_dynamic_type_delete((coda_dynamic_type *)product_file->root_type);
    }
    if (product_file->mem_ptr != NULL)
    {
        free(product_file->mem_ptr);
    }
    if (product_file->raw_product != NULL)
    {
        coda_bin_close((coda_product *)product_file->raw_product);
    }

    free(product_file);

    return 0;
}
