@gmod/cram
Version:
read CRAM files with pure Javascript
155 lines (135 loc) • 4.63 kB
text/typescript
/* !
* This is adapted from the xz-decompress NPM package.
* https://github.com/httptoolkit/xz-decompress/ License MIT
*
* That codebase in turn had these citations
* Based on xzwasm (c) Steve Sanderson. License: MIT - https://github.com/SteveSanderson/xzwasm
* Contains xz-embedded by Lasse Collin and Igor Pavlov. License: Public domain - https://tukaani.org/xz/embedded.html
* and walloc (c) 2020 Igalia, S.L. License: MIT - https://github.com/wingo/walloc
*/
import { xzdecompressWasm as wasmBase64 } from './wasm.ts'
const XZ_OK = 0
const XZ_STREAM_END = 1
interface WasmExports {
memory: WebAssembly.Memory
create_context(): number
destroy_context(ptr: number): void
supply_input(ptr: number, length: number): void
get_next_output(ptr: number): number
}
class XzContext {
exports: WasmExports
memory: WebAssembly.Memory
ptr: number
bufSize: number
inStart: number
outStart: number
mem8?: Uint8Array
mem32?: Uint32Array
constructor(moduleInstance: WebAssembly.Instance) {
this.exports = moduleInstance.exports as unknown as WasmExports
this.memory = this.exports.memory
this.ptr = this.exports.create_context()
this._refresh()
this.bufSize = this.mem32![0]!
this.inStart = this.mem32![1]! - this.ptr
this.outStart = this.mem32![4]! - this.ptr
}
supplyInput(sourceDataUint8Array: Uint8Array) {
this._refresh()
const inBuffer = this.mem8!.subarray(
this.inStart,
this.inStart + this.bufSize,
)
inBuffer.set(sourceDataUint8Array, 0)
this.exports.supply_input(this.ptr, sourceDataUint8Array.byteLength)
}
getNextOutput() {
const result = this.exports.get_next_output(this.ptr)
this._refresh()
if (result !== XZ_OK && result !== XZ_STREAM_END) {
throw new Error(`get_next_output failed with error code ${result}`)
}
const outChunk = this.mem8!.slice(
this.outStart,
this.outStart + /* outPos */ this.mem32![5]!,
)
return { outChunk, finished: result === XZ_STREAM_END }
}
dispose() {
this.exports.destroy_context(this.ptr)
}
_refresh() {
const currentBuffer = this.memory.buffer
if (currentBuffer !== this.mem8?.buffer) {
this.mem8 = new Uint8Array(currentBuffer, this.ptr)
this.mem32 = new Uint32Array(currentBuffer, this.ptr)
}
}
}
let _moduleInstancePromise: Promise<void> | undefined
let _moduleInstance: WebAssembly.Instance | undefined
const _emptyInput = new Uint8Array(0)
async function _getModuleInstance() {
const base64Wasm = wasmBase64.replace('data:application/wasm;base64,', '')
const binaryString = atob(base64Wasm)
const len = binaryString.length
const wasmBytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
wasmBytes[i] = binaryString.charCodeAt(i)
}
const module = await WebAssembly.instantiate(wasmBytes.buffer, {})
_moduleInstance = module.instance
}
export async function xzDecompress(input: Uint8Array): Promise<Uint8Array> {
if (!_moduleInstance) {
await (_moduleInstancePromise ||
(_moduleInstancePromise = _getModuleInstance()))
}
const context = new XzContext(_moduleInstance!)
const chunks: Uint8Array[] = []
let offset = 0
let eofSignaled = false
try {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
// Check if WASM needs more input (inPos === inSize)
if (context.mem32![2] === context.mem32![3]) {
if (offset < input.length) {
const chunkSize = Math.min(context.bufSize, input.length - offset)
context.supplyInput(input.subarray(offset, offset + chunkSize))
offset += chunkSize
} else if (!eofSignaled) {
// Signal EOF by supplying empty input once
context.supplyInput(_emptyInput)
eofSignaled = true
} else {
// Stuck in a loop - WASM needs more input but we're at EOF
throw new Error('XZ decompression error: unexpected end of input')
}
}
const result = context.getNextOutput()
if (result.outChunk.length > 0) {
chunks.push(result.outChunk)
}
// Reset output buffer position
context.mem32![5] = 0
if (result.finished) {
break
}
}
if (chunks.length === 1) {
return chunks[0]!
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
const output = new Uint8Array(totalLength)
let position = 0
for (const chunk of chunks) {
output.set(chunk, position)
position += chunk.length
}
return output
} finally {
context.dispose()
}
}