mirror of
https://github.com/codedread/bitjs
synced 2025-10-03 09:39:16 +02:00
249 lines
7.3 KiB
JavaScript
249 lines
7.3 KiB
JavaScript
/**
|
|
* untar.js
|
|
*
|
|
* Licensed under the MIT License
|
|
*
|
|
* Copyright(c) 2011 Google Inc.
|
|
*
|
|
* Reference Documentation:
|
|
*
|
|
* TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html
|
|
*/
|
|
|
|
import { ByteStream } from '../io/bytestream.js';
|
|
|
|
const UnarchiveState = {
|
|
NOT_STARTED: 0,
|
|
UNARCHIVING: 1,
|
|
WAITING: 2,
|
|
FINISHED: 3,
|
|
};
|
|
|
|
/** @type {MessagePort} */
|
|
let hostPort;
|
|
|
|
// State - consider putting these into a class.
|
|
let unarchiveState = UnarchiveState.NOT_STARTED;
|
|
/** @type {ByteStream} */
|
|
let bytestream = null;
|
|
let allLocalFiles = null;
|
|
let logToConsole = false;
|
|
|
|
// Progress variables.
|
|
let currentFilename = '';
|
|
let currentFileNumber = 0;
|
|
let currentBytesUnarchivedInFile = 0;
|
|
let currentBytesUnarchived = 0;
|
|
let totalUncompressedBytesInArchive = 0;
|
|
let totalFilesInArchive = 0;
|
|
|
|
// Helper functions.
|
|
const info = function (str) {
|
|
hostPort.postMessage({ type: 'info', msg: str });
|
|
};
|
|
const err = function (str) {
|
|
hostPort.postMessage({ type: 'error', msg: str });
|
|
};
|
|
const postProgress = function () {
|
|
hostPort.postMessage({
|
|
type: 'progress',
|
|
currentFilename,
|
|
currentFileNumber,
|
|
currentBytesUnarchivedInFile,
|
|
currentBytesUnarchived,
|
|
totalUncompressedBytesInArchive,
|
|
totalFilesInArchive,
|
|
totalCompressedBytesRead: bytestream.getNumBytesRead(),
|
|
});
|
|
};
|
|
|
|
// Removes all characters from the first zero-byte in the string onwards.
|
|
const readCleanString = function (bstr, numBytes) {
|
|
const str = bstr.readString(numBytes);
|
|
const zIndex = str.indexOf(String.fromCharCode(0));
|
|
return zIndex != -1 ? str.substr(0, zIndex) : str;
|
|
};
|
|
|
|
class TarLocalFile {
|
|
// takes a ByteStream and parses out the local file information
|
|
constructor(bstream) {
|
|
this.isValid = false;
|
|
|
|
let bytesRead = 0;
|
|
|
|
// Read in the header block
|
|
this.name = readCleanString(bstream, 100);
|
|
this.mode = readCleanString(bstream, 8);
|
|
this.uid = readCleanString(bstream, 8);
|
|
this.gid = readCleanString(bstream, 8);
|
|
this.size = parseInt(readCleanString(bstream, 12), 8);
|
|
this.mtime = readCleanString(bstream, 12);
|
|
this.chksum = readCleanString(bstream, 8);
|
|
this.typeflag = readCleanString(bstream, 1);
|
|
this.linkname = readCleanString(bstream, 100);
|
|
this.maybeMagic = readCleanString(bstream, 6);
|
|
|
|
if (this.maybeMagic == 'ustar') {
|
|
this.version = readCleanString(bstream, 2);
|
|
this.uname = readCleanString(bstream, 32);
|
|
this.gname = readCleanString(bstream, 32);
|
|
this.devmajor = readCleanString(bstream, 8);
|
|
this.devminor = readCleanString(bstream, 8);
|
|
this.prefix = readCleanString(bstream, 155);
|
|
|
|
// From https://linux.die.net/man/1/ustar:
|
|
// "The name field (100 chars) an inserted slash ('/') and the prefix field (155 chars)
|
|
// produce the pathname of the file. When recreating the original filename, name and prefix
|
|
// are concatenated, using a slash character in the middle. If a pathname does not fit in the
|
|
// space provided or may not be split at a slash character so that the parts will fit into
|
|
// 100 + 155 chars, the file may not be archived. Linknames longer than 100 chars may not be
|
|
// archived too."
|
|
if (this.prefix.length) {
|
|
this.name = `${this.prefix}/${this.name}`;
|
|
}
|
|
bstream.readBytes(12); // 512 - 500
|
|
} else {
|
|
bstream.readBytes(255); // 512 - 257
|
|
}
|
|
|
|
bytesRead += 512;
|
|
|
|
// Done header, now rest of blocks are the file contents.
|
|
this.filename = this.name;
|
|
/** @type {Uint8Array} */
|
|
this.fileData = null;
|
|
|
|
info(`Untarring file '${this.filename}'`);
|
|
info(` size = ${this.size}`);
|
|
info(` typeflag = ${this.typeflag}`);
|
|
|
|
// A regular file.
|
|
if (this.typeflag == 0) {
|
|
info(' This is a regular file.');
|
|
const sizeInBytes = parseInt(this.size);
|
|
this.fileData = new Uint8Array(bstream.readBytes(sizeInBytes));
|
|
bytesRead += sizeInBytes;
|
|
if (this.name.length > 0 && this.size > 0 && this.fileData && this.fileData.buffer) {
|
|
this.isValid = true;
|
|
}
|
|
|
|
// Round up to 512-byte blocks.
|
|
const remaining = 512 - bytesRead % 512;
|
|
if (remaining > 0 && remaining < 512) {
|
|
bstream.readBytes(remaining);
|
|
}
|
|
} else if (this.typeflag == 5) {
|
|
info(' This is a directory.')
|
|
}
|
|
}
|
|
}
|
|
|
|
const untar = function () {
|
|
let bstream = bytestream.tee();
|
|
|
|
// While we don't encounter an empty block, keep making TarLocalFiles.
|
|
while (bstream.peekNumber(4) != 0) {
|
|
const oneLocalFile = new TarLocalFile(bstream);
|
|
if (oneLocalFile && oneLocalFile.isValid) {
|
|
// If we make it to this point and haven't thrown an error, we have successfully
|
|
// read in the data for a local file, so we can update the actual bytestream.
|
|
bytestream = bstream.tee();
|
|
|
|
allLocalFiles.push(oneLocalFile);
|
|
totalUncompressedBytesInArchive += oneLocalFile.size;
|
|
|
|
// update progress
|
|
currentFilename = oneLocalFile.filename;
|
|
currentFileNumber = totalFilesInArchive++;
|
|
currentBytesUnarchivedInFile = oneLocalFile.size;
|
|
currentBytesUnarchived += oneLocalFile.size;
|
|
hostPort.postMessage({ type: 'extract', unarchivedFile: oneLocalFile }, [oneLocalFile.fileData.buffer]);
|
|
postProgress();
|
|
}
|
|
}
|
|
totalFilesInArchive = allLocalFiles.length;
|
|
|
|
postProgress();
|
|
|
|
bytestream = bstream.tee();
|
|
};
|
|
|
|
// event.data.file has the first ArrayBuffer.
|
|
// event.data.bytes has all subsequent ArrayBuffers.
|
|
const onmessage = function (event) {
|
|
const bytes = event.data.file || event.data.bytes;
|
|
logToConsole = !!event.data.logToConsole;
|
|
|
|
// This is the very first time we have been called. Initialize the bytestream.
|
|
if (!bytestream) {
|
|
bytestream = new ByteStream(bytes);
|
|
} else {
|
|
bytestream.push(bytes);
|
|
}
|
|
|
|
if (unarchiveState === UnarchiveState.NOT_STARTED) {
|
|
currentFilename = '';
|
|
currentFileNumber = 0;
|
|
currentBytesUnarchivedInFile = 0;
|
|
currentBytesUnarchived = 0;
|
|
totalUncompressedBytesInArchive = 0;
|
|
totalFilesInArchive = 0;
|
|
allLocalFiles = [];
|
|
|
|
hostPort.postMessage({ type: 'start' });
|
|
|
|
unarchiveState = UnarchiveState.UNARCHIVING;
|
|
|
|
postProgress();
|
|
}
|
|
|
|
if (unarchiveState === UnarchiveState.UNARCHIVING ||
|
|
unarchiveState === UnarchiveState.WAITING) {
|
|
try {
|
|
untar();
|
|
unarchiveState = UnarchiveState.FINISHED;
|
|
hostPort.postMessage({ type: 'finish', metadata: {} });
|
|
} catch (e) {
|
|
if (typeof e === 'string' && e.startsWith('Error! Overflowed')) {
|
|
// Overrun the buffer.
|
|
unarchiveState = UnarchiveState.WAITING;
|
|
} else {
|
|
console.error('Found an error while untarring');
|
|
console.dir(e);
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Connect the host to the untar implementation with the given MessagePort.
|
|
* @param {MessagePort} port
|
|
*/
|
|
export function connect(port) {
|
|
if (hostPort) {
|
|
throw `hostPort already connected in untar.js`;
|
|
}
|
|
hostPort = port;
|
|
port.onmessage = onmessage;
|
|
}
|
|
|
|
export function disconnect() {
|
|
if (!hostPort) {
|
|
throw `hostPort was not connected in unzip.js`;
|
|
}
|
|
|
|
hostPort = null;
|
|
|
|
unarchiveState = UnarchiveState.NOT_STARTED;
|
|
bytestream = null;
|
|
allLocalFiles = null;
|
|
logToConsole = false;
|
|
|
|
currentFilename = '';
|
|
currentFileNumber = 0;
|
|
currentBytesUnarchivedInFile = 0;
|
|
currentBytesUnarchived = 0;
|
|
totalUncompressedBytesInArchive = 0;
|
|
totalFilesInArchive = 0;
|
|
}
|