"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.FilterStream = void 0;
const crypto_1 = require("crypto");
const debug_1 = require("debug");
const stream_1 = require("stream");
const chunk_1 = require("./chunk");
const read_range_1 = require("./read-range");
const debug = debug_1.debug('blockmap:filterstream');
class FilterStream extends stream_1.Transform {
    constructor(blockMap, verify = true, generateChecksums = false, chunkSize = 64 * 1024) {
        super({ readableObjectMode: true });
        this.blockMap = blockMap;
        this.verify = verify;
        this.generateChecksums = generateChecksums;
        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;
        /** Number of bytes written */
        this.blocksWritten = 0;
        /** Number of bytes written */
        this.bytesWritten = 0;
        /** Current offset in bytes */
        this.position = 0;
        this._chunks = [];
        this._bytes = 0;
        if (verify && generateChecksums) {
            throw new Error('verify and generateChecksums options are mutually exclusive');
        }
        this.ranges = this._getByteRangesFromBlockMap();
        this.currentRange = this.ranges.shift();
        debug('range:next', this.currentRange);
        if (verify || generateChecksums) {
            this._hash = crypto_1.createHash(this.blockMap.checksumType);
        }
    }
    /**
     * Preprocess the `blockMap`'s ranges into byte-ranges
     * with respect to the `start` offset, and an `offset`
     * for tracking chunked range reading
     */
    _getByteRangesFromBlockMap() {
        return this.blockMap.ranges.map((range) => {
            return new read_range_1.ReadRange(range, this.blockMap.blockSize);
        });
    }
    /**
     * Verify a fully read range's checksum against
     * the range's checksum from the blockmap
     * or calculate the range's checksum and update the blockmap
     * if options.generateChecksums is true.
     */
    _verifyRange() {
        if (this._hash === undefined || this.currentRange === 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);
            return;
        }
        if (this.generateChecksums) {
            this.currentRange.range.checksum = digest;
        }
        else {
            this.rangesVerified++;
        }
    }
    /**
     * Determine whether a chunk is in the current range
     */
    _rangeInChunk(chunk) {
        if (this.currentRange === undefined) {
            return false;
        }
        const rangeStart = this.currentRange.start + this.currentRange.offset;
        const isRangeInChunk = rangeStart >= this.position && rangeStart < this.position + chunk.length;
        debug('range-in-chunk', isRangeInChunk);
        return isRangeInChunk;
    }
    /**
     * Chunk a given input buffer into blocks
     * matching the blockSize and advance the
     * current range, if necessary
     */
    _transformBlock(chunk, next) {
        let start = 0;
        let end = 0;
        let length = 0;
        let block;
        while (this.currentRange && this._rangeInChunk(chunk)) {
            start =
                this.currentRange.start + this.currentRange.offset - this.position;
            end = start + this.currentRange.length - this.currentRange.offset;
            debug('slice', start, '-', end);
            // Cut the block, and add position & address to it
            block = new chunk_1.Chunk(chunk.slice(start, end), this.currentRange.start + this.currentRange.offset);
            // Make sure we don't emit buffers not matching
            // the blockSize, in case the range's end is not in the current chunk
            if (end > chunk.length) {
                length = chunk.length - start;
                debug('chunk:partial', length);
                if (length % this.blockMap.blockSize !== 0) {
                    debug('chunk:buffer', 'length < block size');
                    this._chunks.push(block.buffer);
                    this._bytes += block.length;
                    this.position += chunk.length - block.length;
                    return process.nextTick(next);
                }
            }
            // Keep track of where we are within the current range
            this.currentRange.offset += block.length;
            // Advance counters
            this.bytesWritten += block.length;
            this.blocksWritten += block.length / this.blockMap.blockSize;
            // Emit the cut block
            debug('push', block.position, block.position / this.blockMap.blockSize, block);
            this.push(block);
            if (this._hash !== undefined) {
                this._hash.update(block.buffer);
            }
            // Once we've read a complete range,
            // verify it and move to the next range
            if (this.currentRange.length === this.currentRange.offset) {
                this._verifyRange();
                this.rangesRead++;
                this.currentRange = this.currentRange = this.ranges.shift();
                debug('range:next', this.currentRange);
            }
        }
        this.position += chunk.length;
        process.nextTick(next);
    }
    /** Transform input into block-sized chunks */
    _transform(chunk, _encoding, next) {
        debug('position', this.position);
        debug('chunk', chunk.length, chunk);
        this.bytesRead += chunk.length;
        this.blocksRead += chunk.length / this.blockMap.blockSize;
        // We've run out of ranges; ignore everything
        if (this.currentRange === undefined) {
            debug('no current range');
            this.position += chunk.length;
            return process.nextTick(next);
        }
        // If this chunk is not in our range at all, skip it
        if (!this._rangeInChunk(chunk)) {
            debug('chunk:ignore');
            this.position += chunk.length;
            return process.nextTick(next);
        }
        // If we have buffered up chunks,
        // and they don't exceed the highWaterMark yet,
        // buffer this chunk as well, and wait for the next chunk
        if (this._bytes && this._bytes < this.chunkSize) {
            debug('chunk:buffer', 'not enough bytes');
            this._chunks.push(chunk);
            this._bytes += chunk.length;
            return process.nextTick(next);
        }
        // If we have enough buffered chunks, concat & emit them
        if (this._bytes) {
            debug('chunk:concat', this._bytes + chunk.length);
            this._chunks.push(chunk);
            this._bytes += chunk.length;
            chunk = Buffer.concat(this._chunks, this._bytes);
            this._chunks = [];
            this._bytes = 0;
        }
        this._transformBlock(chunk, next);
    }
    /**
     * Flush out any unprocessed chunks from
     * the internal buffer once the stream is being ended
     */
    _flush(done) {
        if (this._bytes) {
            const chunk = Buffer.concat(this._chunks, this._bytes);
            this._chunks = [];
            this._bytes = 0;
            this._transformBlock(chunk, done);
        }
        else {
            done();
        }
    }
}
exports.FilterStream = FilterStream;
//# sourceMappingURL=filter-stream.js.map