@gmod/bgzf-filehandle
Version:
read from a compressed bgzip file (with .gzi) as if it were uncompressed
91 lines (79 loc) • 2.48 kB
text/typescript
import { readUint64LE } from './long.ts'
import type { GenericFilehandle } from 'generic-filehandle2'
const ENTRY_SIZE = 16
function parseEntries(buf: Uint8Array, numEntries: number) {
const entries: [number, number][] = new Array(numEntries + 1)
entries[0] = [0, 0]
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
for (let i = 0; i < numEntries; i += 1) {
const offset = i * ENTRY_SIZE
entries[i + 1] = [readUint64LE(view, offset), readUint64LE(view, offset + 8)]
}
return entries
}
// lower-bound binary search: first index whose uncompressed start > position
function findUpperBound(entries: [number, number][], position: number) {
let lo = 0
let hi = entries.length
while (lo < hi) {
const mid = (lo + hi) >>> 1
if (entries[mid]![1] <= position) {
lo = mid + 1
} else {
hi = mid
}
}
return lo
}
export default class GziIndex {
filehandle: GenericFilehandle
index?: Promise<[number, number][]>
constructor({ filehandle }: { filehandle: GenericFilehandle }) {
this.filehandle = filehandle
}
_getIndex() {
if (!this.index) {
this.index = this._readIndex().catch((error: unknown) => {
this.index = undefined
throw error
})
}
return this.index
}
private async _readIndex(): Promise<[number, number][]> {
const header = await this.filehandle.read(8, 0)
const numEntries = readUint64LE(
new DataView(header.buffer, header.byteOffset, header.byteLength),
)
if (numEntries === 0) {
return [[0, 0]]
}
if (numEntries > Number.MAX_SAFE_INTEGER / ENTRY_SIZE) {
throw new TypeError('integer overflow')
}
const body = await this.filehandle.read(ENTRY_SIZE * numEntries, 8)
return parseEntries(body, numEntries)
}
async getRelevantBlocksForRead(
length: number,
position: number,
): Promise<{
blocks: [number, number][]
nextCompressedPosition: number | undefined
}> {
if (length === 0) {
return { blocks: [], nextCompressedPosition: undefined }
}
const entries = await this._getIndex()
const readEnd = position + length
const startBlock = findUpperBound(entries, position) - 1
let endBlock = startBlock + 1
while (endBlock < entries.length && entries[endBlock]![1] < readEnd) {
endBlock += 1
}
return {
blocks: entries.slice(startBlock, endBlock),
nextCompressedPosition: entries[endBlock]?.[0],
}
}
}