"use strict";
/**
 * @license
 * Copyright 2019 Balena Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReadStream = void 0;
const tslib_1 = require("tslib");
const crypto_1 = require("crypto");
const debug_1 = require("debug");
const fs_1 = require("fs");
const stream_1 = require("stream");
const chunk_1 = require("./chunk");
const read_range_1 = require("./read-range");
const debug = debug_1.debug('blockmap:readstream');
class ReadStream extends stream_1.Readable {
    constructor(fdOrReadFn, blockMap, verify = true, generateChecksums = false, start = 0, end = Infinity, chunkSize = 64 * 1024) {
        super({ objectMode: true });
        this.blockMap = blockMap;
        this.verify = verify;
        this.generateChecksums = generateChecksums;
        this.start = start;
        this.end = end;
        this.chunkSize = chunkSize;
        /** Number of block map ranges read */
        this.rangesRead = 0;
        /** Number of block map ranges verified */
        this.rangesVerified = 0;
        /** Number of blocks read */
        this.blocksRead = 0;
        /** Number of bytes read */
        this.bytesRead = 0;
        /** Current offset in bytes */
        this.position = 0;
        if (verify && generateChecksums) {
            throw new Error('verify and generateChecksums options are mutually exclusive');
        }
        if (start < 0) {
            throw new Error('Start must not be negative');
        }
        if (start > end) {
            throw new Error('Start must be less or equal to end');
        }
        if (verify || generateChecksums) {
            this._hash = crypto_1.createHash(blockMap.checksumType);
        }
        this.ranges = this._prepareRanges();
        if (typeof fdOrReadFn === 'number') {
            this.readFn = (buf, offset, length, position) => {
                return new Promise((resolve, reject) => {
                    fs_1.read(fdOrReadFn, buf, offset, length, position, (error, bytesRead, buffer) => {
                        if (error) {
                            reject(error);
                        }
                        else {
                            resolve({ bytesRead, buffer });
                        }
                    });
                });
            };
        }
        else {
            this.readFn = fdOrReadFn;
        }
    }
    /**
     * Preprocess the `blockMap`'s ranges into byte-ranges
     * with respect to the `start` offset, and an `offset`
     * for tracking chunked range reading
     */
    _prepareRanges() {
        return this.blockMap.ranges
            .map((range) => {
            const readRange = new read_range_1.ReadRange(range, this.blockMap.blockSize);
            // Account for readstream's start offset
            readRange.start += this.start;
            readRange.end += this.start;
            return readRange;
        })
            .filter((readRange) => {
            return readRange.end <= this.end + this.start;
        });
    }
    /**
     * Verify a fully read range's checksum against
     * the range's checksum from the blockmap
     */
    _verifyRange() {
        if (this.currentRange === undefined || this._hash === undefined) {
            return;
        }
        const digest = this._hash.digest('hex');
        debug('verify:checksum', this.currentRange.checksum);
        debug('verify:digest  ', digest);
        this._hash = crypto_1.createHash(this.blockMap.checksumType);
        if (this.verify && this.currentRange.checksum !== digest) {
            const error = new read_range_1.ReadRangeError(`Invalid checksum for range [${this.currentRange.startLBA},${this.currentRange.endLBA}], bytes ${this.currentRange.start}-${this.currentRange.end}`, this.currentRange, digest);
            this.emit('error', error);
        }
        if (this.generateChecksums) {
            this.currentRange.range.checksum = digest;
        }
        else {
            this.rangesVerified++;
        }
    }
    /**
     * Read the current range (or a chunk thereof),
     * update state and emit the read block
     */
    _readBlock() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.currentRange === undefined) {
                return;
            }
            const length = Math.min(this.currentRange.length - this.currentRange.offset, this.chunkSize);
            const position = this.currentRange.start + this.currentRange.offset;
            const chunk = new chunk_1.Chunk(Buffer.allocUnsafe(length), position);
            debug('read-block:position', position);
            debug('read-block:length', length);
            try {
                const { bytesRead } = yield this.readFn(chunk.buffer, 0, length, position);
                if (bytesRead !== length) {
                    throw new Error(`Bytes read mismatch: ${bytesRead} != ${length}`);
                }
                this.currentRange.offset += bytesRead;
                this.blocksRead += bytesRead / this.blockMap.blockSize;
                this.bytesRead += bytesRead;
                this.position += bytesRead;
                debug('read-block:blocksRead', this.blocksRead);
                // Feed the hash if we're verifying
                if (this._hash !== undefined) {
                    this._hash.update(chunk.buffer);
                }
                this.push(chunk);
            }
            catch (error) {
                this.emit('error', error);
            }
        });
    }
    /**
     * Advance to next the Range if there is one then read a block;
     * else end the stream;
     * @see https://nodejs.org/api/stream.html#stream_implementing_a_readable_stream
     */
    _advanceRange() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.ranges.length > 0) {
                this.currentRange = this.ranges.shift();
                this.rangesRead++;
                debug('read:range %O', this.currentRange);
                yield this._readBlock();
            }
            else {
                this.push(null);
            }
        });
    }
    /**
     * Initiate a new read, advancing the range if necessary,
     * and verifying checksums, if enabled
     * @see https://nodejs.org/api/stream.html#stream_implementing_a_readable_stream
     */
    _read() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.currentRange === undefined) {
                yield this._advanceRange();
            }
            else if (this.currentRange.offset === this.currentRange.length) {
                this._verifyRange();
                yield this._advanceRange();
            }
            else {
                yield this._readBlock();
            }
        });
    }
}
exports.ReadStream = ReadStream;
//# sourceMappingURL=read-stream.js.map