/*
 *  $Id: serialize.c 28433 2025-08-21 17:52:38Z yeti-dn $
 *  Copyright (C) 2009-2025 David Nečas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  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.
 */

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

#include "libgwyddion/gwyddion.h"
#include "libgwyddion/serializable-internal.h"

G_STATIC_ASSERT(GLIB_SIZEOF_SIZE_T == 8);
G_STATIC_ASSERT(sizeof(gdouble) == sizeof(guint64));
G_STATIC_ASSERT(sizeof(gfloat) == sizeof(guint32));
G_STATIC_ASSERT(G_BYTE_ORDER == G_LITTLE_ENDIAN || G_BYTE_ORDER == G_BIG_ENDIAN);

static GType                 get_serializable      (const gchar *typename,
                                                    gpointer *classref,
                                                    const GwySerializableInterface **iface,
                                                    GwyErrorList **error_list);
static GType                 get_serializable_boxed(const gchar *typename,
                                                    GwyErrorList **error_list);
static GwySerializableGroup* unpack_group          (const guchar *buffer,
                                                    gsize size,
                                                    GwySerializeSizeType sizetype,
                                                    gboolean exact_size,
                                                    GwyErrorList **error_list);

static inline gboolean
check_size(gsize size,
           gsize required_size,
           const gchar *what,
           GwyErrorList **error_list)
{
    if (G_LIKELY(size >= required_size))
        return TRUE;

    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_TRUNCATED,
                       // TRANSLATORS: Error message.
                       _("End of data was reached while reading value of type ‘%s’. It requires %" G_GSIZE_FORMAT " "
                         "bytes but only %" G_GSIZE_FORMAT " bytes remain."),
                       what, required_size, size);
    return FALSE;
}

static inline gsize
unpack_size(const guchar *buffer,
            gsize size,
            gsize item_size,
            gsize *array_size,
            const gchar *name,
            GwySerializeSizeType sizetype,
            GwyErrorList **error_list)
{
    gsize asize = 0, file_size_max = 0;

    if (sizetype == GWY_SERIALIZE_SIZE_64BIT) {
        guint64 raw_size;

        if (!check_size(size, sizeof(guint64), "data-size", error_list))
            return 0;
        memcpy(&raw_size, buffer, sizeof(guint64));
        asize = GUINT64_FROM_LE(raw_size);
        size -= sizeof(guint64);
        file_size_max = G_MAXUINT64;
    }
    else if (sizetype == GWY_SERIALIZE_SIZE_32BIT) {
        guint32 raw_size;

        if (!check_size(size, sizeof(guint32), "data-size", error_list))
            return 0;
        memcpy(&raw_size, buffer, sizeof(guint32));
        asize = GUINT32_FROM_LE(raw_size);
        size -= sizeof(guint32);
        file_size_max = G_MAXUINT32;
    }
    else {
        g_return_val_if_reached(0);
    }

    /* Check whether the array length is representable and whether the total size is representable. */
    *array_size = asize;
    if (G_UNLIKELY(*array_size != asize || *array_size >= file_size_max/item_size)) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_SIZE_T,
                           // TRANSLATORS: Error message.
                           _("Data of size larger than what can be represented on this machine was encountered."));
        return 0;
    }

    /* And if the arihtmetic works, whether the data fits. */
    if (!check_size(size, asize * item_size, name, error_list))
        return 0;

    return sizetype;
}

static inline gsize
unpack_boolean(const guchar *buffer,
               gsize size,
               gboolean *value,
               GwyErrorList **error_list)
{
    guint8 byte;

    if (!check_size(size, sizeof(guint8), "boolean", error_list))
        return 0;
    byte = *buffer;
    *value = !!byte;
    return sizeof(guint8);
}

static inline gsize
unpack_uint8(const guchar *buffer,
             gsize size,
             guint8 *value,
             GwyErrorList **error_list)
{
    if (!check_size(size, sizeof(guint8), "int8", error_list))
        return 0;
    *value = *buffer;
    return sizeof(guint8);
}

static inline gsize
unpack_uint8_array(const guchar *buffer,
                   gsize size,
                   gsize *array_size,
                   guint8 **value,
                   GwySerializeSizeType sizetype,
                   GwyErrorList **error_list)
{
    gsize rbytes;

    if (!(rbytes = unpack_size(buffer, size, sizeof(guint8), array_size, "int8-array", sizetype, error_list)))
        return 0;

    buffer += rbytes;
    if (*array_size) {
        *value = g_new(guint8, *array_size);
        memcpy(*value, buffer, *array_size*sizeof(guint8));
    }
    return rbytes + *array_size*sizeof(guint8);
}

static inline gsize
unpack_uint16(const guchar *buffer,
              gsize size,
              guint16 *value,
              GwyErrorList **error_list)
{
    if (!check_size(size, sizeof(guint16), "int16", error_list))
        return 0;
    memcpy(value, buffer, sizeof(guint16));
    *value = GINT16_FROM_LE(*value);
    return sizeof(guint16);
}

static inline gsize
unpack_uint16_array(const guchar *buffer,
                    gsize size,
                    gsize *array_size,
                    guint16 **value,
                    GwySerializeSizeType sizetype,
                    GwyErrorList **error_list)
{
    gsize rbytes;

    if (!(rbytes = unpack_size(buffer, size, sizeof(guint16), array_size, "int16-array", sizetype, error_list)))
        return 0;
    buffer += rbytes;

    if (*array_size) {
        *value = g_new(guint16, *array_size);
        memcpy(*value, buffer, *array_size*sizeof(guint16));
    }
    if (G_BYTE_ORDER == G_BIG_ENDIAN) {
        gsize i;

        for (i = 0; i < *array_size; i++) {
            /* The default swapping macros evaulate the value multiple times. */
            guint16 v = (*value)[i];
            (*value)[i] = GUINT16_SWAP_LE_BE(v);
        }
    }
    return rbytes + *array_size*sizeof(guint16);
}

static inline gsize
unpack_uint32(const guchar *buffer,
              gsize size,
              guint32 *value,
              GwyErrorList **error_list)
{
    if (!check_size(size, sizeof(guint32), "int32", error_list))
        return 0;
    memcpy(value, buffer, sizeof(guint32));
    *value = GINT32_FROM_LE(*value);
    return sizeof(guint32);
}

static inline gsize
unpack_uint32_array(const guchar *buffer,
                    gsize size,
                    gsize *array_size,
                    guint32 **value,
                    GwySerializeSizeType sizetype,
                    GwyErrorList **error_list)
{
    gsize rbytes;

    if (!(rbytes = unpack_size(buffer, size, sizeof(guint32), array_size, "int32-array", sizetype, error_list)))
        return 0;
    buffer += rbytes;

    if (*array_size) {
        *value = g_new(guint32, *array_size);
        memcpy(*value, buffer, *array_size*sizeof(guint32));
    }
    if (G_BYTE_ORDER == G_BIG_ENDIAN) {
        gsize i;

        for (i = 0; i < *array_size; i++) {
            /* The default swapping macros evaulate the value multiple times. */
            guint32 v = (*value)[i];
            (*value)[i] = GUINT32_SWAP_LE_BE(v);
        }
    }
    return rbytes + *array_size*sizeof(guint32);
}

static inline gsize
unpack_uint64(const guchar *buffer,
              gsize size,
              guint64 *value,
              GwyErrorList **error_list)
{
    if (!check_size(size, sizeof(guint64), "int64", error_list))
        return 0;
    memcpy(value, buffer, sizeof(guint64));
    *value = GINT64_FROM_LE(*value);
    return sizeof(guint64);
}

static inline gsize
unpack_uint64_array(const guchar *buffer,
                    gsize size,
                    gsize *array_size,
                    guint64 **value,
                    GwySerializeSizeType sizetype,
                    GwyErrorList **error_list)
{
    gsize rbytes;

    if (!(rbytes = unpack_size(buffer, size, sizeof(guint64), array_size, "int64-array", sizetype, error_list)))
        return 0;
    buffer += rbytes;

    if (*array_size) {
        *value = g_new(guint64, *array_size);
        memcpy(*value, buffer, *array_size*sizeof(guint64));
    }
    if (G_BYTE_ORDER == G_BIG_ENDIAN) {
        gsize i;

        for (i = 0; i < *array_size; i++) {
            /* The default swapping macros evaulate the value multiple times. */
            guint64 v = (*value)[i];
            (*value)[i] = GUINT64_SWAP_LE_BE(v);
        }
    }
    return rbytes + *array_size*sizeof(guint64);
}

/* The code says `int64', that is correct we only care about the size. */
static inline gsize
unpack_double(const guchar *buffer,
              gsize size,
              guint64 *value,
              GwyErrorList **error_list)
{
    if (!check_size(size, sizeof(gdouble), "double", error_list))
        return 0;
    memcpy(value, buffer, sizeof(gdouble));
    *value = GINT64_FROM_LE(*value);
    return sizeof(gdouble);
}

/* The code says `int64', that is correct we only care about the size. */
static inline gsize
unpack_double_array(const guchar *buffer,
                    gsize size,
                    gsize *array_size,
                    guint64 **value,
                    GwySerializeSizeType sizetype,
                    GwyErrorList **error_list)
{
    gsize rbytes;

    if (!(rbytes = unpack_size(buffer, size, sizeof(gdouble), array_size, "double-array", sizetype, error_list)))
        return 0;
    buffer += rbytes;

    if (*array_size) {
        *value = g_new(guint64, *array_size);
        memcpy(*value, buffer, *array_size*sizeof(gdouble));
    }
    if (G_BYTE_ORDER == G_BIG_ENDIAN) {
        gsize i;

        for (i = 0; i < *array_size; i++) {
            /* The default swapping macros evaulate the value multiple times. */
            guint64 v = (*value)[i];
            (*value)[i] = GUINT64_SWAP_LE_BE(v);
        }
    }
    return rbytes + *array_size*sizeof(gdouble);
}

/* The code says `int32', that is correct we only care about the size. */
static inline gsize
unpack_float(const guchar *buffer,
             gsize size,
             guint32 *value,
             GwyErrorList **error_list)
{
    if (!check_size(size, sizeof(gfloat), "float", error_list))
        return 0;
    memcpy(value, buffer, sizeof(gfloat));
    *value = GINT64_FROM_LE(*value);
    return sizeof(gfloat);
}

/* The code says `int32', that is correct we only care about the size. */
static inline gsize
unpack_float_array(const guchar *buffer,
                   gsize size,
                   gsize *array_size,
                   guint32 **value,
                   GwySerializeSizeType sizetype,
                   GwyErrorList **error_list)
{
    gsize rbytes;

    if (!(rbytes = unpack_size(buffer, size, sizeof(gfloat), array_size, "float-array", sizetype, error_list)))
        return 0;
    buffer += rbytes;

    if (*array_size) {
        *value = g_new(guint32, *array_size);
        memcpy(*value, buffer, *array_size*sizeof(gfloat));
    }
    if (G_BYTE_ORDER == G_BIG_ENDIAN) {
        gsize i;

        for (i = 0; i < *array_size; i++) {
            /* The default swapping macros evaulate the value multiple times. */
            guint32 v = (*value)[i];
            (*value)[i] = GUINT32_SWAP_LE_BE(v);
        }
    }
    return rbytes + *array_size*sizeof(gfloat);
}

static inline gsize
unpack_name(const guchar *buffer,
            gsize size,
            const gchar **value,
            GwyErrorList **error_list)
{
    const guchar *s = memchr(buffer, '\0', size);

    if (G_UNLIKELY(!s)) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_TRUNCATED,
                           // TRANSLATORS: Error message.
                           _("End of data was reached while looking for the end of a string."));
        return 0;
    }
    const gchar *end;
    if (G_UNLIKELY(!g_utf8_validate((const gchar*)buffer, s - buffer, &end))) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_UTF8,
                           // TRANSLATORS: Error message.
                           _("String is not valid UTF-8."));
        return 0;
    }
    *value = (const gchar*)buffer;
    return s-buffer + 1;
}

static inline gsize
unpack_string(const guchar *buffer,
              gsize size,
              gchar **value,
              GwyErrorList **error_list)
{
    const guchar *s = memchr(buffer, '\0', size);

    if (G_UNLIKELY(!s)) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_TRUNCATED,
                           // TRANSLATORS: Error message.
                           _("End of data was reached while looking for the end of a string."));
        return 0;
    }
    const gchar *end;
    if (G_UNLIKELY(!g_utf8_validate((const gchar*)buffer, s - buffer, &end))) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_UTF8,
                           // TRANSLATORS: Error message.
                           _("String is not valid UTF-8."));
        return 0;
    }
    *value = g_memdup(buffer, s-buffer + 1);
    return s-buffer + 1;
}

static inline gsize
unpack_string_array(const guchar *buffer,
                    gsize size,
                    gsize *parray_size,
                    gchar ***value,
                    GwySerializeSizeType sizetype,
                    GwyErrorList **error_list)
{
    gsize rbytes, position = 0;

    if (!(rbytes = unpack_size(buffer, size, 1, parray_size, "string-array", sizetype, error_list)))
        return 0;
    position += rbytes;

    gsize array_size = *parray_size;
    if (!array_size)
        return position;

    gchar **strings = g_new0(gchar*, array_size);
    for (gsize i = 0; i < array_size; i++) {
        if (!(rbytes = unpack_string(buffer + position, size - position, strings + i, error_list))) {
            while (i--)
                g_free(strings[i]);
            GWY_FREE(strings);
            *parray_size = 0;
            position = 0;
            break;
        }
        position += rbytes;
    }
    *value = strings;

    return position;
}

static inline gsize
unpack_group_array(const guchar *buffer,
                   gsize size,
                   gsize *parray_size,
                   GwySerializableGroup ***value,
                   GwySerializeSizeType sizetype,
                   GwyErrorList **error_list)
{
    gsize rbytes, position = 0;

    /* The minimum object size is 2 bytes the name and sizetype bytes the size. */
    if (!(rbytes = unpack_size(buffer, size, 2 + sizetype, parray_size, "object-array", sizetype, error_list)))
        return 0;
    position += rbytes;

    gsize array_size = *parray_size;
    if (!array_size)
        return position;

    GwySerializableGroup **groups = g_new0(GwySerializableGroup*, array_size);
    for (gsize i = 0; i < array_size; i++) {
        if (!(groups[i] = unpack_group(buffer + position, size - position, sizetype, FALSE, error_list))) {
            while (i--)
                gwy_serializable_group_free(groups[i], TRUE, NULL);
            GWY_FREE(groups);
            position = 0;
            *parray_size = 0;
            break;
        }
        position += groups[i]->size_bytes;
    }
    *value = groups;

    return position;
}

/**
 * gwy_deserialize_memory:
 * @buffer: Memory containing the serialised representation of one object.
 * @size: Size of @buffer in bytes.  If the buffer is larger than the object representation, non-fatal
 *        %GWY_DESERIALIZE_ERROR_PADDING will be reported.
 * @bytes_consumed: (nullable): Location to store the number of bytes actually consumed from @buffer, or %NULL.
 * @error_list: (nullable): Location to store the errors occuring, %NULL to ignore.
 *
 * Deserialises an object in GWY format from plain memory buffer.
 *
 * The initial references of restored objects are according to their nature.  For instance, a #GInitiallyUnowned will
 * have a floating reference, a plain #GObject will have a reference count of 1, etc.
 *
 * Deserialization sidesteps the typical object creation somewhat. It needs to construct an object given its class
 * name, and for that the type has to be already registered in the GObject type system. Library initialisation
 * functions such as gwy_init() or gwy_app_init() normally take care of it. If you introduce your own serialisable
 * objects (or avoid the initialisation functions for some reason), force the registration of all relevant types for
 * instance by calling g_type_class_peak(MY_TYPE_WHATEVER).
 *
 * Errors from %GWY_DESERIALIZE_ERROR domain can always occur in @error_list. For most objects, these are the only
 * errors that can occur. However, special objects can report more specific non-fatal errors from their own domain.
 * For example, #GwyFile does.
 *
 * Returns: (transfer full) (allow-none):
 *          Newly created object on success, %NULL on failure.
 **/
GObject*
gwy_deserialize_memory(const guchar *buffer,
                       gsize size,
                       gsize *bytes_consumed,
                       GwyErrorList **error_list)
{
    gwy_init();
    GwySerializableGroup *group = gwy_deserialize_group_memory(buffer, size, GWY_SERIALIZE_SIZE_DEFAULT, error_list);

    if (bytes_consumed)
        *bytes_consumed = group ? group->size_bytes : 0;
    return group ? gwy_deserialize_construct(group, error_list) : NULL;
}

/**
 * gwy_deserialize_group_memory:
 * @buffer: Memory containing the serialised representation of one object.
 * @size: Size of @buffer in bytes.  If the buffer is larger than the object representation, non-fatal
 *        %GWY_DESERIALIZE_ERROR_PADDING will be reported.
 * @sizetype: Type of size representation.
 * @error_list: (nullable): Location to store the errors occuring, %NULL to ignore. Only errors from
 *                          %GWY_DESERIALIZE_ERROR domain can occur.
 *
 * Deserialises GWY format from plain memory buffer to itemised representation.
 *
 * This is a low-level function. Usually, you use a function such as gwy_deserialize_memory() to immediately construct
 * the object. For this function, the serialised data must be well-formed but are not required to represent actual
 * Gwyddion objects. This is only required when objects are constructed from the itemised representation – which can,
 * however, be used for other purposes if needed.
 *
 * The number of bytes consumed can be obtained by looking at the byte size of the returned group.
 *
 * Object and item names (and possibly other data as well) in the returned group can simply be pointers inside
 * @buffer. Therefore, @buffer must outlive the group.
 *
 * Returns: (transfer full) (allow-none):
 *          Newly created group corresponding to the top-level object, %NULL on failure.
 **/
GwySerializableGroup*
gwy_deserialize_group_memory(const guchar *buffer,
                             gsize size,
                             GwySerializeSizeType sizetype,
                             GwyErrorList **error_list)
{
    return unpack_group(buffer, size, sizetype, FALSE, error_list);
}

/**
 * gwy_deserialize_construct:
 * @group: (transfer full):
 *         Itemised object components. The contents of @group is consumed by the deserialisation and it is freed.
 * @error_list: (nullable): Location to store the errors occuring, %NULL to ignore. Errors from
 *              %GWY_DESERIALIZE_ERROR domain can occur.
 *
 * Creates an object from itemised representation.
 *
 * This is a low-level function. Usually, you use a function such as gwy_deserialize_memory() to immediately construct
 * the object.
 *
 * The initial references of restored objects are according to their nature.  For instance, a #GInitiallyUnowned will
 * have a floating reference, a plain #GObject will have a reference count of 1, etc.
 *
 * Returns: (transfer full) (allow-none):
 *          Newly created object on success, %NULL on failure.
 **/
GObject*
gwy_deserialize_construct(GwySerializableGroup *group,
                          GwyErrorList **error_list)
{
    g_return_val_if_fail(group, NULL);
    gwy_init();

    /* Check if we can deserialize such thing. */
    const GwySerializableInterface *iface;
    gpointer classref = NULL;
    GType type;
    if (!(type = get_serializable(group->name, &classref, &iface, error_list))) {
        gwy_serializable_group_free(group, TRUE, NULL);
        return NULL;
    }

    /* Construct any nested objects and boxed types. */
    gsize n = group->n;
    for (gsize i = 0; i < n; i++) {
        GwySerializableItem *item = group->items + i;
        GwySerializableCType ctype = item->ctype;

        if (!(item->flags & GWY_SERIALIZABLE_IS_GROUP))
            continue;

        if (ctype == GWY_SERIALIZABLE_OBJECT)
            item->value.v_object = gwy_deserialize_construct(item->value.v_group, error_list);
        else if (ctype == GWY_SERIALIZABLE_BOXED) {
            GwySerializableGroup *member_group = item->value.v_group;
            GType boxed_type = get_serializable_boxed(member_group->name, error_list);
            if (boxed_type) {
                item->aux.boxed_type = boxed_type;
                item->value.v_boxed = gwy_serializable_boxed_construct(boxed_type, member_group, error_list);
            }
            else
                item->value.v_boxed = NULL;

            gwy_serializable_group_free(member_group, TRUE, item->value.v_boxed ? error_list : NULL);
        }
        else if (ctype == GWY_SERIALIZABLE_OBJECT_ARRAY) {
            gsize array_size = item->array_size;
            for (gsize j = 0; j < array_size; j++) {
                /* These are actually the same array of pointers. We do not do anything untowards with them. But a
                 * compiler too smart fot its own good may see a misopportunity for aliasing optimisation here. */
                item->value.v_object_array[j] = gwy_deserialize_construct(item->value.v_group_array[j], error_list);
            }
        }
        else {
            g_critical("Non-object and non-boxed item %s has the is-group flag set.", item->name);
        }

        item->flags &= ~GWY_SERIALIZABLE_IS_GROUP;
    }

    /* Remove items correspoding to failed object and boxed members. */
    gsize ngood = 0;
    for (gsize i = 0; i < n; i++) {
        GwySerializableItem *item = group->items + i;
        GwySerializableCType ctype = item->ctype;
        gboolean is_good = TRUE;

        if (ctype == GWY_SERIALIZABLE_OBJECT)
            is_good = !!item->value.v_object;
        else if (ctype == GWY_SERIALIZABLE_BOXED)
            is_good = !!item->value.v_boxed;
        else if (ctype == GWY_SERIALIZABLE_OBJECT_ARRAY) {
            GObject **objects = item->value.v_object_array;
            if (objects) {
                gsize array_size = item->array_size;
                for (gsize j = 0; j < array_size && is_good; j++) {
                    if (!objects[j])
                        is_good = FALSE;
                }
                if (!is_good) {
                    for (gsize j = 0; j < array_size; j++)
                        g_clear_object(objects + j);
                    g_free(item->value.v_object_array);
                }
            }
        }
        if (is_good)
            group->items[ngood++] = group->items[i];
    }
    group->n = ngood;

    /* Finally, construct the object. */
    GObject *object = g_object_newv(type, 0, NULL);
    if (!iface->construct(GWY_SERIALIZABLE(object), group, error_list))
        g_clear_object(&object);

    g_clear_pointer(&classref, g_type_class_unref);
    gwy_serializable_group_free(group, TRUE, object ? error_list : NULL);

    return object;
}

/**
 * get_serializable:
 * @typename: Name of the type.
 * @classref: Location to store the reference to the object class on success.
 * @iface: Location to store the serialisable interface on success.
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 *
 * Finds and verifies the type of a serialisable object.
 *
 * Returns: The type corresponding to @typename on success, %NULL on failure.
 **/
static GType
get_serializable(const gchar *typename,
                 gpointer *classref,
                 const GwySerializableInterface **iface,
                 GwyErrorList **error_list)
{
    GType type = g_type_from_name(typename);

    if (G_UNLIKELY(!type)) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_OBJECT,
                           _("Object type ‘%s’ is not known. It was ignored."),
                           typename);
        return 0;
    }

    *classref = g_type_class_ref(type);
    g_assert(*classref);
    if (G_UNLIKELY(!G_TYPE_IS_INSTANTIATABLE(type))) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_OBJECT,
                           // TRANSLATORS: Error message.
                           // TRANSLATORS: %s is a data type name, e.g. GwyCurve, GwyGradient.
                           _("Object type ‘%s’ is not instantiable. It was ignored."),
                           typename);
        return 0;
    }
    if (G_UNLIKELY(!g_type_is_a(type, GWY_TYPE_SERIALIZABLE))) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_OBJECT,
                           _("Object type ‘%s’ is not serializable. It was ignored."),
                           typename);
        return 0;
    }

    *iface = g_type_interface_peek(*classref, GWY_TYPE_SERIALIZABLE);
    if (G_UNLIKELY(!*iface || !(*iface)->construct)) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_OBJECT,
                           // TRANSLATORS: Error message.
                           _("Object type ‘%s’ does not implement deserialization. It was ignored."),
                           typename);
        return 0;
    }

    return type;
}

/**
 * get_serializable_boxed:
 * @typename: Name of the type.
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 *
 * Finds and verifies the type of a serialisable boxed type.
 *
 * This is used to verify the %GWY_SERIALIZABLE_BOXED items that denote boxed types.
 *
 * Returns: The type corresponding to @typename on success, %NULL on failure.
 **/
static GType
get_serializable_boxed(const gchar *typename,
                       GwyErrorList **error_list)
{
    GType type;

    type = g_type_from_name(typename);
    if (G_UNLIKELY(!type)) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_OBJECT,
                           // TRANSLATORS: Error message.
                           _("Boxed type ‘%s’ is not known. It was ignored."),
                           typename);
        return 0;
    }

    if (G_UNLIKELY(!gwy_boxed_type_is_serializable(type))) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_OBJECT,
                           // TRANSLATORS: Error message.
                           _("Boxed type ‘%s’ is not serializable. It was ignored."),
                           typename);
        return 0;
    }

    return type;
}

static GwySerializableGroup*
unpack_group(const guchar *buffer,
             gsize size,
             GwySerializeSizeType sizetype,
             gboolean exact_size,
             GwyErrorList **error_list)
{
    const gchar *typename = NULL;
    gsize rbytes, object_size, name_size;

    /* Type name */
    if (!(rbytes = unpack_name(buffer, size, &typename, error_list)))
        return NULL;
    name_size = rbytes;
    buffer += rbytes;
    size -= rbytes;

    /* Object size (1 means object size is measured in bytes) */
    if (!(rbytes = unpack_size(buffer, size, 1, &object_size, "object", sizetype, error_list)))
        return NULL;
    buffer += rbytes;
    size -= rbytes;

    if (exact_size && G_UNLIKELY(size != object_size)) {
        /* We should not only fit, but the object size should be exactly size. If there is extra stuff, add an error,
         * but do not fail. */
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_PADDING,
                           // TRANSLATORS: Error message.
                           _("Object ‘%s’ data is smaller than the space alloted for it. The padding was ignored."),
                           typename);
    }

    GwySerializableGroup *group = gwy_serializable_group_new(typename, 6);
    group->size_bytes = object_size + sizetype + name_size;

    size = object_size;
    while (size) {
        /* Grow items if necessary */
        if (group->n == group->len)
            ensure_group_size(group, group->len+1);

        GwySerializableItem *item = group->items + group->n;

        // Component name.
        if (!(rbytes = unpack_name(buffer, size, &item->name, error_list)))
            goto fail;
        buffer += rbytes;
        size -= rbytes;

        // Component type.
        if (!(rbytes = unpack_uint8(buffer, size, &item->ctype, error_list)))
            goto fail;
        buffer += rbytes;
        size -= rbytes;

        GwySerializableCType ctype = item->ctype;

        // The data.
        if (ctype == GWY_SERIALIZABLE_OBJECT || ctype == GWY_SERIALIZABLE_BOXED) {
            if (!(item->value.v_group = unpack_group(buffer, size, sizetype, FALSE, error_list)))
                goto fail;
            item->flags |= GWY_SERIALIZABLE_IS_GROUP;
            rbytes = item->value.v_group->size_bytes;
        }
        else if (ctype == GWY_SERIALIZABLE_BOOLEAN) {
            if (!(rbytes = unpack_boolean(buffer, size, &item->value.v_boolean, error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_INT8) {
            if (!(rbytes = unpack_uint8(buffer, size, &item->value.v_uint8, error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_INT16) {
            if (!(rbytes = unpack_uint16(buffer, size, &item->value.v_uint16, error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_INT32) {
            if (!(rbytes = unpack_uint32(buffer, size, &item->value.v_uint32, error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_INT64) {
            if (!(rbytes = unpack_uint64(buffer, size, &item->value.v_uint64, error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_DOUBLE) {
            if (!(rbytes = unpack_double(buffer, size, &item->value.v_uint64, error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_FLOAT) {
            if (!(rbytes = unpack_float(buffer, size, &item->value.v_uint32, error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_STRING) {
            if (!(rbytes = unpack_string(buffer, size, &item->value.v_string, error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_INT8_ARRAY) {
            if (!(rbytes = unpack_uint8_array(buffer, size, &item->array_size, &item->value.v_uint8_array, sizetype,
                                              error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_INT16_ARRAY) {
            if (!(rbytes = unpack_uint16_array(buffer, size, &item->array_size, &item->value.v_uint16_array, sizetype,
                                               error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_INT32_ARRAY) {
            if (!(rbytes = unpack_uint32_array(buffer, size, &item->array_size, &item->value.v_uint32_array, sizetype,
                                               error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_INT64_ARRAY) {
            if (!(rbytes = unpack_uint64_array(buffer, size, &item->array_size, &item->value.v_uint64_array, sizetype,
                                               error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_DOUBLE_ARRAY) {
            if (!(rbytes = unpack_double_array(buffer, size, &item->array_size, &item->value.v_uint64_array, sizetype,
                                               error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_FLOAT_ARRAY) {
            if (!(rbytes = unpack_float_array(buffer, size, &item->array_size, &item->value.v_uint32_array, sizetype,
                                              error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_STRING_ARRAY) {
            if (!(rbytes = unpack_string_array(buffer, size, &item->array_size, &item->value.v_string_array, sizetype,
                                               error_list)))
                goto fail;
        }
        else if (ctype == GWY_SERIALIZABLE_OBJECT_ARRAY) {
            if (!(rbytes = unpack_group_array(buffer, size, &item->array_size, &item->value.v_group_array, sizetype,
                                              error_list)))
                goto fail;
            item->flags |= GWY_SERIALIZABLE_IS_GROUP;
        }
        else {
            gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_DATA,
                               // TRANSLATORS: Error message.
                               _("Data type 0x%02x is unknown. Since it could not be just skipped, complete "
                                 "object ‘%s’ was ignored."),
                               ctype, item->name);
            goto fail;
        }

        buffer += rbytes;
        size -= rbytes;
        group->n++;
    }

    return group;

fail:
    /* If we got here we hit a hard error. Do not complain about unconsumed items. */
    gwy_serializable_group_free(group, TRUE, NULL);
    return NULL;
}

/**
 * gwy_deserialize_error_quark:
 *
 * Gets the error domain for deserialisation.
 *
 * See and use %GWY_DESERIALIZE_ERROR.
 *
 * Returns: The error domain.
 **/
G_DEFINE_QUARK(gwy-deserialize-error-quark, gwy_deserialize_error);

/**
 * SECTION: deserialize
 * @title: Deserialise
 * @short_description: Object deserialisation
 *
 * Functions available here implement the GWY binary data format, version 3. Some low-level facilities for version
 * 2 format are also provided. Essentially, for version 3, you can use gwy_deserialize_memory() to directly construct
 * objects. For version 2, you can use gwy_deserialize_group_memory() with %GWY_SERIALIZE_SIZE_32BIT to construct the
 * intermediate representation, but not objects directly (since they in general differ).
 *
 * <refsect2>
 * <title>Details of Deserialization</title>
 * <para>The following information is not necessary for implementing #GwySerializable interface in your classes, but
 * it can help prevent wrong decision about serialised representation of your objects.  Also, it might help
 * implementing a different serialisation backend than GWY files.</para>
 * <para>Serialization has two basic steps.</para>
 * <para>First, the serialised buffer is parse into a tree of #GwySerializableGroup, using @v_group for nested
 * objects. This step checks basic validity of the serialised representation. If it succeeds, the
 * #GwySerializableGroup tree is well-formed, although it may not represent valid Gwyddion object data. If only this
 * step is run, it can also be used to represent other things than Gwyddion files. This is also the only
 * backend-depndent step which would be implemented differently for HDF5 as the serialisation format, for
 * instance.</para>
 * <para>Second, objects are reconstructed from the tree in a depth-first order. When an object's #GwySerializable
 * construct() method is called, its object- and boxed-valued components have already been constructed and it can
 * get them by reading @v_object or @v_boxed values of #GwySerializableValue.</para>
 * <para>There is also some cleanup. But it is done during the second step, not as a separate pass.</para>
 * </refsect2>
 **/

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