1
0
Fork 0
mirror of https://github.com/codedread/bitjs synced 2025-10-03 09:39:16 +02:00
bitjs/archive/gunzip.js

125 lines
3.2 KiB
JavaScript

/**
* gunzip.js
*
* Licensed under the MIT License
*
* Copyright(c) 2024 Google Inc.
*
* Reference Documentation:
*
* https://www.ietf.org/rfc/rfc1952.txt
*/
import { BitStream } from '../io/bitstream.js';
import { ByteStream } from '../io/bytestream.js';
/** @type {MessagePort} */
let hostPort;
/** @type {ByteStream} */
let bstream = null;
// undefined unless a FNAME block is present.
let filename;
const err = str => hostPort.postMessage({ type: 'error', msg: str });
async function gunzip() {
const sig = bstream.readBytes(2);
if (sig[0] !== 0x1F || sig[1] !== 0x8B) {
const errMsg = `First two bytes not 0x1F, 0x8B: ${sig[0].toString(16)} ${sig[1].toString(16)}`;
err(errMsg);
return;
}
const compressionMethod = bstream.readNumber(1);
if (compressionMethod !== 8) {
const errMsg = `Compression method ${compressionMethod} not supported`;
err(errMsg);
return;
}
// Parse the GZIP header to see if we can find a filename (FNAME block).
const flags = new BitStream(bstream.readBytes(1).buffer);
flags.skip(1); // skip FTEXT bit
const fhcrc = flags.readBits(1);
const fextra = flags.readBits(1);
const fname = flags.readBits(1);
const fcomment = flags.readBits(1);
bstream.skip(4); // MTIME
bstream.skip(1); // XFL
bstream.skip(1); // OS
if (fextra) {
const xlen = bstream.readNumber(2);
bstream.skip(xlen);
}
if (fname) {
// Find the null-terminator byte.
let numBytes = 0;
const findNull = bstream.tee();
while (findNull.readNumber(1) !== 0) numBytes++;
filename = bstream.readString(numBytes);
}
if (fcomment) {
// Find the null-terminator byte.
let numBytes = 0;
const findNull = bstream.tee();
while (findNull.readNumber(1) !== 0) numBytes++;
bstream.skip(numBytes); // COMMENT
}
if (fhcrc) {
bstream.readNumber(2); // CRC16
}
// Now try to use native implementation of INFLATE, if supported by the runtime.
const blob = new Blob([bstream.bytes.buffer]);
const decompressedStream = blob.stream().pipeThrough(new DecompressionStream('gzip'));
const fileData = new Uint8Array(await new Response(decompressedStream).arrayBuffer());
const unarchivedFile = { filename, fileData };
hostPort.postMessage({ type: 'extract', unarchivedFile }, [fileData.buffer]);
// TODO: Supported chunked decompression?
// TODO: Fall through to non-native implementation via inflate() ?
hostPort.postMessage({ type: 'finish', metadata: {} });
}
// event.data.file has the first ArrayBuffer.
const onmessage = async function (event) {
const bytes = event.data.file;
if (!bstream) {
bstream = new ByteStream(bytes);
bstream.setLittleEndian(true);
} else {
throw `Gunzipper does not calling update() with more bytes. Send the whole file with start().`
}
await gunzip();
};
/**
* Connect the host to the gunzip implementation with the given MessagePort.
* @param {MessagePort} port
*/
export function connect(port) {
if (hostPort) {
throw `connect(): hostPort already connected in gunzip.js`;
}
hostPort = port;
port.onmessage = onmessage;
}
export function disconnect() {
if (!hostPort) {
throw `disconnect(): hostPort was not connected in gunzip.js`;
}
hostPort = null;
bstream = null;
filename = undefined;
}