/*
 *  libMirage: DAA filter: filter stream
 *  Copyright (C) 2008-2026 Rok Mandeljc
 *
 *  Derived from code of GPLed utility daa2iso, written by Luigi Auriemma:
 *  http://aluigi.altervista.org/mytoolz.htm
 *
 *  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-daa.h"

#include <zlib.h>

#include "LzmaDec.h"
#include "Bra.h"

#define __debug__ "DAA-FilterStream"


/* Signatures */
const gchar daa_main_signature[16] = "DAA";
const gchar daa_part_signature[16] = "DAA VOL";

const gchar gbi_main_signature[16] = "GBI";
const gchar gbi_part_signature[16] = "GBI VOL";


/**********************************************************************\
 *                  Object and its private structure                  *
\**********************************************************************/
typedef enum
{
    COMPRESSION_NONE = 0x00,
    COMPRESSION_ZLIB = 0x10,
    COMPRESSION_LZMA = 0x20,
} CompressionType;

typedef enum
{
    IMAGE_DAA = 0x00,
    IMAGE_GBI = 0x01,
} ImageType;

typedef struct
{
    guint64 offset;
    guint32 length;
    CompressionType compression;
} DAA_Chunk;

typedef struct
{
    MirageStream *stream;
    guint64 offset;
    guint64 start;
    guint64 end;
} DAA_Part;

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


struct _MirageFilterStreamDaaPrivate
{
    /* Filename components */
    const gchar *main_filename;
    DAA_create_filename_func create_filename_func;

    /* Header */
    DAA_MainHeader header;

    ImageType image_type;

    gboolean compressed_chunk_table;
    gboolean obfuscated_chunk_table;
    gboolean obfuscated_bits;
    gint bit_swap_type;

    gint chunk_table_offset;
    gint chunk_data_offset;
    guint chunk_size;

    /* Bitsizes of bit-packed chunk table (format version 2) */
    gint bitsize_type;
    gint bitsize_length;

    /* Chunks table */
    gint num_chunks;
    DAA_Chunk *chunk_table;

    /* Parts table */
    gint num_parts;
    DAA_Part *part_table;

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

    /* Inflate buffer */
    guint8 *inflate_buffer;
    gint inflate_buffer_size;
    gint cached_chunk; /* Index of currently cached chunk */
    gsize cached_chunk_size;

    /* Compression */
    z_stream zlib_stream;

    CLzmaDec lzma_decoder;

    /* Encryption */
    gboolean encrypted;
    guint8 decryption_table[128][256];
};


G_DEFINE_DYNAMIC_TYPE_EXTENDED(
    MirageFilterStreamDaa,
    mirage_filter_stream_daa,
    MIRAGE_TYPE_FILTER_STREAM,
    0,
    G_ADD_PRIVATE_DYNAMIC(MirageFilterStreamDaa)
)

void mirage_filter_stream_daa_type_register (GTypeModule *type_module)
{
    mirage_filter_stream_daa_register_type(type_module);
}


/**********************************************************************\
 *                  Memory allocator for LZMA decoder                 *
\**********************************************************************/
static void *lzma_alloc (void *p G_GNUC_UNUSED, size_t size)
{
    return g_malloc0(size);
}

static void lzma_free (void *p G_GNUC_UNUSED, void *address)
{
    g_free(address);
}

static ISzAlloc lzma_allocator = {
    lzma_alloc,
    lzma_free
};


/**********************************************************************\
 *                    Endian-conversion functions                     *
\**********************************************************************/
static inline void daa_format2_header_fix_endian (DAA_Format2Header *header)
{
    header->chunk_table_compressed = GUINT32_FROM_LE(header->chunk_table_compressed);
}

static inline void daa_main_header_fix_endian (DAA_MainHeader *header)
{
    header->chunk_table_offset = GUINT32_FROM_LE(header->chunk_table_offset);
    header->format_version = GUINT32_FROM_LE(header->format_version);
    header->chunk_data_offset = GUINT32_FROM_LE(header->chunk_data_offset);
    header->__dummy__1 = GUINT32_FROM_LE(header->__dummy__1);
    header->__dummy__2 = GUINT32_FROM_LE(header->__dummy__2);
    header->chunk_size = GUINT32_FROM_LE(header->chunk_size);
    header->iso_size = GUINT64_FROM_LE(header->iso_size);
    header->daa_size = GUINT64_FROM_LE(header->daa_size);
    daa_format2_header_fix_endian(&header->format2);
    header->crc = GUINT32_FROM_LE(header->crc);
}

static inline void daa_part_header_fix_endian (DAA_PartHeader *header)
{
    header->chunk_data_offset = GUINT32_FROM_LE(header->chunk_data_offset);
    daa_format2_header_fix_endian(&header->format2);
    header->crc = GUINT32_FROM_LE(header->crc);
}

static inline void daa_descriptor_header_fix_endian (DAA_DescriptorHeader *header)
{
    header->type = GUINT32_FROM_LE(header->type);
    header->length = GUINT32_FROM_LE(header->length);
}

static inline void daa_descriptor_split_fix_endian (DAA_DescriptorSplit *data)
{
    data->num_parts = GUINT32_FROM_LE(data->num_parts);
    data->__dummy__ = GUINT32_FROM_LE(data->__dummy__);
}

static inline void daa_descriptor_encryption_fix_endian (DAA_DescriptorEncryption *data)
{
    data->encryption_type = GUINT32_FROM_LE(data->encryption_type);
    data->password_crc = GUINT32_FROM_LE(data->password_crc);
}


/**********************************************************************\
 *                     Part filename generation                       *
\**********************************************************************/
/* Format: volname.part01.daa, volname.part02.daa, ... */
static gchar *create_filename_func_1 (const gchar *main_filename, gint index)
{
    gchar *ret_filename = g_strdup(main_filename);

    if (index) {
        /* Find last occurrence of 01. and print index into it */
        gchar *position = g_strrstr(ret_filename, "01.");
        position += g_snprintf(position, 3, "%02i", index+1);
        *position = '.'; /* Since it got overwritten with terminating 0 */
    }

    return ret_filename;
}

/* Format: volname.part001.daa, volname.part002.daa, ... */
static gchar *create_filename_func_2 (const gchar *main_filename, gint index)
{
    gchar *ret_filename = g_strdup(main_filename);

    if (index) {
        /* Find last occurrence of 01. and print index+1 into it */
        gchar *position = g_strrstr(ret_filename, "001.");
        position += g_snprintf(position, 4, "%03i", index+1);
        *position = '.'; /* Since it got overwritten with terminating 0 */
    }

    return ret_filename;
}

/* Format: volname.daa, volname.d00, ... */
static gchar *create_filename_func_3 (const gchar *main_filename, gint index)
{
    gchar *ret_filename = g_strdup(main_filename);

    if (index) {
        /* Replace last two characters with index-1 */
        gchar *position = ret_filename + strlen(ret_filename) - 2;
        g_snprintf(position, 3, "%02i", index-1);
    }

    return ret_filename;
}


/**********************************************************************\
 *                  DAA decryption (Luigi Auriemma)                   *
\**********************************************************************/
static void mirage_filter_daa_create_decryption_table (MirageFilterStreamDaa *self, const gchar *pass, gint num)
{
    gint a, b, c, d, s, i, p;
    gint passlen;
    gshort tmp[256];
    guint8 *tab;

    passlen = strlen(pass);
    tab = self->priv->decryption_table[num - 1];
    d = num << 1;

    for (i = 0; i < 256; i++) {
        tmp[i] = i;
    }
    memset(tab, 0, 256);

    if (d <= 64) {
        a = pass[0] >> 5;
        if (a >= d) a = d - 1;
        for (c = 0; c < d; c++) {
            for (s = 0; s != 11;) {
                a++;
                if (a == d) a = 0;
                if (tmp[a] != -1) s++;
            }
            tab[c] = a;
            tmp[a] = -1;
        }
        return;
    }

    a = pass[0];
    b = d - 32;
    a >>= 5;
    tmp[a + 32] = -1;
    tab[0] = a + 32;
    p = 1;

    for (s = 1; s < b; s++) {
        c = 11;
        if (p < passlen) {
            c = pass[p];
            p++;
            if (!c) c = 11;
        }
        for (i = 0; i != c;) {
            a++;
            if (a == d) a = 32;
            if (tmp[a] != -1) i++;
        }
        tmp[a] = -1;
        tab[s] = a;
    }

    i = pass[0] & 7;
    if(!i) i = 7;

    for (; s < d; s++) {
        for (c = 0; c != i;) {
            a++;
            if (a == d) a = 0;
            if (tmp[a] != -1) c++;
        }
        tmp[a] = -1;
        tab[s] = a;
    }

    for (i = 0; i < d; i++) {
        tmp[i] = tab[i];
    }

    i = pass[0] & 24;
    if (i) {
        a = 0;
        for (s = 0; s < d; s++) {
            for (c = 0; c != i;) {
                a++;
                if(a == d) a = 0;
                if(tmp[a] != -1) c++;
            }
            c = tmp[a];
            tmp[a] = -1;
            tab[s] = c;
        }
    }
}

/* Decrypt block of specified size */
static void mirage_filter_daa_decrypt_block (MirageFilterStreamDaa *self, guint8 *ret, guint8 *data, gint size)
{
    guint8 c, t, *tab;

    if (!size) {
        return;
    }
    tab = self->priv->decryption_table[size - 1];

    memset(ret, 0, size);
    for (gint i = 0; i < size; i++) {
        c = data[i] & 15;
        t = tab[i << 1];
        if (t & 1) c <<= 4;
        ret[t >> 1] |= c;

        c = data[i] >> 4;
        t = tab[(i << 1) + 1];
        if (t & 1) c <<= 4;
        ret[t >> 1] |= c;
    }
}

static void mirage_filter_stream_daa_decrypt_buffer (MirageFilterStreamDaa *self, guint8 *data, gint size)
{
    gint blocks, rem;
    guint8 tmp[128];
    guint8 *p;

    blocks = size >> 7;
    for (p = data; blocks--; p += 128) {
        mirage_filter_daa_decrypt_block(self, tmp, p, 128);
        memcpy(p, tmp, 128);
    }

    rem = size & 127;
    if (rem) {
        mirage_filter_daa_decrypt_block(self, tmp, p, rem);
        memcpy(p, tmp, rem);
    }
}

static void mirage_filter_stream_daa_initialize_decryption (MirageFilterStreamDaa *self, guint8 *pwdkey, const gchar *password, guint8 *daakey)
{
    /* Create decryption table */
    for (gint i = 1; i <= 128; i++) {
        mirage_filter_daa_create_decryption_table(self, password, i);
    }

    /* Get password hash */
    mirage_filter_daa_decrypt_block(self, pwdkey, daakey, 128);
}


/**********************************************************************\
 *                            Compression                             *
\**********************************************************************/
static gboolean mirage_filter_stream_daa_initialize_zlib (MirageFilterStreamDaa *self, GError **error)
{
    z_stream *zlib_stream = &self->priv->zlib_stream;
    gint ret;

    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 decoder!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Failed to initialize zlib's inflate (error: %d)!"), ret);
        return FALSE;
    }

    return TRUE;
}

static gint mirage_filter_stream_daa_inflate_zlib (MirageFilterStreamDaa *self, guint8 *in_buf, gsize in_len)
{
    z_stream *zlib_stream = &self->priv->zlib_stream;
    gint ret;

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: decompressing using zlib; in_len: %" G_GSIZE_MODIFIER "d bytes", __debug__, in_len);
    inflateReset(zlib_stream);

    zlib_stream->next_in = in_buf;
    zlib_stream->avail_in = in_len;
    zlib_stream->next_out = self->priv->inflate_buffer;
    zlib_stream->avail_out = self->priv->inflate_buffer_size;

    ret = inflate(zlib_stream, Z_SYNC_FLUSH);
    if (ret != Z_STREAM_END) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to inflate (error code = %d)!", __debug__, ret);
        return 0;
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: inflated: %ld bytes, consumed: %ld bytes", __debug__, zlib_stream->total_out, zlib_stream->total_in);
    return zlib_stream->total_out;
}


static gboolean mirage_filter_stream_daa_initialize_lzma (MirageFilterStreamDaa *self, GError **error G_GNUC_UNUSED)
{
    LzmaDec_Construct(&self->priv->lzma_decoder);
    LzmaDec_Allocate(&self->priv->lzma_decoder, self->priv->header.format2.lzma_props, LZMA_PROPS_SIZE, &lzma_allocator);
    return TRUE;
}

static gint mirage_filter_stream_daa_inflate_lzma (MirageFilterStreamDaa *self, guint8 *in_buf, gsize in_len)
{
    ELzmaStatus status;
    SizeT inlen, outlen;

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: decompressing using LZMA; in_len: %" G_GSIZE_MODIFIER "d bytes", __debug__, in_len);

    /* Initialize decoder */
    LzmaDec_Init(&self->priv->lzma_decoder);

    /* LZMA */
    inlen = in_len;
    outlen = self->priv->inflate_buffer_size;
    if (LzmaDec_DecodeToBuf(&self->priv->lzma_decoder, self->priv->inflate_buffer, &outlen, in_buf, &inlen, LZMA_FINISH_END, &status) != SZ_OK) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to inflate (status: %d)!", __debug__, status);
        return 0;
    }

    /* Filter */
    switch (self->priv->header.format2.lzma_filter) {
        case 0: {
            /* No filter */
            break;
        }
        case 1: {
            /* x86 BCJ filter */
            guint32 state;
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: applying x86 BCJ filter to decompressed data", __debug__);
            x86_Convert_Init(state);
            x86_Convert(self->priv->inflate_buffer, outlen, 0, &state, 0);
            break;
        }
        default: {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unhandled LZMA filter type %d!", __debug__, self->priv->header.format2.lzma_filter);
            break;
        }
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: inflated: %" G_GSIZE_MODIFIER "d bytes, consumed: %" G_GSIZE_MODIFIER "d bytes", __debug__, outlen, inlen);
    return outlen;
}



/**********************************************************************\
 *                            Data accesss                            *
\**********************************************************************/
static gboolean mirage_filter_stream_daa_read_main_header (MirageFilterStreamDaa *self, MirageStream *stream, DAA_MainHeader *header, GError **error)
{
    guint32 crc;

    /* Seek to the beginning */
    mirage_stream_seek(stream, 0, G_SEEK_SET, NULL);

    /* Read main header */
    if (mirage_stream_read(stream, header, sizeof(DAA_MainHeader), NULL) != sizeof(DAA_MainHeader)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read main file's header!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read main file's header!"));
        return FALSE;
    }

    /* Compute CRC */
    crc = crc32(0, (guint8 *)header, sizeof(DAA_MainHeader) - 4);

    /* Fix endianess */
    daa_main_header_fix_endian(header);

    /* Debug */
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: Main file header:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - signature: %.16s", __debug__, header->signature);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - chunk_table_offset: 0x%X (%d)", __debug__, header->chunk_table_offset, header->chunk_table_offset);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - format_version: 0x%X", __debug__, header->format_version);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - chunk_data_offset: 0x%X (%d)", __debug__, header->chunk_data_offset, header->chunk_data_offset);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - __dummy__1: 0x%X", __debug__, header->__dummy__1);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - __dummy__2: 0x%X", __debug__, header->__dummy__2);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - chunk_size: 0x%X (%d)", __debug__, header->chunk_size, header->chunk_size);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - iso_size: 0x%" G_GINT64_MODIFIER "X (%" G_GINT64_MODIFIER "d)", __debug__, header->iso_size, header->iso_size);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - daa_size: 0x%" G_GINT64_MODIFIER "X (%" G_GINT64_MODIFIER "d)", __debug__, header->daa_size, header->daa_size);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - format2 header:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - profile: %d", __debug__, header->format2.profile);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - chunk_table_compressed: 0x%X (%d)", __debug__, header->format2.chunk_table_compressed, header->format2.chunk_table_compressed);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - chunk_table_bit_settings: 0x%X (%d)", __debug__, header->format2.chunk_table_bit_settings, header->format2.chunk_table_bit_settings);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - lzma_filter: %d", __debug__, header->format2.lzma_filter);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - lzma_props: %02X %02X %02X %02X %02X", __debug__, header->format2.lzma_props[0], header->format2.lzma_props[1], header->format2.lzma_props[2], header->format2.lzma_props[3], header->format2.lzma_props[4]);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - reserved: %02X %02X %02X %02X", __debug__, header->format2.reserved[0], header->format2.reserved[1], header->format2.reserved[2], header->format2.reserved[3]);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - crc: 0x%X (computed: 0x%X)", __debug__, header->crc, crc);

    if (crc != header->crc) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: CRC32 checksum mismatch!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("CRC32 checksum mismatch!"));
        return FALSE;
    }

    return TRUE;
}

static gboolean mirage_filter_stream_daa_read_part_header (MirageFilterStreamDaa *self, MirageStream *stream, DAA_PartHeader *header, GError **error)
{
    guint32 crc;

    /* Seek to the beginning */
    mirage_stream_seek(stream, 0, G_SEEK_SET, NULL);

    /* Read main header */
    if (mirage_stream_read(stream, header, sizeof(DAA_PartHeader), NULL) != sizeof(DAA_PartHeader)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read part file's header!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read part file's header!"));
        return FALSE;
    }

    /* Compute CRC */
    crc = crc32(0, (guint8 *)header, sizeof(DAA_PartHeader) - 4);

    /* Fix endianess */
    daa_part_header_fix_endian(header);

    /* Debug */
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: Part file header:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - signature: %.16s", __debug__, header->signature);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - chunk_data_offset: 0x%X (%d)", __debug__, header->chunk_data_offset, header->chunk_data_offset);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - format2 header:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - profile: %d", __debug__, header->format2.profile);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - chunk_table_compressed: 0x%X (%d)", __debug__, header->format2.chunk_table_compressed, header->format2.chunk_table_compressed);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - chunk_table_bit_settings: 0x%X (%d)", __debug__, header->format2.chunk_table_bit_settings, header->format2.chunk_table_bit_settings);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - lzma_filter: %d", __debug__, header->format2.lzma_filter);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - lzma_props: %02X %02X %02X %02X %02X", __debug__, header->format2.lzma_props[0], header->format2.lzma_props[1], header->format2.lzma_props[2], header->format2.lzma_props[3], header->format2.lzma_props[4]);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:       - reserved: %02X %02X %02X %02X", __debug__, header->format2.reserved[0], header->format2.reserved[1], header->format2.reserved[2], header->format2.reserved[3]);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:   - crc: 0x%X (computed: 0x%X)", __debug__, header->crc, crc);

    if (crc != header->crc) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: CRC32 checksum mismatch!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("CRC32 checksum mismatch!"));
        return FALSE;
    }

    return TRUE;
}

static gboolean mirage_filter_stream_daa_read_from_stream (MirageFilterStreamDaa *self, guint64 offset, guint32 length, guint8 *buffer, GError **error)
{
    /* A rather complex loop, thanks to the possibility that a chunk spans across
     * multiple part files... */
    while (length > 0) {
        guint64 local_offset, file_offset;
        guint32 read_length;
        DAA_Part *part = NULL;

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: reading 0x%X bytes from stream at offset 0x%" G_GINT64_MODIFIER "X", __debug__, length, offset);

        /* Find the part to which the given offset belongs */
        for (gint i = 0; i < self->priv->num_parts; i++) {
            if (offset >= self->priv->part_table[i].start && offset < self->priv->part_table[i].end) {
                part = &self->priv->part_table[i];
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: using part #%i", __debug__, i);
                break;
            }
        }
        if (!part) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to find part for offset 0x%" G_GINT64_MODIFIER "X!", __debug__, offset);

            gchar tmp[100] = ""; /* Work-around for lack of direct G_GINT64_MODIFIER support in xgettext() */
            g_snprintf(tmp, sizeof(tmp)/sizeof(tmp[0]), "0x%" G_GINT64_MODIFIER "X", offset);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_FRAGMENT_ERROR, Q_("Failed to find part for offset %s!"), tmp);

            return FALSE;
        }

        read_length = length;
        if (offset + length > part->end) {
            read_length = part->end - offset;
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: requested data range spanning across the range of this part; clipping read length to 0x%X bytes", __debug__, read_length);
        }

        local_offset = offset - part->start; /* Offset within part */
        file_offset = part->offset + local_offset; /* Actual offset within part file */

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: local offset: 0x%" G_GINT64_MODIFIER "X", __debug__, local_offset);
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: file offset: 0x%" G_GINT64_MODIFIER "X", __debug__, file_offset);

        if (!mirage_stream_seek(part->stream, file_offset, G_SEEK_SET, NULL)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to seek to 0x%" G_GINT64_MODIFIER "X", __debug__, file_offset);

            gchar tmp[100] = ""; /* Work-around for lack of direct G_GINT64_MODIFIER support in xgettext() */
            g_snprintf(tmp, sizeof(tmp)/sizeof(tmp[0]), "0x%" G_GINT64_MODIFIER "X", file_offset);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_FRAGMENT_ERROR, Q_("Failed to seek to %s!"), tmp);

            return FALSE;
        }

        if ((gsize)mirage_stream_read(part->stream, buffer, read_length, NULL) != read_length) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read 0x%X bytes!", __debug__, read_length);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_FRAGMENT_ERROR, Q_("Failed to read 0x%X bytes!"), read_length);
            return FALSE;
        }

        /* Update length and offset */
        length -= read_length;
        offset += read_length;
        buffer += read_length;
    }

    return TRUE;
}

/**********************************************************************\
 *                         Descriptor parsing                         *
\**********************************************************************/
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;
}

static gboolean mirage_filter_stream_daa_parse_descriptor_split (MirageFilterStreamDaa *self, gint descriptor_size, GError **error)
{
    MirageStream *stream = mirage_filter_stream_get_underlying_stream(MIRAGE_FILTER_STREAM(self));
    DAA_DescriptorSplit descriptor;
    const gchar *filename;

    /* First field is number of parts (files) */
    if (mirage_stream_read(stream, &descriptor, sizeof(descriptor), NULL) != sizeof(descriptor)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read descriptor data!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read descriptor data!"));
        return FALSE;
    }
    daa_descriptor_split_fix_endian(&descriptor);
    descriptor_size -= sizeof(descriptor);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: number of parts: %d", __debug__, descriptor.num_parts);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: unknown field: %d (should always be 1)", __debug__, descriptor.__dummy__);

    self->priv->num_parts = descriptor.num_parts; /* Set number of parts */

    /* The descriptor data seems to consists of a fixed-number of 5-byte
     * fields, in which part sizes are stored. We don't really need these,
     * as we can get same info from part descriptor of each part file.
     * In older versions of DAA, the number of entries (99, 101, 512)
     * could be used to infer the naming scheme; but contemporary images
     * seem to have significantly larger descriptor. Therefore, try to
     * determine the naming scheme from the filename. */
    filename = mirage_stream_get_filename(stream);

    if (check_filename_suffix(filename, ".part001.daa") || check_filename_suffix(filename, ".part001.gbi")) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: filename format: volname.part001.daa, volname.part002.daa, ...", __debug__);
        self->priv->create_filename_func = create_filename_func_2;
    } else if (check_filename_suffix(filename, ".part01.daa") || check_filename_suffix(filename, ".part01.gbi")) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: filename format: volname.part01.daa, volname.part02.daa, ...", __debug__);
        self->priv->create_filename_func = create_filename_func_1;
    } else if (check_filename_suffix(filename, ".daa") || check_filename_suffix(filename, ".gbi")) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: filename format: volname.daa, volname.d00, ...", __debug__);
        self->priv->create_filename_func = create_filename_func_3;
    } else {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to determine part naming scheme!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to determine part naming scheme!"));
        return FALSE;
    }

    mirage_stream_seek(stream, descriptor_size, G_SEEK_CUR, NULL);

    return TRUE;
}

static gboolean mirage_filter_stream_daa_parse_descriptor_encryption (MirageFilterStreamDaa *self, gint descriptor_size, GError **error)
{
    MirageStream *stream = mirage_filter_stream_get_underlying_stream(MIRAGE_FILTER_STREAM(self));
    DAA_DescriptorEncryption descriptor;
    guint8 computed_key[128];

    /* Validate descriptor size */
    if (descriptor_size != sizeof(descriptor)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: invalid size for encryption descriptor (%d vs %" G_GSIZE_MODIFIER "d)!", __debug__, descriptor_size, sizeof(descriptor));
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Invalid size for encryption descriptor!"));
        return FALSE;
    }

    /* Read descriptor data */
    if (mirage_stream_read(stream, &descriptor, sizeof(descriptor), NULL) != sizeof(descriptor)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read descriptor data!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read descriptor data!"));
        return FALSE;
    }
    daa_descriptor_encryption_fix_endian(&descriptor);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: encryption type: 0x%X", __debug__, descriptor.encryption_type);
    if (descriptor.encryption_type != 0) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: type of encryption 0x%d might not be supported!", __debug__, descriptor.encryption_type);
    }


    /* First, check if password has already been provided via context options
     * (separate code paths because if acquired via password function, the string
     * must be freed) */
    GVariant *password_value = mirage_contextual_get_option(MIRAGE_CONTEXTUAL(self), "password");
    if (password_value) {
        mirage_filter_stream_daa_initialize_decryption(self, computed_key, g_variant_get_string(password_value, NULL), descriptor.daa_key);
        g_variant_unref(password_value);
    } else {
        /* Get password from user via password function */
        gchar *prompt_password = mirage_contextual_obtain_password(MIRAGE_CONTEXTUAL(self), NULL);
        if (!prompt_password) {
            /* Password not provided (or password function is not set) */
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  failed to obtain password for encrypted image!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_ENCRYPTED_IMAGE, Q_("Image is encrypted!"));
            return FALSE;
        }

        mirage_filter_stream_daa_initialize_decryption(self, computed_key, prompt_password, descriptor.daa_key);
        g_free(prompt_password);
    }


    /* Check if password is correct */
    if (descriptor.password_crc != crc32(0, computed_key, 128)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  incorrect password!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Incorrect password!"));
        return FALSE;
    }


    /* Set encrypted flag - used later, when reading data */
    self->priv->encrypted = TRUE;

    return TRUE;
}

static gboolean mirage_filter_stream_daa_parse_descriptors (MirageFilterStreamDaa *self, GError **error)
{
    MirageStream *stream = mirage_filter_stream_get_underlying_stream(MIRAGE_FILTER_STREAM(self));

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing descriptors (stream position: 0x%" G_GOFFSET_MODIFIER "X)", __debug__, mirage_stream_tell(stream));

    /* Set number of parts to 1 (true for non-split images); if image consists
     * of multiple parts, this will be set accordingly by the code below */
    self->priv->num_parts = 1;

    /* Parse descriptors... they are located between header and chunk table */
    while (mirage_stream_tell(stream) < self->priv->chunk_table_offset) {
        DAA_DescriptorHeader descriptor_header;

        /* Read descriptor header */
        if (mirage_stream_read(stream, &descriptor_header, sizeof(descriptor_header), NULL) != sizeof(descriptor_header)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read descriptor header!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read descriptor type!"));
            return FALSE;
        }

        /* Fix endianess */
        daa_descriptor_header_fix_endian(&descriptor_header);

        /* Length includes type and length fields... */
        descriptor_header.length -= 2*sizeof(guint32);

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: descriptor #%i (data length: %d):", __debug__, descriptor_header.type, descriptor_header.length);

        switch (descriptor_header.type) {
            case DESCRIPTOR_PART: {
                /* Part information; we skip it here, as it will be parsed by another function */
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: -> part information; skipping", __debug__);
                mirage_stream_seek(stream, descriptor_header.length, G_SEEK_CUR, NULL);
                break;
            }
            case DESCRIPTOR_SPLIT: {
                /* Split archive information */
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: -> split archive information", __debug__);
                if (!mirage_filter_stream_daa_parse_descriptor_split(self, descriptor_header.length, error)) {
                    return FALSE;
                }
                break;
            }
            case DESCRIPTOR_ENCRYPTION: {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: -> encryption information", __debug__);
                if (!mirage_filter_stream_daa_parse_descriptor_encryption(self, descriptor_header.length, error)) {
                    return FALSE;
                }
                break;
            }
            case DESCRIPTOR_COMMENT: /* FIXME */
            default: {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: -> unhandled block type 0x%X; skipping", __debug__, descriptor_header.type);
                mirage_stream_seek(stream, descriptor_header.length, G_SEEK_CUR, NULL);
                break;
            }
        }
    }

    return TRUE;
}


/**********************************************************************\
 *                         Chunk table parsing                        *
\**********************************************************************/
static void deobfuscate_chunk_table_gbi (guint8 *data, gint size, guint8 crc8)
{
    guint8 d = size / 4;
    for (gint i = 0; i < size; i++) {
        data[i] -= crc8;
        data[i] ^= d;
    }
}

static void deobfuscate_chunk_table_daa (guint8 *data, gint size, guint64 iso_size)
{
    guint8 a, c;

    iso_size /= 2048;
    a = (iso_size >> 8) & 0xff;
    c = iso_size & 0xff;
    for (gint i = 0; i < size; i++) {
        data[i] -= c;
        c += a;
    }
}

static inline guint read_bits (guint bits, guint8 *in, guint in_bits, gboolean bits_obfuscated, gint *bits_obfuscation_counter, gboolean increment_counter)
{
    static const guint8 obfuscation_mask[] = {0x0A, 0x35, 0x2D, 0x3F, 0x08, 0x33, 0x09, 0x15};
    guint seek_bits;
    guint rem;
    guint seek = 0;
    guint ret = 0;
    guint mask = 0xFFFFFFFF;

    if (bits > 32) {
        return 0;
    }

    if (bits < 32) {
        mask = (1 << bits) - 1;
    }

    for (;;) {
        seek_bits = in_bits & 7;
        ret |= ((in[in_bits >> 3] >> seek_bits)) << seek;
        rem = 8 - seek_bits;
        if (rem >= bits) {
            break;
        }
        bits -= rem;
        in_bits += rem;
        seek += rem;
    }

    if (bits_obfuscated) {
        gint counter = *bits_obfuscation_counter;
        ret ^= ((counter ^ obfuscation_mask[counter & 7]) & 0xff) * 0x01010101;
        if (increment_counter) {
            *bits_obfuscation_counter = counter++;
        }
    }

    return ret & mask;
}

static gboolean mirage_filter_stream_daa_parse_chunk_table (MirageFilterStreamDaa *self, GError **error)
{
    MirageStream *stream = mirage_filter_stream_get_underlying_stream(MIRAGE_FILTER_STREAM(self));
    guint8 *tmp_chunks_data;
    gint tmp_chunks_len;
    gint num_chunks = 0;
    guint64 tmp_offset = 0;

    gint bit_pos = 0; /* Bit position */
    gint bit_obfuscation_counter = 0; /* Bit obfuscation counter */

    guint32 max_chunk_size = 0;

    const gint format_version = self->priv->header.format_version;

    /* Compute chunk table size */
    tmp_chunks_len = self->priv->chunk_data_offset - self->priv->chunk_table_offset;
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing chunk table (length: %d bytes, from 0x%X to 0x%X)...", __debug__, tmp_chunks_len, self->priv->chunk_table_offset, self->priv->chunk_data_offset);

    /* Sanity check */
    if (tmp_chunks_len <= 0) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: sanity check failed: tmp_chunks_len (%d) should be greater than 0, but is not!", __debug__, tmp_chunks_len);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Sanity check failed!"));
        return FALSE;
    }

    /* Allocate temporary buffer */
    tmp_chunks_data = g_try_new(guint8, tmp_chunks_len);
    if (!tmp_chunks_data) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate chunk table buffer (%d bytes)!", __debug__, tmp_chunks_len);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Failed to allocate chunk table buffer (%d bytes)!"), tmp_chunks_len);
        return FALSE;
    }

    /* Read chunk data */
    mirage_stream_seek(stream, self->priv->chunk_table_offset, G_SEEK_SET, NULL);
    if (mirage_stream_read(stream, tmp_chunks_data, tmp_chunks_len, NULL) != tmp_chunks_len) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read chunk table data!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read chunk table data!"));
        return FALSE;
    }

    /* De-obfuscate chunk table data */
    if (self->priv->image_type == IMAGE_GBI) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: applying GBI-specific chunk table deobfuscation...", __debug__);
        deobfuscate_chunk_table_gbi(tmp_chunks_data, tmp_chunks_len, self->priv->header.crc & 0xff);
    } else if (self->priv->obfuscated_chunk_table) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: applying DAA-specific chunk table deobfuscation...", __debug__);
        deobfuscate_chunk_table_daa(tmp_chunks_data, tmp_chunks_len, self->priv->header.iso_size);
    }

    /* Compute number of chunks */
    switch (format_version) {
        case FORMAT_VERSION1: {
            /* 3-byte fields */
            num_chunks = tmp_chunks_len / 3;
            break;
        }
        case FORMAT_VERSION2: {
            /* tmp_chunks_len bytes * 8 bits, over bit size of type and length fields */
            num_chunks = (tmp_chunks_len * 8) / (self->priv->bitsize_type + self->priv->bitsize_length);
            break;
        }
    }

    /* Sanity check */
    if (num_chunks <= 0) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: sanity check failed: num_chunks (%d) should be greater than 0, but is not!", __debug__, num_chunks);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Sanity check failed!"));
        return FALSE;
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: chunk table: %d entries", __debug__, num_chunks);

    /* Allocate chunk table */
    self->priv->num_chunks = num_chunks;
    self->priv->chunk_table = g_try_new(DAA_Chunk, self->priv->num_chunks);
    if (!self->priv->chunk_table) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate chunk table (%" G_GSIZE_MODIFIER "d bytes)!", __debug__, self->priv->num_chunks*sizeof(DAA_Chunk));

        gchar tmp[100] = ""; /* Work-around for lack of direct G_GSIZE_MODIFIER support in xgettext() */
        g_snprintf(tmp, sizeof(tmp)/sizeof(tmp[0]), "%" G_GSIZE_MODIFIER "d", self->priv->num_chunks*sizeof(DAA_Chunk));
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Failed to allocate chunk table (%s bytes)!"), tmp);

        return FALSE;
    }

    for (gint i = 0; i < num_chunks; i++) {
        DAA_Chunk *chunk = &self->priv->chunk_table[i];

        guint32 tmp_chunk_length = 0;
        gint tmp_compression_type = -1;
        gchar *compression_type_string = "unknown";

        switch (format_version) {
            case FORMAT_VERSION1: {
                gint off = i*3;
                tmp_chunk_length = (tmp_chunks_data[off+0] << 16) | (tmp_chunks_data[off+2] << 8) | tmp_chunks_data[off+1];
                tmp_compression_type = 1;
                break;
            }
            case FORMAT_VERSION2: {
                /* In version 2, chunk table is bit-packed */
                tmp_chunk_length = read_bits(self->priv->bitsize_length, tmp_chunks_data, bit_pos, self->priv->obfuscated_bits, &bit_obfuscation_counter, FALSE);
                bit_pos += self->priv->bitsize_length;

                tmp_chunk_length += LZMA_PROPS_SIZE; /* LZMA props size */

                tmp_compression_type = read_bits(self->priv->bitsize_type, tmp_chunks_data, bit_pos, self->priv->obfuscated_bits, &bit_obfuscation_counter, TRUE);
                bit_pos += self->priv->bitsize_type;

                /* Detect uncompressed chunk */
                if (tmp_chunk_length >= self->priv->chunk_size) {
                    tmp_compression_type = -1;
                }
                break;
            }
        }

        /* Chunk compression type; format 0x100 uses zlib, while format 0x110
         * can use either only LZMA or combination of zlib and LZMA */
        switch (tmp_compression_type) {
            case -1: {
                tmp_compression_type = COMPRESSION_NONE;
                compression_type_string = "NONE";
                break;
            }
            case 0: {
                tmp_compression_type = COMPRESSION_LZMA;
                compression_type_string = "LZMA";
                break;
            }
            case 1: {
                tmp_compression_type = COMPRESSION_ZLIB;
                compression_type_string = "ZLIB";
                break;
            }
            default: {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unknown compression type %d!", __debug__, tmp_compression_type);
            }
        }

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  entry #%i: offset 0x%" G_GINT64_MODIFIER "X, length: 0x%X, compression: %s", __debug__, i, tmp_offset, tmp_chunk_length, compression_type_string);

        chunk->offset = tmp_offset;
        chunk->length = tmp_chunk_length;
        chunk->compression = tmp_compression_type;

        tmp_offset += tmp_chunk_length;

        max_chunk_size = MAX(max_chunk_size, chunk->length);
    }

    g_free(tmp_chunks_data);

    /* Allocate I/O buffer */
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: max chunk size: %d (0x%X)", __debug__, max_chunk_size, max_chunk_size);
    self->priv->io_buffer_size = max_chunk_size;
    self->priv->io_buffer = g_try_malloc(self->priv->io_buffer_size);
    if (!self->priv->io_buffer) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: faled to allocate I/O buffer (%d bytes)!", __debug__, self->priv->io_buffer_size);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Failed to allocate I/O buffer size (%d bytes)!"), self->priv->io_buffer_size);
        return FALSE;
    }

    return TRUE;
}


/**********************************************************************\
 *                       Part table construction                      *
\**********************************************************************/
static gboolean mirage_filter_stream_daa_build_part_table (MirageFilterStreamDaa *self, GError **error)
{
    guint64 tmp_offset = 0;
    guint64 part_length;
    guint64 tmp_position;
    DAA_Part *part;
    gchar *part_filename;
    gchar part_signature[16];

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: building parts table (%d entries)...", __debug__, self->priv->num_parts);

    /* Allocate part table */
    self->priv->part_table = g_try_new0(DAA_Part, self->priv->num_parts);
    if (!self->priv->part_table) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: faled to allocate part table (%" G_GSIZE_MODIFIER "d bytes)!", __debug__, self->priv->num_parts*sizeof(DAA_Part));
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Failed to allocate part table!"));
        return FALSE;
    }

    /* First part is the DAA file we are parsing */
    part = &self->priv->part_table[0];

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: creating part for main file", __debug__);

    part->stream = g_object_ref(mirage_filter_stream_get_underlying_stream(MIRAGE_FILTER_STREAM(self)));

    part->offset = self->priv->chunk_data_offset;
    part->start = tmp_offset;

    tmp_position = mirage_stream_tell(part->stream); /* Store current position */
    mirage_stream_seek(part->stream, 0, G_SEEK_END, NULL);
    part_length = mirage_stream_tell(part->stream);
    mirage_stream_seek(part->stream, tmp_position, G_SEEK_SET, NULL); /* Restore position */

    part_length -= part->offset;

    part->start = tmp_offset;
    tmp_offset += part_length;
    part->end = tmp_offset;


    /* Add the rest of the parts */
    for (gint i = 1; i < self->priv->num_parts; i++) {
        part = &self->priv->part_table[i];

        /* If we have create_filename_func set, use it... otherwise we're a
         * non-split image and should be using self->priv->main_filename anyway */
        if (self->priv->create_filename_func) {
            part_filename = self->priv->create_filename_func(self->priv->main_filename, i);
        } else {
            part_filename = g_strdup(self->priv->main_filename);
        }

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: part #%i: %s", __debug__, i, part_filename);

        /* Create stream */
        part->stream = mirage_contextual_create_input_stream(MIRAGE_CONTEXTUAL(self), part_filename, error);
        if (!part->stream) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to open stream on file '%s'!", __debug__, part_filename);
            g_free(part_filename);
            return FALSE;
        }
        g_free(part_filename);

        /* Read signature */
        if (mirage_stream_read(part->stream, part_signature, sizeof(part_signature), NULL) != sizeof(part_signature)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read part's signature!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_DATA_FILE_ERROR, Q_("Failed to read part's signature!"));
            return FALSE;
        }

        /* Read header */
        if (!memcmp(part_signature, daa_part_signature, sizeof(daa_part_signature))
            || !memcmp(part_signature, gbi_part_signature, sizeof(gbi_part_signature))) {
            DAA_PartHeader part_header;
            if (!mirage_filter_stream_daa_read_part_header(self, part->stream, &part_header, error)) {
                return FALSE;
            }
            part->offset = part_header.chunk_data_offset & 0x00FFFFFF;
        } else {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: invalid part's signature!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Part's signature is invalid!"));
            return FALSE;
        }

        /* We could parse part descriptor here; it's present in both main and part
         * files, and it has same format. It appears to contain previous
         * and current part's index, and the length of current part (with some
         * other fields in between). However, those part lengths are literal part
         * files' lengths, and we actually need the lengths of zipped streams
         * they contain. So we'll calculate that ourselves and leave part descriptor
         * alone... Part indices aren't of any use to us either, because I
         * haven't seen any DAA image having them mixed up... */
        mirage_stream_seek(part->stream, 0, G_SEEK_END, NULL);
        part_length = mirage_stream_tell(part->stream);

        part_length -= part->offset;

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: part length: 0x%" G_GINT64_MODIFIER "X", __debug__, part_length);

        part->start = tmp_offset;
        tmp_offset += part_length;
        part->end = tmp_offset;
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: part start: 0x%" G_GINT64_MODIFIER "X", __debug__, part->start);
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: part end: 0x%" G_GINT64_MODIFIER "X", __debug__, part->end);
    }

    return TRUE;
}


/**********************************************************************\
 *                        DAA file parsing                            *
\**********************************************************************/
static gboolean mirage_filter_stream_daa_parse_daa_file (MirageFilterStreamDaa *self, GError **error)
{
    MirageStream *stream = mirage_filter_stream_get_underlying_stream(MIRAGE_FILTER_STREAM(self));

    /* Read main header */
    if (!mirage_filter_stream_daa_read_main_header(self, stream, &self->priv->header, error)) {
        return FALSE;
    }

    /* Set stream size */
    mirage_filter_stream_simplified_set_stream_length(MIRAGE_FILTER_STREAM(self), self->priv->header.iso_size);

    /* Format-dependent header parsing */
    switch (self->priv->header.format_version) {
        case FORMAT_VERSION1: {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: format version 1", __debug__);

            self->priv->chunk_table_offset = self->priv->header.chunk_table_offset;
            self->priv->chunk_data_offset = self->priv->header.chunk_data_offset;
            self->priv->chunk_size = self->priv->header.chunk_size;

            break;
        }
        case FORMAT_VERSION2: {
            guint32 len;
            guint bsize_type = 0;
            guint bsize_len = 0;

            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: format version 2", __debug__);

            self->priv->chunk_table_offset = self->priv->header.chunk_table_offset;
            self->priv->chunk_data_offset = self->priv->header.chunk_data_offset & 0x00FFFFFF;
            self->priv->chunk_size = (self->priv->header.chunk_size & 0x00000FFF) << 14;

            /* Chunk table compression/obfuscation flags */
            self->priv->compressed_chunk_table = self->priv->header.chunk_size & 0x4000;

            self->priv->obfuscated_bits = self->priv->header.chunk_size & 0x20000;
            self->priv->obfuscated_chunk_table = self->priv->header.chunk_size & 0x8000000;

            self->priv->bit_swap_type = (self->priv->header.chunk_size >> 0x17) & 3;;
            if (self->priv->image_type == IMAGE_GBI) {
                self->priv->bit_swap_type ^= 1;
            }

            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: actual chunk_data_offset: 0x%X (%d)", __debug__, self->priv->chunk_data_offset, self->priv->chunk_data_offset);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: actual chunk_size: 0x%X (%d)", __debug__, self->priv->chunk_size, self->priv->chunk_size);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: compressed chunk table: %d", __debug__, self->priv->compressed_chunk_table);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: obfuscated chunk table: %d", __debug__, self->priv->obfuscated_chunk_table);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: obfuscated bits in chunk table: %d", __debug__, self->priv->obfuscated_bits);
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: bit swap type: %d", __debug__, self->priv->bit_swap_type);

            /* We do not handle compressed chunk table yet, as I'd like
             * to have a test image first... */
            if (self->priv->compressed_chunk_table) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: compressed chunk table not supported yet!", __debug__);
                g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Compressed chunk table not supported yet!"));
                return FALSE;
            }

            /* We do not handle swapped bytes, either */
            if (self->priv->bit_swap_type != 0) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: bit swap type %d not supported yet!", __debug__, self->priv->bit_swap_type);
                g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Bit swap type %d not supported yet!"), self->priv->bit_swap_type);
                return FALSE;
            }

            /* Decipher bit size for type and length fields of chunk table */
            bsize_type = self->priv->header.format2.chunk_table_bit_settings & 7;
            bsize_len  = self->priv->header.format2.chunk_table_bit_settings >> 3;
            if (bsize_len) {
                bsize_len += 10;
            }
            if (!bsize_len) {
                for (bsize_len = 0, len = self->priv->chunk_size; len > bsize_type; bsize_len++, len >>= 1);
            }
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: chunk table bit-size: length field: %d, compression type field: %d", __debug__, bsize_len, bsize_type);
            self->priv->bitsize_length = bsize_len;
            self->priv->bitsize_type = bsize_type;

            /* Initialize LZMA */
            if (!mirage_filter_stream_daa_initialize_lzma(self, error)) {
                return FALSE;
            }

            break;
        }
        default: {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unsupported format: 0x%X!", __debug__, self->priv->header.format_version);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Unsupported format: 0x%X!"), self->priv->header.format_version);
            return FALSE;
        }
    }

    /* Initialize zlib stream */
    if (!mirage_filter_stream_daa_initialize_zlib(self, error)) {
        return FALSE;
    }

    /* Allocate inflate buffer */
    self->priv->inflate_buffer_size = self->priv->chunk_size;
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: allocating inflate buffer: 0x%X", __debug__, self->priv->inflate_buffer_size);
    self->priv->inflate_buffer = g_try_malloc(self->priv->inflate_buffer_size);
    if (!self->priv->inflate_buffer) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to allocate inflate buffer (%d bytes)!", __debug__, self->priv->inflate_buffer_size);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Failed to allocate inflate buffer (%d bytes)!"), self->priv->inflate_buffer_size);
        return FALSE;
    }

    /* Parse descriptors */
    if (!mirage_filter_stream_daa_parse_descriptors(self, error)) {
        return FALSE;
    }


    /* Parse chunk table */
    if (!mirage_filter_stream_daa_parse_chunk_table(self, error)) {
        return FALSE;
    }

    /* Build parts table */
    if (!mirage_filter_stream_daa_build_part_table(self, error)) {
        return FALSE;
    }

    return TRUE;
}


/**********************************************************************\
 *              MirageFilterStream methods implementations            *
\**********************************************************************/
static gboolean mirage_filter_stream_daa_open (MirageFilterStream *_self, MirageStream *stream, gboolean writable G_GNUC_UNUSED, GError **error)
{
    MirageFilterStreamDaa *self = MIRAGE_FILTER_STREAM_DAA(_self);

    gchar signature[16];

    /* Look for signature at the beginning */
    mirage_stream_seek(stream, 0, G_SEEK_SET, NULL);
    if (mirage_stream_read(stream, signature, sizeof(signature), NULL) != sizeof(signature)) {
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_CANNOT_HANDLE, Q_("Filter cannot handle given data: failed to read 16-byte signature!"));
        return FALSE;
    }

    /* Check signature */
    if (!memcmp(signature, daa_main_signature, sizeof(daa_main_signature))) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: DAA (PowerISO) format", __debug__);
        self->priv->image_type = IMAGE_DAA;
    } else if (!memcmp(signature, gbi_main_signature, sizeof(gbi_main_signature))) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: GBI (gBurner) format", __debug__);
        self->priv->image_type = IMAGE_GBI;
    } else {
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_CANNOT_HANDLE, Q_("Filter cannot handle given data: invalid signature!"));
        return FALSE;
    }

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

    /* Store filename for later processing */
    self->priv->main_filename = mirage_stream_get_filename(stream);

    /* Parse DAA file */
    if (!mirage_filter_stream_daa_parse_daa_file(self, error)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing failed!", __debug__);
        return FALSE;
    }

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

    return TRUE;
}


static gssize mirage_filter_stream_daa_partial_read (MirageFilterStream *_self, void *buffer, gsize count)
{
    MirageFilterStreamDaa *self = MIRAGE_FILTER_STREAM_DAA(_self);
    goffset position = mirage_filter_stream_simplified_get_position(MIRAGE_FILTER_STREAM(self));
    gint chunk_index;

    /* Find chunk that corresponds to current position */
    chunk_index = position / self->priv->chunk_size;
    if (chunk_index >= self->priv->num_chunks) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: stream position %" G_GOFFSET_MODIFIER "d (0x%" G_GOFFSET_MODIFIER "X) beyond end of stream, doing nothing!", __debug__, position, position);
        return 0;
    }

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

    /* Inflate, if necessary */
    if (chunk_index != self->priv->cached_chunk) {
        DAA_Chunk *chunk = &self->priv->chunk_table[chunk_index];
        gsize expected_inflated_size, inflated_size;

        /* Determine expected inflated size */
        if (chunk_index == self->priv->num_chunks-1) {
            /* Last chunk: remainder */
            expected_inflated_size = self->priv->header.iso_size % self->priv->chunk_size;
        } else {
            expected_inflated_size = self->priv->chunk_size;
        }

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

        /* Read chunk */
        if (!mirage_filter_stream_daa_read_from_stream(self, chunk->offset, chunk->length, self->priv->io_buffer, NULL)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read data for chunk #%i", __debug__, chunk_index);
            return -1;
        }

        /* Decrypt if encrypted */
        if (self->priv->encrypted) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: decrypting...", __debug__);
            mirage_filter_stream_daa_decrypt_buffer(self, self->priv->io_buffer, chunk->length);
        }

        /* Inflate */
        memset(self->priv->inflate_buffer, 0, self->priv->inflate_buffer_size); /* Clear the buffer in case we get a failure */
        switch (chunk->compression) {
            case COMPRESSION_NONE: {
                inflated_size = chunk->length - 4;
                memcpy(self->priv->inflate_buffer, self->priv->io_buffer, inflated_size);
                break;
            }
            case COMPRESSION_ZLIB: {
                inflated_size = mirage_filter_stream_daa_inflate_zlib(self, self->priv->io_buffer, chunk->length);
                break;
            }
            case COMPRESSION_LZMA: {
                inflated_size = mirage_filter_stream_daa_inflate_lzma(self, self->priv->io_buffer, chunk->length);
                break;
            }
            default: {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: invalid chunk compression type %d!", __debug__, chunk->compression);
                return -1;
            }
        }

        /* Inflated size should match the expected one */
        if (inflated_size != expected_inflated_size && chunk_index != self->priv->num_chunks - 1) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to inflate whole chunk #%i (0x%" G_GSIZE_MODIFIER "X bytes instead of 0x%" G_GSIZE_MODIFIER "X)", __debug__, chunk_index, inflated_size, expected_inflated_size);
            return -1;
        } else {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: successfully inflated chunk #%i (0x%" G_GSIZE_MODIFIER "X bytes)", __debug__, chunk_index, inflated_size);
        }

        /* Set the index of currently inflated chunk */
        self->priv->cached_chunk = chunk_index;
        self->priv->cached_chunk_size = inflated_size;
    } else {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: chunk already cached", __debug__);
    }


    /* Copy data */
    gint chunk_offset = position % self->priv->chunk_size;
    count = MIN(count, self->priv->cached_chunk_size - chunk_offset);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_STREAM, "%s: offset within chunk: %d, copying %" G_GSIZE_MODIFIER "d bytes", __debug__, chunk_offset, count);

    memcpy(buffer, self->priv->inflate_buffer + chunk_offset, count);

    return count;
}


/**********************************************************************\
 *                             Object init                            *
\**********************************************************************/
static void mirage_filter_stream_daa_init (MirageFilterStreamDaa *self)
{
    self->priv = mirage_filter_stream_daa_get_instance_private(self);

    mirage_filter_stream_generate_info(MIRAGE_FILTER_STREAM(self),
        "FILTER-DAA",
        Q_("DAA File Filter"),
        FALSE,
        2,
        Q_("PowerISO images (*.daa)"), "application/x-daa",
        Q_("gBurner images (*.gbi)"), "application/x-gbi"
    );

    self->priv->chunk_table = NULL;
    self->priv->part_table = NULL;
    self->priv->io_buffer = NULL;
    self->priv->inflate_buffer = NULL;

    self->priv->cached_chunk = -1;
}

static void mirage_filter_stream_daa_finalize (GObject *gobject)
{
    MirageFilterStreamDaa *self = MIRAGE_FILTER_STREAM_DAA(gobject);

    /* Free stream */
    inflateEnd(&self->priv->zlib_stream);
    LzmaDec_Free(&self->priv->lzma_decoder, &lzma_allocator);

    /* Free chunk table */
    g_free(self->priv->chunk_table);

    /* Free part table */
    if (self->priv->part_table) {
        for (gint i = 0; i < self->priv->num_parts; i++) {
            DAA_Part *part = &self->priv->part_table[i];
            if (part->stream) {
                g_object_unref(part->stream);
            }
        }
    }
    g_free(self->priv->part_table);

    /* Free buffer */
    g_free(self->priv->io_buffer);
    g_free(self->priv->inflate_buffer);

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

static void mirage_filter_stream_daa_class_init (MirageFilterStreamDaaClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    MirageFilterStreamClass *filter_stream_class = MIRAGE_FILTER_STREAM_CLASS(klass);

    gobject_class->finalize = mirage_filter_stream_daa_finalize;

    filter_stream_class->open = mirage_filter_stream_daa_open;

    filter_stream_class->simplified_partial_read = mirage_filter_stream_daa_partial_read;
}

static void mirage_filter_stream_daa_class_finalize (MirageFilterStreamDaaClass *klass G_GNUC_UNUSED)
{
}
