mirror of
https://github.com/codedread/bitjs
synced 2025-10-03 09:39:16 +02:00
125 lines
3.2 KiB
JavaScript
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;
|
|
}
|