/*
 *  $Id: opengps.c 29611 2026-03-17 10:31:06Z yeti-dn $
 *  Copyright (C) 2014-2025 David Necas (Yeti).
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/* NB: There is a draft of ISO 25178 Part 72, which may replace 5436-2
 * eventually.  The identifiers may change then. */

/**
 * [FILE-MAGIC-USERGUIDE]
 * OpenGPS X3P (ISO 5436-2)
 * .x3p
 * Read
 **/

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-iso5436-2-spm">
 *   <comment>ISO 5436-2 OpenGPS X3P data</comment>
 *   <glob pattern="*.x3p"/>
 *   <glob pattern="*.X3P"/>
 * </mime-type>
 **/

#include "config.h"
#include <glib/gi18n-lib.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <gwy.h>

#include "err.h"
#include "gwyzip.h"

#ifdef HAVE_MEMRCHR
#define strlenrchr(s,c,len) (gchar*)memrchr((s),(c),(len))
#else
#define strlenrchr(s,c,len) strrchr((s),(c))
#endif

#ifndef NAN
#define NAN (0.0/0.0)
#endif

#define MAGIC "PK\x03\x04"
#define MAGIC_SIZE (sizeof(MAGIC)-1)
#define MAGIC1 "main.xml"
#define MAGIC1_SIZE (sizeof(MAGIC1)-1)
#define MAGIC2 "bindata/"
#define MAGIC2_SIZE (sizeof(MAGIC2)-1)
#define BLOODY_UTF8_BOM "\xef\xbb\xbf"
#define EXTENSION ".x3p"

#define MAT_DIM_PREFIX "/ISO5436_2/Record3/MatrixDimension"
#define AXES_PREFIX "/ISO5436_2/Record1/Axes"
#define DATA_LINK_PREFIX "/ISO5436_2/Record3/DataLink"

typedef enum {
    X3P_FEATURE_SUR,
    X3P_FEATURE_PRF,
    X3P_FEATURE_PCL,
} X3PFeatureType;

typedef struct {
    const gchar *filename;
    GHashTable *hash;
    GString *path;
    X3PFeatureType feature_type;
    gboolean seen_datum;
    guint xres;
    guint yres;
    guint zres;
    guint ndata;
    guint datapos;
    gdouble dx;
    gdouble dy;
    gdouble dz;
    gdouble xoff;
    gdouble yoff;
    gdouble zoff;
    gdouble *values;
    gboolean *valid;
} X3PFile;

static gboolean      module_register (void);
static gint          detect_file     (const GwyFileDetectInfo *fileinfo,
                                      gboolean only_name);
static GwyFile*      load_file       (const gchar *filename,
                                      GwyRunModeFlags mode,
                                      GError **error);
static void          create_images   (const X3PFile *x3pfile,
                                      GwyFile *file);
static void          create_profiles (const X3PFile *x3pfile,
                                      GwyFile *file);
static gboolean      parse_main      (GwyZipFile zipfile,
                                      X3PFile *x3pfile,
                                      GError **error);
static gboolean      data_start      (X3PFile *x3pfile,
                                      GError **error);
static gboolean      read_binary_data(X3PFile *x3pfile,
                                      GwyZipFile zipfile,
                                      GError **error);
static GwyDict* get_meta        (const X3PFile *x3pfile);
static void          add_meta_record (gpointer hkey,
                                      gpointer hvalue,
                                      gpointer user_data);
static void          free_x3p_file   (X3PFile *x3pfile);
static gboolean      get_data_type   (const gchar *type,
                                      GwyRawDataType *rawtype,
                                      GError **error);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Reads ISO 5436-2 OpenGPS .x3p files."),
    "Yeti <yeti@gwyddion.net>",
    "2.0",
    "David Nečas (Yeti)",
    "2014",
};

GWY_MODULE_QUERY(module_info)

static gboolean
module_register(void)
{
    gwy_file_func_register("opengps",
                           N_("ISO 5436-2 OpenGPS data (.x3p)"),
                           detect_file, load_file, NULL, NULL);

    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    GwyZipFile zipfile;
    guchar *content;
    gint score = 0;

    if (only_name)
        return g_str_has_suffix(fileinfo->name_lowercase, EXTENSION) ? 15 : 0;

    /* Generic ZIP file. */
    if (fileinfo->file_size < MAGIC_SIZE || memcmp(fileinfo->head, MAGIC, MAGIC_SIZE) != 0)
        return 0;

    /* It contains main.xml and maybe directory bindata.  One of them should be
     * somewehre near the begining of the file. */
    if (!gwy_memmem(fileinfo->head, fileinfo->buffer_len, MAGIC1, MAGIC1_SIZE)
        && !gwy_memmem(fileinfo->tail, fileinfo->buffer_len, MAGIC1, MAGIC1_SIZE)
        && !gwy_memmem(fileinfo->head, fileinfo->buffer_len, MAGIC2, MAGIC2_SIZE)
        && !gwy_memmem(fileinfo->tail, fileinfo->buffer_len, MAGIC2, MAGIC2_SIZE))
        return 0;

    /* We have to realy look inside.  And since main.xml is a popular name for the main XML document within such
     * files, we also have to see if we find "ISO5436_2" somewehre near the begining of the file. */
    if ((zipfile = gwyzip_open(fileinfo->name, NULL))) {
        if (gwyzip_locate_file(zipfile, "main.xml", 1, NULL)
            && (content = gwyzip_get_file_content(zipfile, NULL, NULL))) {
            if (g_strstr_len(content, 4096, "ISO5436_2"))
                score = 100;
            g_free(content);
        }
        gwyzip_close(zipfile);
    }

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    X3PFile x3pfile;
    GwyZipFile zipfile;

    if (!(zipfile = gwyzip_open(filename, NULL)))
        return NULL;

    gwy_clear1(x3pfile);
    x3pfile.filename = filename;
    if (!parse_main(zipfile, &x3pfile, error))
        goto fail;

    if (!x3pfile.ndata) {
        err_NO_DATA(error);
        goto fail;
    }

    if (!x3pfile.datapos) {
        if (!read_binary_data(&x3pfile, zipfile, error))
            goto fail;
    }

    file = gwy_file_new_in_construction();
    if (x3pfile.feature_type == X3P_FEATURE_SUR)
        create_images(&x3pfile, file);
    else if (x3pfile.feature_type == X3P_FEATURE_PRF)
        create_profiles(&x3pfile, file);
    else {
        g_assert_not_reached();
    }

fail:
    gwy_debug("calling gwyzip_close()");
    gwyzip_close(zipfile);
    free_x3p_file(&x3pfile);

    return file;
}

static void
create_images(const X3PFile *x3pfile, GwyFile *file)
{
    gint id;

    for (id = 0; id < x3pfile->zres; id++) {
        guint n = x3pfile->xres*x3pfile->yres, k;
        const gboolean *valid = x3pfile->valid + id*n;
        GwyField *dfield = gwy_field_new(x3pfile->xres, x3pfile->yres,
                                         x3pfile->xres*x3pfile->dx, x3pfile->yres*x3pfile->dy,
                                         FALSE);
        gdouble *d = gwy_field_get_data(dfield);
        gwy_assign(d, x3pfile->values + id*n, n);
        for (k = 0; k < n; k++) {
            if (!valid[k])
                d[k] = NAN;
        }

        gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), "m");
        gwy_unit_set_from_string(gwy_field_get_unit_z(dfield), "m");
        gwy_file_set_image(file, id, dfield);
        gwy_image_title_fall_back(file, id);
        gwy_check_nonsquare_image(file, id);
        gwy_log_add_import(file, GWY_FILE_IMAGE, id, NULL, x3pfile->filename);

        GwyNield *mask;
        if ((mask = gwy_mask_nans_in_field(dfield, TRUE)))
            gwy_file_pass_image_mask(file, id, mask);
        g_object_unref(dfield);

        GwyDict *meta;
        if ((meta = get_meta(x3pfile)))
            gwy_file_pass_meta(file, GWY_FILE_IMAGE, id, meta);
    }
}

static void
create_profiles(const X3PFile *x3pfile,
                GwyFile *file)
{
    GwyGraphModel *gmodel;
    GwyUnit *siunitx, *siunity;
    GArray *validx, *validy;
    gint id;

    gmodel = gwy_graph_model_new();
    siunitx = gwy_unit_new("m");
    siunity = gwy_unit_new("m");
    g_object_set(gmodel,
                 "title", "Profiles",
                 "unit-x", siunitx,
                 "unit-y", siunity,
                 NULL);
    g_object_unref(siunity);
    g_object_unref(siunitx);

    validx = g_array_new(FALSE, FALSE, sizeof(gdouble));
    validy = g_array_new(FALSE, FALSE, sizeof(gdouble));
    for (id = 0; id < x3pfile->zres; id++) {
        guint n = x3pfile->xres;
        GwyGraphCurveModel *gcmodel;
        gchar *title;
        guint j;

        g_array_set_size(validx, 0);
        g_array_set_size(validy, 0);
        for (j = 0; j < x3pfile->xres; j++) {
            gdouble v = x3pfile->values[id*n + j];

            if (gwy_isnan(v) || gwy_isinf(v) || !x3pfile->valid[id*n + j])
                continue;

            g_array_append_val(validy, v);
            v = j*x3pfile->dx;
            g_array_append_val(validx, v);
        }

        if (!validx->len)
            continue;

        gcmodel = gwy_graph_curve_model_new();
        title = g_strdup_printf(_("Profile %u"), id+1);
        g_object_set(gcmodel,
                     "mode", GWY_GRAPH_CURVE_LINE,
                     "description", title,
                     "color", gwy_graph_get_preset_color(id),
                     NULL);
        g_free(title);
        gwy_graph_curve_model_set_data(gcmodel, (gdouble*)validx->data, (gdouble*)validy->data, validx->len);
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_unref(gcmodel);
    }

    g_array_free(validy, TRUE);
    g_array_free(validx, TRUE);

    gwy_file_pass_graph(file, 0, gmodel);
    gwy_log_add_import(file, GWY_FILE_GRAPH, 0, NULL, x3pfile->filename);
}

static const gchar*
remove_namespace(const gchar *element_name)
{
    const gchar *p;

    if ((p = strchr(element_name, ':')))
        return p+1;
    return element_name;
}

static void
start_element(G_GNUC_UNUSED GMarkupParseContext *context,
              const gchar *element_name,
              G_GNUC_UNUSED const gchar **attribute_names,
              G_GNUC_UNUSED const gchar **attribute_values,
              gpointer user_data,
              GError **error)
{
    X3PFile *x3pfile = (X3PFile*)user_data;
    gchar *path;

    element_name = remove_namespace(element_name);
    g_string_append_c(x3pfile->path, '/');
    g_string_append(x3pfile->path, element_name);
    path = x3pfile->path->str;
    gwy_debug("%s", path);

    if (gwy_strequal(path, "/ISO5436_2/Record3/DataLink") || gwy_strequal(path, "/ISO5436_2/Record3/DataList")) {
        if (!data_start(x3pfile, error))
            return;
    }

    if (gwy_strequal(path, "/ISO5436_2/Record3/DataList/Datum"))
        x3pfile->seen_datum = FALSE;
}

static void
end_element(G_GNUC_UNUSED GMarkupParseContext *context,
            const gchar *element_name,
            gpointer user_data,
            GError **error)
{
    X3PFile *x3pfile = (X3PFile*)user_data;
    guint n, len = x3pfile->path->len;
    gchar *path = x3pfile->path->str;

    element_name = remove_namespace(element_name);
    n = strlen(element_name);
    g_return_if_fail(g_str_has_suffix(path, element_name));
    g_return_if_fail(len > n);
    g_return_if_fail(path[len-1 - n] == '/');
    gwy_debug("%s", path);

    /* Invalid data points are represented by empty <Datum>. But then
     * text() is not called at all and we must handle that here. */
    if (gwy_strequal(path, "/ISO5436_2/Record3/DataList/Datum") && !x3pfile->seen_datum) {
        if (x3pfile->datapos >= x3pfile->ndata) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Too many DataList items for given matrix dimensions."));
            return;
        }
        x3pfile->values[x3pfile->datapos] = 0.0;
        x3pfile->valid[x3pfile->datapos] = FALSE;
        x3pfile->datapos++;
        gwy_debug("invalid Datum");
    }

    g_string_set_size(x3pfile->path, len-1 - n);
}

static void
text(G_GNUC_UNUSED GMarkupParseContext *context,
     const gchar *value,
     gsize value_len,
     gpointer user_data,
     G_GNUC_UNUSED GError **error)
{
    X3PFile *x3pfile = (X3PFile*)user_data;
    const gchar *semicolon;
    gchar *path = x3pfile->path->str;
    gchar *hvalue;

    if (!*value)
        return;

    /* Data represented directly in XML. */
    if (gwy_strequal(path, "/ISO5436_2/Record3/DataList/Datum")) {
        if (x3pfile->datapos >= x3pfile->ndata) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Too many DataList items for given matrix dimensions."));
            return;
        }
        /* There can be a ;-separated list.  And it can contain dummy values for the unused y-axis for profiles (poor
         * design).  Since we do not care about PCL, we only ever want the last value of the list, whatever it is. */
        if ((semicolon = strlenrchr(value, ';', value_len)))
            value = semicolon + 1;

        x3pfile->values[x3pfile->datapos] = x3pfile->dz*g_ascii_strtod(value, NULL) + x3pfile->zoff;
        x3pfile->valid[x3pfile->datapos] = TRUE;
        gwy_debug("valid Datum %g", x3pfile->values[x3pfile->datapos]);
        x3pfile->datapos++;
        x3pfile->seen_datum = TRUE;
        return;
    }

    hvalue = g_strdup(value);
    g_strstrip(hvalue);
    g_hash_table_replace(x3pfile->hash, g_strdup(path), hvalue);
}

static gboolean
parse_main(GwyZipFile zipfile,
           X3PFile *x3pfile,
           GError **error)
{
    GMarkupParser parser = {
        &start_element,
        &end_element,
        &text,
        NULL,
        NULL,
    };
    GMarkupParseContext *context = NULL;
    guchar *content = NULL, *s;
    gboolean ok = FALSE;
    GError *err = NULL;

    if (!gwyzip_locate_file(zipfile, "main.xml", 1, error)
        || !(content = gwyzip_get_file_content(zipfile, NULL, error)))
        return FALSE;

    gwy_strkill(content, "\r");
    s = content;
    /* Not seen in the wild but the XML people tend to use BOM in UTF-8... */
    if (g_str_has_prefix(s, BLOODY_UTF8_BOM))
        s += strlen(BLOODY_UTF8_BOM);

    if (!x3pfile->path)
        x3pfile->path = g_string_new(NULL);
    if (!x3pfile->hash)
        x3pfile->hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);

    context = g_markup_parse_context_new(&parser, 0, x3pfile, NULL);
    if (!g_markup_parse_context_parse(context, s, -1, &err) || !g_markup_parse_context_end_parse(context, &err))
        err_XML(error, &err);
    else
        ok = TRUE;

    if (context)
        g_markup_parse_context_free(context);
    g_free(content);

    return ok;
}

/* This is the main verification function that checks we have everything we need and the data are of a supported type.
 * */
static gboolean
data_start(X3PFile *x3pfile, GError **error)
{
    static const GwyEnum features[] = {
        { "SUR", X3P_FEATURE_SUR, },
        { "PRF", X3P_FEATURE_PRF, },
        { "PCL", X3P_FEATURE_PCL, },
    };

    gchar *s;

    if (x3pfile->values) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("File main.xml contains multiple data elements."));
        return FALSE;
    }

    /* First check axes to get meaningful error messages if their types are not as expected. */
    if (!require_keys(x3pfile->hash, error,
                      "/ISO5436_2/Record1/FeatureType",
                      AXES_PREFIX "/CX/AxisType",
                      AXES_PREFIX "/CY/AxisType",
                      AXES_PREFIX "/CZ/AxisType",
                      NULL))
        return FALSE;

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, "/ISO5436_2/Record1/FeatureType");
    x3pfile->feature_type = gwy_string_to_enum(s, features, G_N_ELEMENTS(features));
    if (x3pfile->feature_type == (X3PFeatureType)-1) {
        err_UNSUPPORTED(error, "/ISO5436_2/Record1/FeatureType");
        return FALSE;
    }

    if (x3pfile->feature_type != X3P_FEATURE_SUR && x3pfile->feature_type != X3P_FEATURE_PRF) {
        err_UNSUPPORTED(error, "/ISO5436_2/Record1/FeatureType");
        return FALSE;
    }

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CX/AxisType");
    if (!gwy_strequal(s, "I")) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    /* TRANSLATORS: type and axis are symbols such as I, CX, ...*/
                    _("Only type %s is supported for axis %s."),
                    "I", "CX");
        return FALSE;
    }

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CY/AxisType");
    if (x3pfile->feature_type != X3P_FEATURE_PRF && !gwy_strequal(s, "I")) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Only type %s is supported for axis %s."),
                    "I", "CY");
        return FALSE;
    }

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CZ/AxisType");
    if (!gwy_strequal(s, "A")) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Only type %s is supported for axis %s."),
                    "A", "CZ");
        return FALSE;
    }

    /* Then check sizes, offsets and steps when we know the grid is regular. */
    if (!require_keys(x3pfile->hash, error,
                      AXES_PREFIX "/CX/Increment",
                      AXES_PREFIX "/CY/Increment",
                      AXES_PREFIX "/CX/Offset",
                      AXES_PREFIX "/CY/Offset",
                      MAT_DIM_PREFIX "/SizeX",
                      MAT_DIM_PREFIX "/SizeY",
                      MAT_DIM_PREFIX "/SizeZ",
                      NULL))
        return FALSE;

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, MAT_DIM_PREFIX "/SizeX");
    x3pfile->xres = atoi(s);

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, MAT_DIM_PREFIX "/SizeY");
    x3pfile->yres = atoi(s);

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, MAT_DIM_PREFIX "/SizeZ");
    x3pfile->zres = atoi(s);

    gwy_debug("xres=%u, yres=%u, zres=%u",
              x3pfile->xres, x3pfile->yres, x3pfile->zres);

    if (err_DIMENSION(error, x3pfile->xres)
        || err_DIMENSION(error, x3pfile->yres)
        || err_DIMENSION(error, x3pfile->zres))
        return FALSE;

    /* PRF feature types are sets of profiles N×1×M. */
    if (x3pfile->feature_type == X3P_FEATURE_PRF && x3pfile->yres != 1) {
        err_UNSUPPORTED(error, MAT_DIM_PREFIX "/SizeY");
        return FALSE;
    }

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CX/Increment");
    x3pfile->dx = g_ascii_strtod(s, NULL);
    sanitise_real_size(&x3pfile->dx, "x step");

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CY/Increment");
    x3pfile->dy = g_ascii_strtod(s, NULL);
    sanitise_real_size(&x3pfile->dy, "y step");

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CX/Offset");
    x3pfile->xoff = g_ascii_strtod(s, NULL);

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CY/Offset");
    x3pfile->yoff = g_ascii_strtod(s, NULL);

    /* Defaults that are good for floating point data conversion.  If a file with floating point data specifies
     * Increment and Offset, we apply them without hesitation.  The behaviour is probably undefined.  */
    x3pfile->dz = 1.0;
    x3pfile->zoff = 0.0;

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CZ/Increment");
    if (s)
        x3pfile->dz = g_ascii_strtod(s, NULL);

    s = (gchar*)g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CZ/Offset");
    if (s)
        x3pfile->zoff = g_ascii_strtod(s, NULL);

    gwy_debug("dz=%g, zoff=%g", x3pfile->dz, x3pfile->zoff);

    x3pfile->ndata = x3pfile->xres*x3pfile->yres*x3pfile->zres;
    x3pfile->values = g_new(gdouble, x3pfile->ndata);
    x3pfile->valid = g_new(gboolean, x3pfile->ndata);
    x3pfile->datapos = 0;

    return TRUE;
}

static gboolean
read_binary_data(X3PFile *x3pfile, GwyZipFile zipfile, GError **error)
{
    GwyRawDataType rawtype;
    gsize size;
    guchar *bindata;
    gchar *s;
    guint i;

    s = g_hash_table_lookup(x3pfile->hash, DATA_LINK_PREFIX "/PointDataLink");
    if (!s) {
        err_NO_DATA(error);
        return FALSE;
    }
    gwy_debug("binary data file %s", s);

    if (!gwyzip_locate_file(zipfile, s, 1, error))
        return FALSE;

    s = g_hash_table_lookup(x3pfile->hash, AXES_PREFIX "/CZ/DataType");
    if (!s) {
        err_MISSING_FIELD(error, AXES_PREFIX "CZ/DataType");
        return FALSE;
    }

    if (!get_data_type(s, &rawtype, error))
        return FALSE;

    if (!(bindata = gwyzip_get_file_content(zipfile, &size, error)))
        return FALSE;

    if (err_SIZE_MISMATCH(error, x3pfile->ndata * gwy_raw_data_size(rawtype), size, TRUE)) {
        g_free(bindata);
        return FALSE;
    }

    gwy_convert_raw_data(bindata, x3pfile->ndata, 1, rawtype, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                         x3pfile->values, x3pfile->dz, x3pfile->zoff);
    g_free(bindata);

    for (i = 0; i < x3pfile->ndata; i++)
        x3pfile->valid[i] = TRUE;

    s = g_hash_table_lookup(x3pfile->hash, DATA_LINK_PREFIX "/ValidPointsLink");
    if (!s)
        return TRUE;

    if (!gwyzip_locate_file(zipfile, s, 1, error) || !(bindata = gwyzip_get_file_content(zipfile, &size, error)))
        return FALSE;

    if (err_SIZE_MISMATCH(error, (x3pfile->ndata + 7)/8, size, TRUE)) {
        g_free(bindata);
        return FALSE;
    }

    for (i = 0; i < x3pfile->ndata; i++)
        x3pfile->valid[i] = bindata[i/8] & (1 << (i % 8));

    g_free(bindata);

    return TRUE;
}

static GwyDict*
get_meta(const X3PFile *x3pfile)
{
    GwyDict *meta = gwy_dict_new_in_construction();

    g_hash_table_foreach(x3pfile->hash, add_meta_record, meta);
    if (gwy_dict_get_n_items(meta))
        return meta;

    g_object_unref(meta);
    return NULL;
}

static void
add_meta_record(gpointer hkey, gpointer hvalue, gpointer user_data)
{
    const gchar *key = (const gchar*)hkey;
    const gchar *value = (const gchar*)hvalue;
    GwyDict *meta = (GwyDict*)user_data;

    if (!gwy_stramong(key,
                      "/ISO5436_2/Record1/Revision",
                      "/ISO5436_2/Record1/FeatureType",
                      "/ISO5436_2/Record2/Date",
                      "/ISO5436_2/Record2/Creator",
                      "/ISO5436_2/Record2/Instrument/Manufacturer",
                      "/ISO5436_2/Record2/Instrument/Model",
                      "/ISO5436_2/Record2/Instrument/Serial",
                      "/ISO5436_2/Record2/Instrument/Version",
                      "/ISO5436_2/Record2/CalibrationDate",
                      "/ISO5436_2/Record2/ProbingSystem/Type",
                      "/ISO5436_2/Record2/ProbingSystem/Identification",
                      "/ISO5436_2/Record2/Comment",
                      NULL)
        && !g_str_has_prefix(key, "/ISO5436_2/Record2/ProbingSystem/Identification/")
        && !g_str_has_prefix(key, "/ISO5436_2/Record1/Axes/Rotation"))
        return;

    key = strrchr(key, '/');
    g_return_if_fail(key);
    key++;
    gwy_dict_pass_string_by_name(meta, key, g_strdup(value));
}

static void
free_x3p_file(X3PFile *x3pfile)
{
    if (x3pfile->hash) {
        g_hash_table_destroy(x3pfile->hash);
        x3pfile->hash = NULL;
    }
    if (x3pfile->path) {
        g_string_free(x3pfile->path, TRUE);
        x3pfile->path = NULL;
    }
    g_free(x3pfile->values);
    x3pfile->values = NULL;
    g_free(x3pfile->valid);
    x3pfile->valid = NULL;
}

static gboolean
get_data_type(const gchar *type,
              GwyRawDataType *rawtype,
              GError **error)
{
    if (gwy_strequal(type, "I")) {
        *rawtype = GWY_RAW_DATA_SINT16;
        return TRUE;
    }
    if (gwy_strequal(type, "L")) {
        *rawtype = GWY_RAW_DATA_SINT32;
        return TRUE;
    }
    if (gwy_strequal(type, "F")) {
        *rawtype = GWY_RAW_DATA_FLOAT;
        return TRUE;
    }
    if (gwy_strequal(type, "D")) {
        *rawtype = GWY_RAW_DATA_DOUBLE;
        return TRUE;
    }

    err_UNSUPPORTED(error, AXES_PREFIX "/CZ/DataType");
    return FALSE;
}

/* vim: set cin et columns=120 tw=118 ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
