import { MAX_BAM_HEADER_READ } from './helpers.js';
function concatChunks(chunks) {
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
function readFileSlice(file, start, end) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsArrayBuffer(file.slice(start, end));
});
}
async function decompressGzipBlock(block) {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
try {
await writer.write(block);
await writer.close();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
return concatChunks(chunks);
} catch (err) {
reader.cancel().catch(() => {});
writer.abort(err).catch(() => {});
throw err;
}
}
async function decompressBgzfBlocks(bytes, neededBytes = 0) {
const chunks = [];
let offset = 0;
let accumulated = 0;
while (offset < bytes.length) {
if (offset + 18 > bytes.length || bytes[offset] !== 0x1f || bytes[offset + 1] !== 0x8b) {
break;
}
if ((bytes[offset + 3] & 0x04) === 0) break;
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, bytes.byteLength - offset);
const xlen = view.getUint16(10, true);
const extraEnd = 12 + xlen;
let bsize = -1;
let pos = 12;
while (pos + 4 <= extraEnd) {
if (bytes[offset + pos] === 0x42 && bytes[offset + pos + 1] === 0x43) {
const slen = view.getUint16(pos + 2, true);
if (slen === 2) {
bsize = view.getUint16(pos + 4, true) + 1;
break;
}
}
const slen = view.getUint16(pos + 2, true);
pos += 4 + slen;
}
if (bsize <= 0 || offset + bsize > bytes.length) break;
const block = bytes.subarray(offset, offset + bsize);
try {
const decompressed = await decompressGzipBlock(block);
if (decompressed.length === 0) break; chunks.push(decompressed);
accumulated += decompressed.length;
} catch {
break;
}
offset += bsize;
if (neededBytes > 0 && accumulated >= neededBytes) break;
}
return concatChunks(chunks);
}
export async function extractBamHeader(file) {
const readSize = Math.min(file.size, MAX_BAM_HEADER_READ);
const buffer = await readFileSlice(file, 0, readSize);
const bytes = new Uint8Array(buffer);
let uncompressed = await decompressBgzfBlocks(bytes, 8);
if (uncompressed.length < 8 ||
uncompressed[0] !== 0x42 || uncompressed[1] !== 0x41 ||
uncompressed[2] !== 0x4d || uncompressed[3] !== 0x01) {
throw new Error('Not a valid BAM file (bad magic bytes)');
}
const view = new DataView(uncompressed.buffer);
const headerLength = view.getInt32(4, true);
if (headerLength < 0) {
throw new Error('BAM header length is negative');
}
const needed = 8 + headerLength;
if (uncompressed.length < needed) {
uncompressed = await decompressBgzfBlocks(bytes, needed);
if (uncompressed.length < needed) {
throw new Error('BAM header length exceeds available data');
}
}
const decoder = new TextDecoder('ascii');
const headerText = decoder.decode(uncompressed.subarray(8, 8 + headerLength));
return { header: headerText.trimEnd(), filename: file.name };
}
export function isBamFile(filename) {
return filename.toLowerCase().endsWith('.bam');
}