/*
 *  libMirage: DMG filter: filter stream
 *  Copyright (C) 2012-2026 Henrik Stokseth
 *
 *  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 "filter-dmg.h"
#include "filter-encrcdsa.h"

#define __debug__ "DMG-FilterStream"


static const guint8 koly_signature[4] = {'k', 'o', 'l', 'y'};
/*static const guint8 mish_signature[4] = {'m', 'i', 's', 'h'};*/

typedef struct
{
    DMG_block_type type;

    guint64 first_sector;
    guint64 num_sectors;
    gint segment;
    goffset in_offset;
    gsize in_length;
} DMG_Part;

typedef gchar * (*DMG_create_filename_func) (const gchar *main_filename, gint index);


/**********************************************************************\
 *                  Object and its private structure                  *
\**********************************************************************/
struct _MirageFilterStreamDmgPrivate
{
    /* koly blocks - one for each segment */
    koly_block_t *koly_blocks;
    guint num_segments;

    /* resource fork */
    rsrc_fork_t *rsrc_fork;

    /* streams */
    MirageStream **streams;

    /* Part list */
    DMG_Part *parts;
    guint num_parts;

    /* Inflate buffer */
    guint8 *inflate_buffer;
    guint inflate_buffer_size;
    gint cached_part;

    /* I/O buffer */
    guint8 *io_buffer;
    guint io_buffer_size;

    /* Compression streams */
    z_stream  zlib_stream;
    bz_stream bzip2_stream;

    /* Filename function for multi-segment images */
    DMG_create_filename_func create_filename_func;
};


G_DEFINE_DYNAMIC_TYPE_EXTENDED(
    MirageFilterStreamDmg,
    mirage_filter_stream_dmg,
    MIRAGE_TYPE_FILTER_STREAM,
    0,
    G_ADD_PRIVATE_DYNAMIC(MirageFilterStreamDmg)
)

void mirage_filter_stream_dmg_type_register (GTypeModule *type_module)
{
    mirage_filter_stream_dmg_register_type(type_module);
}


/**********************************************************************\
 *                    Endian-conversion functions                     *
\**********************************************************************/
static inline void mirage_filter_stream_dmg_koly_block_fix_endian (koly_block_t *koly_block)
{
    g_assert(koly_block);

    koly_block->version = GUINT32_FROM_BE(koly_block->version);
    koly_block->header_size = GUINT32_FROM_BE(koly_block->header_size);
    koly_block->flags = GUINT32_FROM_BE(koly_block->flags);
    koly_block->image_variant = GUINT32_FROM_BE(koly_block->image_variant);

    koly_block->running_data_fork_offset = GUINT64_FROM_BE(koly_block->running_data_fork_offset);
    koly_block->data_fork_offset = GUINT64_FROM_BE(koly_block->data_fork_offset);
    koly_block->data_fork_length = GUINT64_FROM_BE(koly_block->data_fork_length);
    koly_block->rsrc_fork_offset = GUINT64_FROM_BE(koly_block->rsrc_fork_offset);
    koly_block->rsrc_fork_length = GUINT64_FROM_BE(koly_block->rsrc_fork_length);
    koly_block->xml_offset = GUINT64_FROM_BE(koly_block->xml_offset);
    koly_block->xml_length = GUINT64_FROM_BE(koly_block->xml_length);
    koly_block->sector_count = GUINT64_FROM_BE(koly_block->sector_count);

    koly_block->segment_number = GUINT32_FROM_BE(koly_block->segment_number);
    koly_block->segment_count  = GUINT32_FROM_BE(koly_block->segment_count);

    /* Note: It seems segment_id should not be endian converted */

    koly_block->data_fork_checksum.type = GUINT32_FROM_BE(koly_block->data_fork_checksum.type);
    koly_block->data_fork_checksum.size = GUINT32_FROM_BE(koly_block->data_fork_checksum.size);
    koly_block->master_checksum.type = GUINT32_FROM_BE(koly_block->master_checksum.type);
    koly_block->master_checksum.size = GUINT32_FROM_BE(koly_block->master_checksum.size);

    for (guint i = 0; i < 32; i++) {
        koly_block->master_checksum.data[i] = GUINT32_FROM_BE(koly_block->master_checksum.data[i]);
    }

    for (guint i = 0; i < 32; i++) {
        koly_block->data_fork_checksum.data[i] = GUINT32_FROM_BE(koly_block->data_fork_checksum.data[i]);
    }

    /* skip reserved1 and reserved2 */
}


static inline void mirage_filter_stream_dmg_blkx_block_fix_endian (blkx_block_t *blkx_block)
{
    g_assert(blkx_block);

    blkx_block->info_version = GUINT32_FROM_BE(blkx_block->info_version);

    blkx_block->first_sector_number = GUINT64_FROM_BE(blkx_block->first_sector_number);
    blkx_block->sector_count = GUINT64_FROM_BE(blkx_block->sector_count);
    blkx_block->data_start = GUINT64_FROM_BE(blkx_block->data_start);

    blkx_block->decompressed_buffer_requested = GUINT32_FROM_BE(blkx_block->decompressed_buffer_requested);

    blkx_block->blocks_descriptor = GINT32_FROM_BE(blkx_block->blocks_descriptor);
    blkx_block->blocks_run_count = GUINT32_FROM_BE(blkx_block->blocks_run_count);
    blkx_block->checksum.type = GUINT32_FROM_BE(blkx_block->checksum.type);
    blkx_block->checksum.size = GUINT32_FROM_BE(blkx_block->checksum.size);

    for (guint i = 0; i < 32; i++) {
        blkx_block->checksum.data[i] = GUINT32_FROM_BE(blkx_block->checksum.data[i]);
    }

    /* skip reserved */
}

static inline void mirage_filter_stream_dmg_blkx_data_fix_endian (blkx_data_t *blkx_data)
{
    g_assert(blkx_data);

    blkx_data->block_type = GINT32_FROM_BE(blkx_data->block_type);

    blkx_data->sector_offset = GUINT64_FROM_BE(blkx_data->sector_offset);
    blkx_data->sector_count = GUINT64_FROM_BE(blkx_data->sector_count);
    blkx_data->compressed_offset = GUINT64_FROM_BE(blkx_data->compressed_offset);
    blkx_data->compressed_length = GUINT64_FROM_BE(blkx_data->compressed_length);
}

static inline void mirage_filter_stream_dmg_csum_fix_endian (csum_block_t *csum_block)
{
    g_assert(csum_block);

    csum_block->version = GUINT16_FROM_LE(csum_block->version);
    csum_block->type = GUINT16_FROM_LE(csum_block->type);
    csum_block->data = GUINT32_FROM_LE(csum_block->data);
}

static inline void mirage_filter_stream_dmg_size_fix_endian (size_block_t *size_block)
{
    g_assert(size_block);

    size_block->version = GUINT16_FROM_LE(size_block->version);
    size_block->is_hfs = GUINT32_FROM_LE(size_block->is_hfs);
    size_block->unknown1 = GUINT32_FROM_LE(size_block->unknown1);
    size_block->unknown2 = GUINT32_FROM_LE(size_block->unknown2);
    size_block->unknown3 = GUINT32_FROM_LE(size_block->unknown3);
    size_block->vol_modified = GUINT32_FROM_LE(size_block->vol_modified);
    size_block->unknown4 = GUINT32_FROM_LE(size_block->unknown4);
    size_block->vol_sig.as_int = GUINT16_FROM_BE(size_block->vol_sig.as_int);
    size_block->size_present = GUINT16_FROM_LE(size_block->size_present);
}


/**********************************************************************\
 *                         Debug functions                            *
\**********************************************************************/
static inline void _dump_checksum_fields (MirageFilterStreamDmg *self, const guint32 *data, const gchar *indent)
{
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  %s0x%08X 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x", __debug__, indent, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  %s0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x", __debug__, indent, data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  %s0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x", __debug__, indent, data[16], data[17], data[18], data[19], data[20], data[21], data[22], data[23]);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  %s0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x", __debug__, indent, data[24], data[25], data[26], data[27], data[28], data[29], data[30], data[31]);
}

static void mirage_filter_stream_dmg_print_koly_block (MirageFilterStreamDmg *self, koly_block_t *koly_block)
{
    g_assert(self && koly_block);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: DMG trailer:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  signature: %.4s", __debug__, koly_block->signature);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  version: %u", __debug__, koly_block->version);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  header_size: %u", __debug__, koly_block->header_size);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  flags: 0x%X", __debug__, koly_block->flags);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  image_variant: %u", __debug__, koly_block->image_variant);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  running_data_fork_offset: 0x%" G_GINT64_MODIFIER "x", __debug__, koly_block->running_data_fork_offset);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data_fork_offset: 0x%" G_GINT64_MODIFIER "x", __debug__, koly_block->data_fork_offset);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data_fork_length: %" G_GINT64_MODIFIER "u", __debug__, koly_block->data_fork_length);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  rsrc_fork_offset: 0x%" G_GINT64_MODIFIER "x", __debug__, koly_block->rsrc_fork_offset);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  rsrc_fork_length: %" G_GINT64_MODIFIER "u", __debug__, koly_block->rsrc_fork_length);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  xml_offset: 0x%" G_GINT64_MODIFIER "x", __debug__, koly_block->xml_offset);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  xml_length: %" G_GINT64_MODIFIER "u", __debug__, koly_block->xml_length);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  sector_count: %" G_GINT64_MODIFIER "u", __debug__, koly_block->sector_count);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  segment_number: %u", __debug__, koly_block->segment_number);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  segment_count: %u", __debug__, koly_block->segment_count);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  segment_id: 0x%08x 0x%08x 0x%08x 0x0%08x", __debug__, koly_block->segment_id[0], koly_block->segment_id[1], koly_block->segment_id[2], koly_block->segment_id[3]);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data_fork_checksum.type: %u", __debug__, koly_block->data_fork_checksum.type);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data_fork_checksum.size: %u", __debug__, koly_block->data_fork_checksum.size);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data_fork_checksum.data:", __debug__);
    _dump_checksum_fields(self, koly_block->data_fork_checksum.data, " ");

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  master_checksum.type: %u", __debug__, koly_block->master_checksum.type);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  master_checksum.size: %u", __debug__, koly_block->master_checksum.size);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  master_checksum.data:", __debug__);
    _dump_checksum_fields(self, koly_block->master_checksum.data, " ");
}

static void mirage_filter_stream_dmg_print_blkx_block (MirageFilterStreamDmg *self, blkx_block_t *blkx_block)
{
    g_assert(self && blkx_block);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: mish block:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  signature: %.4s", __debug__, blkx_block->signature);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  info_version: %u", __debug__, blkx_block->info_version);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  first_sector_number: %" G_GINT64_MODIFIER "u", __debug__, blkx_block->first_sector_number);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  sector_count: %" G_GINT64_MODIFIER "u", __debug__, blkx_block->sector_count);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data_start: %" G_GINT64_MODIFIER "u", __debug__, blkx_block->data_start);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  decompressed_buffer_requested: %u", __debug__, blkx_block->decompressed_buffer_requested);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  blocks_descriptor: %i", __debug__, blkx_block->blocks_descriptor);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  checksum.type: %u", __debug__, blkx_block->checksum.type);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  checksum.size: %u", __debug__, blkx_block->checksum.size);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  checksum.data:", __debug__);
    _dump_checksum_fields(self, blkx_block->checksum.data, " ");
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  blocks_run_count: %u", __debug__, blkx_block->blocks_run_count);
}

static void mirage_filter_stream_dmg_print_csum_block (MirageFilterStreamDmg *self, csum_block_t *csum_block)
{
    g_assert(self && csum_block);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: cSum block:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  version: %u", __debug__, csum_block->version);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  type: %u", __debug__, csum_block->type);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data: 0x%08x", __debug__, csum_block->data);
}

static void mirage_filter_stream_dmg_print_size_block (MirageFilterStreamDmg *self, size_block_t *size_block)
{
    g_assert(self && size_block);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: size block:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  version: %u", __debug__, size_block->version);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  is_hfs: %u", __debug__, size_block->is_hfs);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  unknown1: 0x%08x", __debug__, size_block->unknown1);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data_length: %u", __debug__, size_block->data_length);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  unknown2: 0x%08x", __debug__, size_block->unknown2);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  unknown3: 0x%08x", __debug__, size_block->unknown3);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  vol_modified: 0x%08x", __debug__, size_block->vol_modified);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  unknown4: 0x%08x", __debug__, size_block->unknown4);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  vol_signature: %.2s", __debug__, size_block->vol_sig.as_array);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  size_present: %u", __debug__, size_block->size_present);
}


/**********************************************************************\
 *                     Part filename generation                       *
\**********************************************************************/
static gboolean check_filename_suffix (const gchar *filename, const gchar *suffix)
{
    const gsize len = strlen(filename);
    const gsize suffix_len = strlen(suffix);

    /* We require non-empty prefix part */
    if (len <= suffix_len) {
        return FALSE;
    }

    /* Since we are interested in matching only the ASCII suffix,
     * we can use strncasecmp() */
    return strncasecmp(filename + (len - suffix_len), suffix, suffix_len) == 0;
}

/* Format: volname.001.dmg, volname.002.dmg, ... */
static gchar *create_filename_func_legacy (const gchar *main_filename, gint index)
{
    if (!main_filename) {
        return NULL;
    }

    gint main_fn_len = strlen(main_filename) - 7; /* volname. (without 001.dmg) */
    gchar *ret_filename = g_try_malloc(main_fn_len + 7 + 1); /* len + NNN.dmg + \NUL */

    if (!ret_filename) {
        return NULL;
    }

    /* Copy base filename without '001.dmg' */
    memcpy(ret_filename, main_filename, main_fn_len);

    /* Replace three characters with index and append '.dmg' */
    gchar *position = ret_filename + main_fn_len;
    g_snprintf(position, 7 + 1, "%03i.dmg", index + 1);

    return ret_filename;
}

/* Format: volname.dmg, volname.002.dmgpart, ... */
static gchar *create_filename_func (const gchar *main_filename, gint index)
{
    if (!main_filename) {
        return NULL;
    }

    gint main_fn_len = strlen(main_filename) - 3; /* volname. (without dmg)*/
    gchar *ret_filename = g_try_malloc(main_fn_len + 11 + 1); /* len + NNN.dmgpart + \NUL */

    if (!ret_filename) {
        return NULL;
    }

    /* Copy base filename without '.dmg' */
    memcpy(ret_filename, main_filename, main_fn_len);

    /* Append index and '.dmgpart' */
    gchar *position = ret_filename + main_fn_len;
    g_snprintf(position, 11 + 1, "%03i.dmgpart", index + 1);

    return ret_filename;
}


/**********************************************************************\
 *                         Parsing functions                          *
\**********************************************************************/
static gboolean mirage_filter_stream_dmg_read_descriptor (MirageFilterStreamDmg *self, MirageStream *stream, GError **error)
{
    const koly_block_t *koly_block = &self->priv->koly_blocks[0]; /* First koly block */
    rsrc_fork_t  *rsrc_fork = NULL;
    gchar *rsrc_fork_data = NULL;

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: reading descriptor...", __debug__);

    /* Read and parse resource-fork */
    if (koly_block->xml_offset && koly_block->xml_length) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: image contains XML resource-fork...", __debug__);

        rsrc_fork_data = g_try_malloc(koly_block->xml_length);
        if (!rsrc_fork_data) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate memory for XML resource-fork!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to allocate memory!"));
            return FALSE;
        }

        mirage_stream_seek(stream, koly_block->xml_offset, G_SEEK_SET, NULL);
        if ((gsize)mirage_stream_read(stream, rsrc_fork_data, koly_block->xml_length, NULL) != koly_block->xml_length) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read XML resource-fork!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read XML resource-fork!"));
            return FALSE;
        }

        rsrc_fork = self->priv->rsrc_fork = rsrc_fork_read_xml(rsrc_fork_data, koly_block->xml_length);
    } else if (koly_block->rsrc_fork_offset && koly_block->rsrc_fork_length) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: image contains binary resource-fork...", __debug__);

        rsrc_fork_data = g_try_malloc(koly_block->rsrc_fork_length);
        if (!rsrc_fork_data) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate memory for binary resource-fork!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to allocate memory!"));
            return FALSE;
        }

        mirage_stream_seek(stream, koly_block->rsrc_fork_offset, G_SEEK_SET, NULL);
        if ((gsize)mirage_stream_read(stream, rsrc_fork_data, koly_block->rsrc_fork_length, NULL) != koly_block->rsrc_fork_length) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read binary resource-fork!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read binary resource-fork!"));
            return FALSE;
        }

        rsrc_fork = self->priv->rsrc_fork = rsrc_fork_read_binary(rsrc_fork_data, koly_block->rsrc_fork_length);
    } else {
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Image lacks either an XML or a binary descriptor!"));
        return FALSE;
    }

    /* Did all go well? */
    if (!rsrc_fork) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to parse resource-fork!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to parse resource-fork!"));
        return FALSE;
    }

    g_free(rsrc_fork_data);
    rsrc_fork_data = NULL;

    /* Loop through resource types */
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing data from resource-fork...", __debug__);
    for (guint t = 0; t < rsrc_fork->type_list->len; t++) {
        rsrc_type_t *rsrc_type = &g_array_index(rsrc_fork->type_list, rsrc_type_t, t);

        /* Loop through resource refs */
        for (guint r = 0; r < rsrc_type->ref_list->len; r++) {
            rsrc_ref_t *rsrc_ref = &g_array_index(rsrc_type->ref_list, rsrc_ref_t, r);

            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing resource:", __debug__);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  type: %.4s", __debug__, rsrc_type->type.as_array);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  id: %i", __debug__, rsrc_ref->id);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  name: %s", __debug__, rsrc_ref->name->str);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  attrs: 0x%04hx", __debug__, rsrc_ref->attrs);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  data length: %i", __debug__, rsrc_ref->data_length);

            /* Convert resource endianness */
            if (!memcmp(rsrc_type->type.as_array, "plst", 4)) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: this resource contains partition table information - skipping...", __debug__);
            } else if (!memcmp(rsrc_type->type.as_array, "cSum", 4)) {
                csum_block_t *csum_block = (csum_block_t *)rsrc_ref->data;

                g_assert(rsrc_ref->data_length == sizeof(csum_block_t));
                mirage_filter_stream_dmg_csum_fix_endian(csum_block);
                mirage_filter_stream_dmg_print_csum_block(self, csum_block);
            } else if (!memcmp(rsrc_type->type.as_array, "size", 4)) {
                size_block_t *size_block = (size_block_t *)rsrc_ref->data;

                g_assert(rsrc_ref->data_length == sizeof(size_block_t));
                mirage_filter_stream_dmg_size_fix_endian(size_block);
                mirage_filter_stream_dmg_print_size_block(self, size_block);
            } else if (!memcmp(rsrc_type->type.as_array, "nsiz", 4)) {
                /* Note: There are the same amount of nsiz and the cSum blocks which have the same checksums */
                GString *nsiz_data = NULL;

                nsiz_data = g_string_new_len(rsrc_ref->data, rsrc_ref->data_length);
                g_assert(nsiz_data);

                MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: nsiz data: %s", __debug__, nsiz_data->str);

                g_string_free(nsiz_data, TRUE);
            } else if (!memcmp(rsrc_type->type.as_array, "blkx", 4)) {
                blkx_block_t *blkx_block = (blkx_block_t *)rsrc_ref->data;
                blkx_data_t *blkx_data = (blkx_data_t *)(rsrc_ref->data + sizeof(blkx_block_t));

                mirage_filter_stream_dmg_blkx_block_fix_endian(blkx_block);
                mirage_filter_stream_dmg_print_blkx_block(self, blkx_block); /* Prints only blkx header, without data */

                MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: processing blkx entry with %d block(s)...", __debug__, blkx_block->blocks_run_count);
                for (guint d = 0; d < blkx_block->blocks_run_count; d++) {
                    mirage_filter_stream_dmg_blkx_data_fix_endian(&blkx_data[d]);

                    gint32 block_type = blkx_data[d].block_type;
                    switch (block_type) {
                        case ADC:
                        case ZLIB:
                        case BZLIB:
                        case ZERO:
                        case RAW:
                        case IGNORE: {
                            /* Update parts count */
                            self->priv->num_parts++;
                            break;
                        }
                        case COMMENT:
                        case TERM: {
                            /* Do nothing */
                            break;
                        }
                        default: {
                            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: encountered unknown block type: %d", __debug__, block_type);
                            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Encountered unknown block type: %d!"), block_type);
                            return FALSE;
                        }
                    }
                }
            } else {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: encountered unknown resource type: %.4s", __debug__, rsrc_type->type.as_array);
            }
        }
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: finished parsing descriptor", __debug__);

    return TRUE;
}

static gboolean mirage_filter_stream_dmg_read_part_index (MirageFilterStreamDmg *self, GError **error)
{
    z_stream *zlib_stream = &self->priv->zlib_stream;
    bz_stream *bzip2_stream = &self->priv->bzip2_stream;

    const koly_block_t *koly_block = &self->priv->koly_blocks[0]; /* First koly block */
    const rsrc_fork_t *rsrc_fork = self->priv->rsrc_fork;
    const rsrc_type_t *rsrc_type = NULL;

    gint cur_part = 0;
    gint ret;

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: reading part index...", __debug__);

    /* Allocate part index */
    self->priv->parts = g_try_new(DMG_Part, self->priv->num_parts);
    if (!self->priv->parts) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate memory for part index!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to allocate memory for part index!"));
        return FALSE;
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: number of parts: %d", __debug__, self->priv->num_parts);

    /* Loop through resource refs of type 'blkx' */
    rsrc_type = rsrc_find_type(rsrc_fork, "blkx");
    if (!rsrc_type) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to find required 'blkx' resource type!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to find required 'blkx' resource type!"));
        return FALSE;
    }

    for (guint r = 0; r < rsrc_type->ref_list->len; r++) {
        const rsrc_ref_t *rsrc_ref = &g_array_index(rsrc_type->ref_list, rsrc_ref_t, r);
        const blkx_block_t *blkx_block = (const blkx_block_t *)rsrc_ref->data;
        const blkx_data_t *blkx_data  = (const blkx_data_t *)(rsrc_ref->data + sizeof(blkx_block_t));

        /* Loop through blocks */
        for (guint n = 0; n < blkx_block->blocks_run_count; n++) {
            DMG_Part temp_part;

            temp_part.type = blkx_data[n].block_type;
            temp_part.first_sector = blkx_block->first_sector_number + blkx_data[n].sector_offset;
            temp_part.num_sectors = blkx_data[n].sector_count;
            temp_part.in_offset = blkx_block->data_start + blkx_data[n].compressed_offset;
            temp_part.in_length = blkx_data[n].compressed_length;

            /* Find segment belonging to part */
            temp_part.segment = -1;
            for (guint s = 0; s < self->priv->num_segments; s++) {
                if (temp_part.in_offset >= (goffset)koly_block[s].running_data_fork_offset) {
                    temp_part.segment = s;
                } else {
                    break;
                }
            }

            /* Update required buffer sizes */
            switch (temp_part.type) {
                case ADC:
                case ZLIB:
                case BZLIB: {
                    const guint io_buffer_size = temp_part.in_length;
                    const guint inflate_buffer_size = temp_part.num_sectors * DMG_SECTOR_SIZE;

                    self->priv->io_buffer_size = MAX(self->priv->io_buffer_size, io_buffer_size);
                    self->priv->inflate_buffer_size = MAX(self->priv->inflate_buffer_size, inflate_buffer_size);

                    break;
                }
                case RAW: {
                    /* We are reading RAW data chunks into inflate buffer... */
                    const guint inflate_buffer_size = temp_part.num_sectors * DMG_SECTOR_SIZE;
                    self->priv->inflate_buffer_size = MAX(self->priv->inflate_buffer_size, inflate_buffer_size);
                    break;
                }
                case ZERO:
                case IGNORE: {
                    /* No buffer used */
                    break;
                }
                case COMMENT:
                case TERM: {
                     /* Skip */
                    continue;
                }
                default: {
                    /* Earlier processing in mirage_filter_stream_dmg_read_descriptor()
                     * has already validated types */
                    g_assert_not_reached();
                }
            }

            /* Add part to list */
            self->priv->parts[cur_part] = temp_part;
            cur_part++;
        }
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: required I/O buffer size: %u", __debug__, self->priv->io_buffer_size);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: required inflate buffer size: %u", __debug__, self->priv->inflate_buffer_size);

    /* Initialize zlib stream */
    zlib_stream->zalloc = Z_NULL;
    zlib_stream->zfree = Z_NULL;
    zlib_stream->opaque = Z_NULL;
    zlib_stream->avail_in = 0;
    zlib_stream->next_in = Z_NULL;

    ret = inflateInit2(zlib_stream, 15);

    if (ret != Z_OK) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to initialize zlib's inflate! Error: %d", __debug__, ret);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to initialize zlib's inflate (error: %d)!"), ret);
        return FALSE;
    }

    /* Initialize bzip2 stream */
    bzip2_stream->bzalloc = NULL;
    bzip2_stream->bzfree = NULL;
    bzip2_stream->opaque = NULL;
    bzip2_stream->avail_in = 0;
    bzip2_stream->next_in = NULL;

    ret = BZ2_bzDecompressInit(bzip2_stream, 0, 0);

    if (ret != BZ_OK) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to initialize libbz2's decompress! Error: %d", __debug__, ret);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to initialize libbz2's decompress (error: %d)!"), ret);
        return FALSE;
    }

    /* Allocate inflate buffer */
    self->priv->inflate_buffer = g_try_malloc(self->priv->inflate_buffer_size);
    if (!self->priv->inflate_buffer && self->priv->inflate_buffer_size) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate memory for inflate buffer!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to allocate memory for inflate buffer!"));
        return FALSE;
    }

    /* Allocate I/O buffer */
    self->priv->io_buffer = g_try_malloc(self->priv->io_buffer_size);
    if (!self->priv->io_buffer && self->priv->io_buffer_size) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate memory for I/O buffer!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to allocate memory for I/O buffer!"));
        return FALSE;
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: successfully read part index", __debug__);

    return TRUE;
}


/**********************************************************************\
 *               MirageFilterStream methods implementation            *
\**********************************************************************/
static gboolean mirage_filter_stream_dmg_open_streams (MirageFilterStreamDmg *self, GError **error)
{
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: opening stream(s) for %d segment(s)...", __debug__, self->priv->num_segments);

    /* Fill in existing stream */
    self->priv->streams[0] = g_object_ref(mirage_filter_stream_get_underlying_stream(MIRAGE_FILTER_STREAM(self)));

    const gchar *original_filename = mirage_stream_get_filename(self->priv->streams[0]);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: reusing stream #0 on filename: %s", __debug__, original_filename);

    /* Create the rest of the streams */
    for (guint s = 1; s < self->priv->num_segments; s++) {
        MirageStream *stream;
        GError *local_error = NULL;
        gchar *filename = self->priv->create_filename_func(original_filename, s);

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: trying to create stream #%d for file: %s", __debug__, s, filename);

        /* Directly create an instance of MirageFileStream, instead of
         * going through mirage_contextual_create_input_stream(). Using
         * the latter would try to instantiate DMG filter again, and that
         * would fail due to file not being the first one in the set. While
         * we could demote the corresponding error in mirage_filter_stream_dmg_open()
         * from MIRAGE_ERROR_STREAM_ERROR to MIRAGE_ERROR_CANNOT_HANDLE,
         * that would effectively result in creation of a MirageFileStream. */
        stream = g_object_new(MIRAGE_TYPE_FILE_STREAM, NULL);

        /* Propagate context; for debugging */
        mirage_contextual_inherit_context(MIRAGE_CONTEXTUAL(stream), MIRAGE_CONTEXTUAL(self));

        if (!mirage_file_stream_open(MIRAGE_FILE_STREAM(stream), filename, FALSE, &local_error)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to create stream #%d on file %s: %s", __debug__, s, filename, local_error->message);
            g_error_free(local_error);
            g_object_unref(stream);
            g_free(filename);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to create stream!"));
            return FALSE;
        }

        g_free(filename);

        /* If the first stream is an instance of EncrCDSA stream, the image
         * is encrypted; in that case, we also need to create EncrCDSA stream
         * for each segment. */
        if (MIRAGE_IS_FILTER_STREAM_ENCRCDSA(self->priv->streams[0])) {
            MirageStream *orig_stream = stream;

            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: trying to create EncrCDSA stream on top of stream #%d...", __debug__, s);

            stream = g_object_new(MIRAGE_TYPE_FILTER_STREAM_ENCRCDSA, NULL);

            /* Propagate context; for debugging and password settings */
            mirage_contextual_inherit_context(MIRAGE_CONTEXTUAL(stream), MIRAGE_CONTEXTUAL(self));

            if (!mirage_filter_stream_open(MIRAGE_FILTER_STREAM(stream), orig_stream, FALSE, &local_error)) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to create EncrCDSA stream on top of stream #%d: %s", __debug__, s, local_error->message);
                g_error_free(local_error);
                g_object_unref(stream);
                g_object_unref(orig_stream);
                g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to create stream!"));
                return FALSE;
            }

            g_object_unref(orig_stream); /* Now owned by EncrCDSA stream */
        }

        self->priv->streams[s] = stream;
    }

    /* Read the rest of the koly blocks */
    for (guint s = 1; s < self->priv->num_segments; s++) {
        MirageStream *stream = self->priv->streams[s];
        koly_block_t *koly_block = &self->priv->koly_blocks[s];

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: reading koly block in stream #%d...!", __debug__, s);
        for (guint try = 0; try < 2; try++) {
            /* Find koly block either on end (most often) or beginning of file */
            if (try == 0) {
                mirage_stream_seek(stream, -sizeof(koly_block_t), G_SEEK_END, NULL);
            } else {
                mirage_stream_seek(stream, 0, G_SEEK_SET, NULL);
            }

            /* Read koly block */
            if (mirage_stream_read(stream, koly_block, sizeof(koly_block_t), NULL) != sizeof(koly_block_t)) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read koly block!", __debug__);
                g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to read koly block!"));
                return FALSE;
            }

            /* Validate koly block */
            if (memcmp(koly_block->signature, koly_signature, sizeof(koly_signature))) {
                if (try == 1) {
                    MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: invalid koly block!", __debug__);
                    g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Invalid koly block!"));
                    return FALSE;
                }
            } else {
                mirage_filter_stream_dmg_koly_block_fix_endian(koly_block);
                break;
            }
        }

        /* Output koly block info */
        mirage_filter_stream_dmg_print_koly_block(self, koly_block);
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: successfully opened %d streams", __debug__, self->priv->num_segments);

    return TRUE;
}

static gboolean mirage_filter_stream_dmg_open (MirageFilterStream *_self, MirageStream *stream, gboolean writable G_GNUC_UNUSED, GError **error)
{
    MirageFilterStreamDmg *self = MIRAGE_FILTER_STREAM_DMG(_self);
    koly_block_t *koly_block = NULL;

    /* Allocate initial koly block */
    self->priv->num_segments = 1;
    self->priv->koly_blocks = g_try_new(koly_block_t, self->priv->num_segments);
    if (!self->priv->koly_blocks) {
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to allocate memory for koly block!"));
        return FALSE;
    }
    koly_block = &self->priv->koly_blocks[0];

    for (guint try = 0; try < 2; try++) {
        /* Find koly block either on end (most often) or beginning of file */
        if (try == 0) {
            mirage_stream_seek(stream, -sizeof(koly_block_t), G_SEEK_END, NULL);
        } else {
            mirage_stream_seek(stream, 0, G_SEEK_SET, NULL);
        }

        /* Read koly block */
        if (mirage_stream_read(stream, koly_block, sizeof(koly_block_t), NULL) != sizeof(koly_block_t)) {
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_CANNOT_HANDLE, Q_("Filter stream cannot handle given image: failed to read koly block!"));
            return FALSE;
        }

        /* Validate koly block */
        if (memcmp(koly_block->signature, koly_signature, sizeof(koly_signature))) {
            if (try == 1) {
                g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_CANNOT_HANDLE, Q_("Filter stream cannot handle given image: invalid koly block!"));
                return FALSE;
            }
        } else {
            mirage_filter_stream_dmg_koly_block_fix_endian(koly_block);
            break;
        }
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing the underlying stream data...", __debug__);

    /* Display koly block for debugging purposes */
    mirage_filter_stream_dmg_print_koly_block(self, koly_block);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:", __debug__);

    /* Only perform parsing on the first file in a segment image.
     * Note that non-segmented images may have num_segments and
     * segment_number set either both to 0 or both to 1. */
    if (koly_block->segment_count > 1 && koly_block->segment_number != 1) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: file is not the first file of a set (has segment_number=%d)!", __debug__, koly_block->segment_number);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("File is not the first file of a set!"));
        return FALSE;
    }

    /* Determine number of segments */
    self->priv->num_segments = koly_block->segment_count;
    if (self->priv->num_segments == 0) {
        self->priv->num_segments = 1;
    }

    /* In case of multi-segment image, attempt to infer the naming scheme.
     *
     * Contemporary versions of `hdiutil` on macOS use the following naming
     * scheme: volname.dmg, volname.002.dmgpart, volname.003.dmgpart, ...
     *
     * Some sources on the internet refer to legacy multi-segment DMG
     * images with the following naming scheme: volname.001.dmg,
     * volname.002.dmg, volname.003.dmg, ...
     *
     * However, it seems that `hdiutil` does NOT rely on filenames, and
     * is able to stitch together multi-segment DMG images with arbitrarily
     * named parts, as long as they are all in the same location. Similarly,
     * it does not matter which of the parts is passed to it. This likely
     * means that it reads the `segment_id` in the koly block of the file
     * it is passed, and then scans the directory for other DMG files with
     * matching `segment_id` value in their koly block.
     *
     * While we could try implementing the same approach, we might run
     * into problems with encrypted images, in particular when multiple
     * multi-segment and encrypted images are located in the same directory
     * (because we would need a password for each of them to be able to
     * read their koly blocks).
     *
     * So for now, require that files adhere to one of the two mentioned
     * naming schemes. */
    if (self->priv->num_segments > 1) {
        const gchar *original_filename = mirage_stream_get_filename(stream);
        if (check_filename_suffix(original_filename, ".001.dmg")) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: multi-segment image; assumed filename format: volname.001.dmg, volname.002.dmg, ...", __debug__);
            self->priv->create_filename_func = create_filename_func_legacy;
        } else {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: multi-segment image; assumed filename format: volname.dmg, volname.002.dmgpart, ...", __debug__);
            self->priv->create_filename_func = create_filename_func;
        }
    }

    /* Re-allocate space for additional koly blocks, if necessary */
    if (self->priv->num_segments > 1) {
        self->priv->koly_blocks = g_try_renew(koly_block_t, self->priv->koly_blocks, self->priv->num_segments);
        if (!self->priv->koly_blocks) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to reallocate memory for koly blocks!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to reallocate memory for koly blocks!"));
            return FALSE;
        }
        /* Update local pointer to first koly block! */
        koly_block = &self->priv->koly_blocks[0];
    }

    /* Allocate space for streams (and make sure to clear the memory,
     * so that in case of an error, we get NULL pointers for un-initialized
     * streams). */
    self->priv->streams = g_try_new0(MirageStream *, self->priv->num_segments);
    if (!self->priv->streams) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate memory for streams!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to allocate memory for streams!"));
        return FALSE;
    }

    /* Open streams */
    if (!mirage_filter_stream_dmg_open_streams(self, error)) {
        return FALSE;
    }

    /* Read descriptor */
    if (!mirage_filter_stream_dmg_read_descriptor(self, stream, error)) {
        return FALSE;
    }

    /* Read part index */
    if (!mirage_filter_stream_dmg_read_part_index(self, error)) {
        return FALSE;
    }

    /* Set file size */
    mirage_filter_stream_simplified_set_stream_length(_self, koly_block->sector_count * DMG_SECTOR_SIZE);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: original stream size: %" G_GINT64_MODIFIER "u", __debug__, koly_block->sector_count * DMG_SECTOR_SIZE);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing completed successfully", __debug__);

    return TRUE;
}

static gssize mirage_filter_stream_dmg_read_raw_chunk (MirageFilterStreamDmg *self, guint8 *buffer, gint chunk_num)
{
    const DMG_Part *part = &self->priv->parts[chunk_num];
    MirageStream *stream = self->priv->streams[part->segment];
    const koly_block_t *koly_block = &self->priv->koly_blocks[part->segment];

    gsize to_read = part->in_length;
    gsize have_read = 0;
    goffset part_offs = koly_block->data_fork_offset + part->in_offset - koly_block->running_data_fork_offset;
    gsize part_avail = koly_block->running_data_fork_offset + koly_block->data_fork_length - part->in_offset;
    gssize ret;

    /* Seek to the position */
    if (!mirage_stream_seek(stream, part_offs, G_SEEK_SET, NULL)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to seek to %" G_GOFFSET_MODIFIER "d in underlying stream!", __debug__, part_offs);
        return -1;
    }

    /*MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: raw position: %u", __debug__, part_offs);*/

    /* Read raw chunk data */
    ret = mirage_stream_read(stream, &buffer[have_read], MIN(to_read, part_avail), NULL);
    if (ret < 0) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read %" G_GSIZE_MODIFIER "d bytes from underlying stream!", __debug__, to_read);
        return -1;
    } else if (ret == 0) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unexpectedly reached EOF!", __debug__);
        return -1;
    } else if ((gsize)ret == to_read) {
        have_read += ret;
        to_read -= ret;
    } else if ((gsize)ret < to_read) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: reading remaining data!", __debug__);
        have_read += ret;
        to_read -= ret;

        koly_block = &self->priv->koly_blocks[part->segment + 1];
        stream = self->priv->streams[part->segment + 1];
        part_offs = koly_block->data_fork_offset;

        /* Seek to the position */
        if (!mirage_stream_seek(stream, part_offs, G_SEEK_SET, NULL)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to seek to %" G_GOFFSET_MODIFIER "d in underlying stream!", __debug__, part_offs);
            return -1;
        }

        /* Read raw chunk data */
        ret = mirage_stream_read(stream, &buffer[have_read], to_read, NULL);
        if (ret < 0) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read %" G_GSIZE_MODIFIER "d bytes from underlying stream!", __debug__, to_read);
            return -1;
        } else if (ret == 0) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unexpectedly reached EOF!", __debug__);
            return -1;
        } else if ((gsize)ret == to_read) {
            have_read += ret;
            to_read -= ret;
        }
    }

    g_assert(to_read == 0 && have_read == part->in_length);

    return have_read;
}

static gssize mirage_filter_stream_dmg_partial_read (MirageFilterStream *_self, void *buffer, gsize count)
{
    MirageFilterStreamDmg *self = MIRAGE_FILTER_STREAM_DMG(_self);
    goffset position = mirage_filter_stream_simplified_get_position(MIRAGE_FILTER_STREAM(self));
    gint part_idx = -1;

    /* Find part that corresponds to current position */
    for (guint p = 0; p < self->priv->num_parts; p++) {
        const DMG_Part *cur_part = &self->priv->parts[p];
        guint req_sector = position / DMG_SECTOR_SIZE;

        if ((cur_part->first_sector <= req_sector) && (cur_part->first_sector + cur_part->num_sectors >= req_sector)) {
            part_idx = p;
        }
    }

    if (part_idx == -1) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: failed to find part!", __debug__);
        return 0;
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: stream position: %" G_GOFFSET_MODIFIER "d (0x%" G_GOFFSET_MODIFIER "X) -> part #%d (cached: #%d)", __debug__, position, position, part_idx, self->priv->cached_part);

    /* If we do not have part in cache, uncompress it */
    if (part_idx != self->priv->cached_part) {
        const DMG_Part *part = &self->priv->parts[part_idx];

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: part not cached, reading...", __debug__);

        /* Read a part */
        if (part->type == ZERO || part->type == IGNORE) {
            /* We don't use internal buffers for zero data */
        } else if (part->type == RAW) {
            /* Read uncompressed part */
            gssize ret = mirage_filter_stream_dmg_read_raw_chunk (self, self->priv->inflate_buffer, part_idx);
            if ((gsize)ret != part->in_length) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read raw chunk!", __debug__);
                return -1;
            }
        } else if (part->type == ZLIB) {
            z_stream *zlib_stream = &self->priv->zlib_stream;
            int zlib_ret;
            gssize read_bytes;

            /* Reset inflate engine */
            zlib_ret = inflateReset2(zlib_stream, 15);
            if (zlib_ret != Z_OK) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to reset inflate engine!", __debug__);
                return -1;
            }

            /* Uncompress whole part */
            zlib_stream->avail_in = part->in_length;
            zlib_stream->next_in = self->priv->io_buffer;
            zlib_stream->avail_out = self->priv->inflate_buffer_size;
            zlib_stream->next_out = self->priv->inflate_buffer;

            /* Read some compressed data */
            read_bytes = mirage_filter_stream_dmg_read_raw_chunk (self, self->priv->io_buffer, part_idx);
            if ((gsize)read_bytes != part->in_length) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read raw chunk!", __debug__);
                return -1;
            }

            do {
                /* Inflate */
                zlib_ret = inflate(zlib_stream, Z_NO_FLUSH);
                if (zlib_ret == Z_NEED_DICT || zlib_ret == Z_MEM_ERROR || zlib_ret == Z_DATA_ERROR) {
                    MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to inflate part: %s!", __debug__, zlib_stream->msg);
                    return -1;
                }
            } while (zlib_stream->avail_in);
        } else if (part->type == BZLIB) {
            bz_stream *bzip2_stream = &self->priv->bzip2_stream;
            int bz_ret;
            gssize read_bytes;

            /* Reset decompress engine */
            bz_ret = BZ2_bzDecompressInit(bzip2_stream, 0, 0);
            if (bz_ret != BZ_OK) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to initialize decompress engine!", __debug__);
                return -1;
            }

            /* Uncompress whole part */
            bzip2_stream->avail_in = part->in_length;
            bzip2_stream->next_in = (gchar *)self->priv->io_buffer;
            bzip2_stream->avail_out = self->priv->inflate_buffer_size;
            bzip2_stream->next_out = (gchar *)self->priv->inflate_buffer;

            /* Read some compressed data */
            read_bytes = mirage_filter_stream_dmg_read_raw_chunk (self, self->priv->io_buffer, part_idx);
            if ((gsize)read_bytes != part->in_length) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read raw chunk!", __debug__);
                return -1;
            }

            do {
                /* Inflate */
                bz_ret = BZ2_bzDecompress(bzip2_stream);
                if (bz_ret < 0) {
                    MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to inflate part: %d!", __debug__, bz_ret);
                    return -1;
                }
            } while (bzip2_stream->avail_in);

            /* Uninitialize decompress engine */
            bz_ret = BZ2_bzDecompressEnd(bzip2_stream);
            if (bz_ret != BZ_OK) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to uninitialize decompress engine!", __debug__);
                return -1;
            }
        } else if (part->type == ADC) {
            gsize read_bytes;
            gsize written_bytes;
            gsize ret;

            /* Read some compressed data */
            read_bytes = mirage_filter_stream_dmg_read_raw_chunk(self, self->priv->io_buffer, part_idx);
            if ((gsize)read_bytes != part->in_length) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read raw chunk!", __debug__);
                return -1;
            }

            /* Inflate */
            ret = adc_decompress(part->in_length, self->priv->io_buffer, part->num_sectors * DMG_SECTOR_SIZE, self->priv->inflate_buffer, &written_bytes);

            g_assert(ret == part->in_length);
            g_assert(written_bytes == part->num_sectors * DMG_SECTOR_SIZE);
        } else {
            /* We should never get here... */
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: Encountered unknown chunk type %u!", __debug__, part->type);
            return -1;
        }

        /* Set currently cached part */
        if (part->type != ZERO && part->type != IGNORE) {
            self->priv->cached_part = part_idx;
        }
    } else {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: part already cached", __debug__);
    }

    /* Copy data */
    const DMG_Part *part = &self->priv->parts[part_idx];

    gsize part_size = part->num_sectors * DMG_SECTOR_SIZE;
    guint64 part_offset = position - (part->first_sector * DMG_SECTOR_SIZE);
    count = MIN(count, part_size - part_offset);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: offset within part: %" G_GINT64_MODIFIER "d, copying %" G_GSIZE_MODIFIER "d bytes", __debug__, part_offset, count);

    if (part->type == ZERO || part->type == IGNORE) {
        memset(buffer, 0, count);
    } else {
        memcpy(buffer, &self->priv->inflate_buffer[part_offset], count);
    }

    return count;
}


/**********************************************************************\
 *                             Object init                            *
\**********************************************************************/
static void mirage_filter_stream_dmg_init (MirageFilterStreamDmg *self)
{
    self->priv = mirage_filter_stream_dmg_get_instance_private(self);

    mirage_filter_stream_generate_info(MIRAGE_FILTER_STREAM(self),
        "FILTER-DMG",
        Q_("DMG File Filter"),
        FALSE,
        1,
        Q_("Apple Disk Image (*.dmg)"), "application/x-apple-diskimage"
    );

    self->priv->koly_blocks = NULL;

    self->priv->streams = NULL;

    self->priv->rsrc_fork = NULL;

    self->priv->num_segments = 0;

    self->priv->num_parts = 0;
    self->priv->parts = NULL;

    self->priv->cached_part = -1;
    self->priv->inflate_buffer = NULL;
    self->priv->io_buffer = NULL;

    self->priv->create_filename_func = NULL;
}

static void mirage_filter_stream_dmg_finalize (GObject *gobject)
{
    MirageFilterStreamDmg *self = MIRAGE_FILTER_STREAM_DMG(gobject);

    if (self->priv->streams) {
        for (guint s = 0; s < self->priv->num_segments; s++) {
            MirageStream *stream = self->priv->streams[s];
            if (stream) {
                g_object_unref(stream);
            }
        }
        g_free(self->priv->streams);
    }

    g_free(self->priv->parts);
    g_free(self->priv->inflate_buffer);
    g_free(self->priv->io_buffer);

    inflateEnd(&self->priv->zlib_stream);
    BZ2_bzDecompressEnd(&self->priv->bzip2_stream);

    g_free(self->priv->koly_blocks);

    rsrc_fork_free(self->priv->rsrc_fork);

    /* Chain up to the parent class */
    G_OBJECT_CLASS(mirage_filter_stream_dmg_parent_class)->finalize(gobject);
}

static void mirage_filter_stream_dmg_class_init (MirageFilterStreamDmgClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    MirageFilterStreamClass *filter_stream_class = MIRAGE_FILTER_STREAM_CLASS(klass);

    gobject_class->finalize = mirage_filter_stream_dmg_finalize;

    filter_stream_class->open = mirage_filter_stream_dmg_open;

    filter_stream_class->simplified_partial_read = mirage_filter_stream_dmg_partial_read;
}

static void mirage_filter_stream_dmg_class_finalize (MirageFilterStreamDmgClass *klass G_GNUC_UNUSED)
{
}
