@gmod/bgzf-filehandle
Version:
read from a compressed bgzip file (with .gzi) as if it were uncompressed
97 lines • 3.52 kB
JavaScript
import { longFromBytesToUnsigned } from "./long.js";
const UNCOMPRESSED_POSITION = 1;
// binary search to find the block that the
// read starts in and extend forward from that
function compare(position, entry, nextEntry) {
const uncompressedPosition = entry[UNCOMPRESSED_POSITION];
const nextUncompressedPosition = nextEntry
? nextEntry[UNCOMPRESSED_POSITION]
: Infinity;
if (uncompressedPosition <= position && nextUncompressedPosition > position) {
// block overlaps read start
return 0;
}
else if (uncompressedPosition < position) {
// block is before read start
return -1;
}
else {
// block is after read start
return 1;
}
}
export default class GziIndex {
constructor({ filehandle }) {
this.filehandle = filehandle;
}
_getIndex() {
if (!this.index) {
this.index = this._readIndex().catch((e) => {
this.index = undefined;
throw e;
});
}
return this.index;
}
async _readIndex() {
const buf = await this.filehandle.read(8, 0);
const numEntries = longFromBytesToUnsigned(buf);
if (!numEntries) {
return [[0, 0]];
}
const entries = new Array(numEntries + 1);
entries[0] = [0, 0];
// TODO rewrite this to make an index-index that stays in memory
const bufSize = 8 * 2 * numEntries;
if (bufSize > Number.MAX_SAFE_INTEGER) {
throw new TypeError('integer overflow');
}
const b2 = await this.filehandle.read(bufSize, 8);
for (let entryNumber = 0; entryNumber < numEntries; entryNumber += 1) {
const compressedPos = longFromBytesToUnsigned(b2, entryNumber * 16);
const uncompressedPos = longFromBytesToUnsigned(b2, entryNumber * 16 + 8);
entries[entryNumber + 1] = [compressedPos, uncompressedPos];
}
return entries;
}
async getLastBlock() {
const entries = await this._getIndex();
return entries.at(-1);
}
async getRelevantBlocksForRead(length, position) {
const endPosition = position + length;
if (length === 0) {
return [];
}
const entries = await this._getIndex();
const relevant = [];
let lowerBound = 0;
let upperBound = entries.length - 1;
let searchPosition = Math.floor(entries.length / 2);
let comparison = compare(position, entries[searchPosition], entries[searchPosition + 1]);
while (comparison !== 0) {
if (comparison > 0) {
upperBound = searchPosition - 1;
}
else if (comparison < 0) {
lowerBound = searchPosition + 1;
}
searchPosition = Math.ceil((upperBound - lowerBound) / 2) + lowerBound;
comparison = compare(position, entries[searchPosition], entries[searchPosition + 1]);
}
// here's where we read forward
relevant.push(entries[searchPosition]);
let i = searchPosition + 1;
for (; i < entries.length; i += 1) {
relevant.push(entries[i]);
if (entries[i][UNCOMPRESSED_POSITION] >= endPosition) {
break;
}
}
if (relevant[relevant.length - 1][UNCOMPRESSED_POSITION] < endPosition) {
relevant.push([]);
}
return relevant;
}
}
//# sourceMappingURL=gziIndex.js.map