UNPKG

blockmap

Version:
215 lines 8.67 kB
"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