"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Image = void 0;
const plist = require("@balena/apple-plist");
const crc32 = require("cyclic-32");
const stream_1 = require("stream");
const blockmap_1 = require("./blockmap");
const constants_1 = require("./constants");
const footer_1 = require("./footer");
const readstream_1 = require("./readstream");
const sparse_readstream_1 = require("./sparse-readstream");
const utils_1 = require("./utils");
/**
 * Apple Disk Image (DMG)
 */
class Image {
    constructor(fs) {
        this.fs = fs;
        this.resourceFork = {
            blkx: [],
            nsiz: [],
            cSum: [],
            plst: [],
            size: [],
        };
        this.ready = this.open();
    }
    async open() {
        await this.readFooter();
        await this.readPropertyList();
    }
    static parseBlkx(blkx) {
        return blkx.map((block) => ({
            id: +block['ID'],
            attributes: +block['Attributes'],
            name: block['Name'],
            coreFoundationName: block['CFName'],
            map: blockmap_1.BlockMap.parse(block['Data']),
        }));
    }
    static parseNsiz(nsiz) {
        return nsiz.map((block) => ({
            id: +block['ID'],
            attributes: +block['Attributes'],
            name: block['Name'],
            data: plist.parse(block['Data'].toString()).data,
        }));
    }
    static parsePlst(plst) {
        return plst.map((block) => ({
            id: +block['ID'],
            attributes: +block['Attributes'],
            name: block['Name'],
            data: block['Data'],
        }));
    }
    static parseCsum(plst) {
        return plst.map((block) => ({
            id: +block['ID'],
            attributes: +block['Attributes'],
            name: block['Name'],
            data: {
                unknown: block['Data'].readUInt16LE(0),
                type: block['Data'].readUInt32LE(2),
                value: block['Data'].toString('hex', 6),
            },
        }));
    }
    static parseSize(size) {
        return size.map((block) => ({
            id: +block['ID'],
            attributes: +block['Attributes'],
            name: block['Name'],
            data: block['Data'],
        }));
    }
    /** Create a readable stream of this image */
    async createReadStream(end = Infinity) {
        // end is inclusive
        await this.ready;
        // @ts-ignore: Readable.from exists
        return stream_1.Readable.from((0, readstream_1.readStream)(this, end));
    }
    /** Create a sparse readable stream of this image */
    async createSparseReadStream() {
        await this.ready;
        // @ts-ignore: Readable.from exists
        return stream_1.Readable.from((0, sparse_readstream_1.sparseReadStream)(this));
    }
    /** Calculate the uncompressed size of the contained resource */
    async getUncompressedSize() {
        await this.ready;
        return this.resourceFork.blkx.reduce((size, resource) => {
            return resource.map.blocks.reduce((s, block) => {
                return s + block.sectorCount * constants_1.SECTOR_SIZE;
            }, size);
        }, 0);
    }
    /** Calculate the amount of mapped (non-zero & non-free) bytes */
    async getMappedSize() {
        await this.ready;
        return this.resourceFork.blkx.reduce((size, resource) => {
            return resource.map.blocks.reduce((s, block) => {
                if (block.type !== constants_1.BLOCK.ZEROFILL && block.type !== constants_1.BLOCK.FREE) {
                    return s + block.sectorCount * constants_1.SECTOR_SIZE;
                }
                else {
                    return s;
                }
            }, size);
        }, 0);
    }
    async verifyData() {
        await this.ready;
        if (this.footer === undefined) {
            throw new Error('Must read footer before calling verifyData');
        }
        // Return `null` if there's no data checksum, or the type is set to NONE
        if (!this.footer.dataChecksum ||
            this.footer.dataChecksum.type === constants_1.CHECKSUM_TYPE.NONE) {
            return null;
        }
        let hash;
        let checksum = '';
        if (this.footer.dataChecksum.type === constants_1.CHECKSUM_TYPE.CRC32) {
            hash = new crc32.Hash({ encoding: 'hex' });
        }
        else {
            throw new Error(`Unknown or unsupported checksum type "${this.footer.dataChecksum.type}"`);
        }
        const readable = await this.fs.createReadStream(this.footer.dataForkOffset, this.footer.dataForkOffset + this.footer.dataForkLength - 1);
        return await new Promise((resolve, reject) => {
            readable
                .on('error', reject)
                .pipe(hash)
                .on('error', reject)
                .on('readable', function () {
                let chunk = this.read();
                while (chunk) {
                    checksum += chunk;
                    chunk = this.read();
                }
            })
                .on('end', () => {
                var _a, _b;
                resolve(checksum === ((_b = (_a = this.footer) === null || _a === void 0 ? void 0 : _a.dataChecksum) === null || _b === void 0 ? void 0 : _b.value));
            });
        });
    }
    async readFooter() {
        const length = footer_1.Footer.SIZE;
        const position = this.fs.size - footer_1.Footer.SIZE;
        const buffer = await (0, utils_1.streamToBuffer)(await this.fs.createReadStream(position, position + length - 1));
        this.footer = footer_1.Footer.parse(buffer);
        return this.footer;
    }
    async readPropertyList() {
        if (this.footer === undefined) {
            throw new Error('Must read footer before property list');
        }
        const length = this.footer.xmlLength;
        const position = this.footer.xmlOffset;
        const buffer = await (0, utils_1.streamToBuffer)(await this.fs.createReadStream(position, position + length - 1));
        const data = plist.parse(buffer.toString()).data;
        const resourceFork = data['resource-fork'];
        if (resourceFork.blkx) {
            this.resourceFork.blkx = Image.parseBlkx(resourceFork.blkx);
        }
        if (resourceFork.nsiz) {
            this.resourceFork.nsiz = Image.parseNsiz(resourceFork.nsiz);
        }
        if (resourceFork.cSum) {
            this.resourceFork.cSum = Image.parseCsum(resourceFork.cSum);
        }
        if (resourceFork.plst) {
            this.resourceFork.plst = Image.parsePlst(resourceFork.plst);
        }
        if (resourceFork.size) {
            this.resourceFork.size = Image.parseSize(resourceFork.size);
        }
        return this.resourceFork;
    }
}
exports.Image = Image;
//# sourceMappingURL=image.js.map