@obsidize/tar-browserify
Version:
Browser-based tar utility for packing and unpacking tar files (stream-capable)
236 lines (235 loc) • 10.5 kB
JavaScript
import { __asyncValues, __awaiter } from "tslib";
import { InMemoryAsyncUint8Array } from '../common/async-uint8-array';
import { AsyncUint8ArrayIterator } from '../common/async-uint8-array-iterator';
import { Constants } from '../common/constants';
import { TarUtility } from '../common/tar-utility';
import { TarEntry } from '../entry/tar-entry';
import { PaxTarHeader } from '../header/pax/pax-tar-header';
import { TarHeader } from '../header/tar-header';
import { TarHeaderUtility } from '../header/tar-header-utility';
import { UstarHeader } from '../header/ustar/ustar-header';
const MAX_LOADED_BYTES = Constants.SECTOR_SIZE * 100000; // ~50Mb
/**
* Errors that will be thrown if the reader encounters an invalid data layout
*/
export var ArchiveReadError;
(function (ArchiveReadError) {
/**
* Occurs when the reader fails to fully load the content buffer of an entry
* due to the input data stream ending prematurely.
*/
ArchiveReadError["ERR_ENTRY_CONTENT_MIN_BUFFER_LENGTH_NOT_MET"] = "ERR_ENTRY_CONTENT_MIN_BUFFER_LENGTH_NOT_MET";
/**
* Occurs when the reader fails to fully load a PAX header
* due to the input data stream ending prematurely.
*/
ArchiveReadError["ERR_HEADER_PAX_MIN_BUFFER_LENGTH_NOT_MET"] = "ERR_HEADER_PAX_MIN_BUFFER_LENGTH_NOT_MET";
/**
* Occurs when the reader fails to fully load a PAX header
* due to the third and final segment not appearing in the input data stream.
*/
ArchiveReadError["ERR_HEADER_MISSING_POST_PAX_SEGMENT"] = "ERR_HEADER_MISSING_POST_PAX_SEGMENT";
})(ArchiveReadError || (ArchiveReadError = {}));
/**
* Generic utility for parsing tar entries from a stream of octets via `AsyncUint8ArrayIterator`
*/
export class ArchiveReader {
constructor(bufferIterator) {
this.bufferIterator = bufferIterator;
this.mGlobalPaxHeaders = [];
this.mBufferCache = null;
this.mOffset = 0;
this.mHasSyncInput = false;
}
static wrap(archiveContent) {
return __awaiter(this, void 0, void 0, function* () {
const stream = TarUtility.isUint8Array(archiveContent)
? new InMemoryAsyncUint8Array(archiveContent)
: archiveContent;
const iterator = new AsyncUint8ArrayIterator(stream);
const reader = new ArchiveReader(iterator);
yield reader.initialize();
return reader;
});
}
[Symbol.asyncIterator]() {
return this;
}
get source() {
return this.bufferIterator.input;
}
get globalPaxHeaders() {
return this.mGlobalPaxHeaders;
}
initialize() {
return __awaiter(this, void 0, void 0, function* () {
this.mBufferCache = null;
this.mOffset = 0;
this.mHasSyncInput = (this.bufferIterator.input instanceof InMemoryAsyncUint8Array);
yield this.bufferIterator.initialize();
});
}
readAllEntries() {
var _a, e_1, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const entries = [];
yield this.initialize();
try {
for (var _d = true, _e = __asyncValues(this), _f; _f = yield _e.next(), _a = _f.done, !_a;) {
_c = _f.value;
_d = false;
try {
const entry = _c;
entries.push(entry);
}
finally {
_d = true;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
}
finally { if (e_1) throw e_1.error; }
}
return entries;
});
}
next() {
return __awaiter(this, void 0, void 0, function* () {
const entry = yield this.tryParseNextEntry();
if (entry !== null) {
return { done: false, value: entry };
}
return { done: true, value: null };
});
}
clearBufferCache() {
this.mBufferCache = null;
this.mOffset = 0;
}
getBufferCacheSlice(start, end) {
return TarUtility.cloneUint8Array(this.mBufferCache, start, end);
}
tryRequireBufferSize(size) {
return __awaiter(this, void 0, void 0, function* () {
const buffer = yield this.requireBufferSize(size);
return buffer !== null;
});
}
requireBufferSize(size) {
return __awaiter(this, void 0, void 0, function* () {
while (!this.mBufferCache || this.mBufferCache.byteLength < size) {
if (!(yield this.loadNextChunk())) {
this.clearBufferCache();
return null;
}
}
return this.mBufferCache;
});
}
loadNextChunk() {
return __awaiter(this, void 0, void 0, function* () {
const nextChunk = yield this.bufferIterator.tryNext();
if (!nextChunk) {
return false;
}
if (this.mBufferCache) {
this.mBufferCache = TarUtility.concatUint8Arrays(this.mBufferCache, nextChunk);
}
else {
this.mBufferCache = nextChunk;
this.mOffset = 0;
}
return true;
});
}
tryParseNextEntry() {
return __awaiter(this, void 0, void 0, function* () {
const headerParseResult = yield this.tryParseNextHeader();
if (headerParseResult === null) {
this.clearBufferCache();
return null;
}
const context = this;
const { header, headerOffset, contentOffset } = headerParseResult;
const contentEnd = contentOffset + header.fileSize;
const offset = headerOffset;
// `contentEnd` may not be an even division of SECTOR_SIZE, so
// round up to the nearest sector start point after the content end.
const nextSectorStart = TarUtility.roundUpSectorOffset(contentEnd);
let content = null;
// If the buffer source is in-memory already, just read the content immediately
if (this.mHasSyncInput && header.fileSize > 0) {
if (!(yield this.tryRequireBufferSize(nextSectorStart))) {
throw ArchiveReadError.ERR_ENTRY_CONTENT_MIN_BUFFER_LENGTH_NOT_MET;
}
content = this.getBufferCacheSlice(contentOffset, contentEnd);
}
if ((nextSectorStart + Constants.SECTOR_SIZE) <= this.mBufferCache.byteLength) {
this.mBufferCache = this.getBufferCacheSlice(nextSectorStart);
this.mOffset = 0;
}
else {
this.mOffset = nextSectorStart;
}
return new TarEntry({ header, offset, content, context });
});
}
tryParseNextHeader() {
return __awaiter(this, void 0, void 0, function* () {
if (!(yield this.tryRequireBufferSize(this.mOffset + Constants.HEADER_SIZE))) {
return null;
}
let ustarOffset = TarHeaderUtility.findNextUstarSectorOffset(this.mBufferCache, this.mOffset);
// Find next ustar marker
while (ustarOffset < 0 && this.mBufferCache.byteLength < MAX_LOADED_BYTES && (yield this.loadNextChunk())) {
ustarOffset = TarHeaderUtility.findNextUstarSectorOffset(this.mBufferCache, this.mOffset);
}
// No header marker found and we ran out of bytes to load, terminate
if (ustarOffset < 0) {
this.clearBufferCache();
return null;
}
// Construct Header
let headerOffset = ustarOffset;
let headerBuffer = this.getBufferCacheSlice(headerOffset, headerOffset + Constants.HEADER_SIZE);
let ustarHeader = new UstarHeader(headerBuffer);
let header = new TarHeader({ ustar: ustarHeader });
// Advance cursor to process potential PAX header or entry content
let nextOffset = TarUtility.advanceSectorOffset(headerOffset, this.mBufferCache.byteLength);
if (ustarHeader.isPaxHeader) {
// Make sure we've buffered the pax header region and the next sector after that (next sector contains the _actual_ header)
const paxHeaderSectorEnd = nextOffset + TarUtility.roundUpSectorOffset(header.fileSize);
const requiredBufferSize = paxHeaderSectorEnd + Constants.HEADER_SIZE;
const isGlobalPax = header.isGlobalPaxHeader;
const preambleHeader = ustarHeader;
if (!(yield this.tryRequireBufferSize(requiredBufferSize))) {
throw ArchiveReadError.ERR_HEADER_PAX_MIN_BUFFER_LENGTH_NOT_MET;
}
// Parse the pax header out from the next sector
const paxHeader = PaxTarHeader.from(this.mBufferCache, nextOffset);
nextOffset = paxHeaderSectorEnd;
if (!TarHeaderUtility.isUstarSector(this.mBufferCache, nextOffset)) {
throw ArchiveReadError.ERR_HEADER_MISSING_POST_PAX_SEGMENT;
}
// The _actual_ header is AFTER the pax header, so need to do the header parse song and dance one more time
headerOffset = nextOffset;
headerBuffer = this.getBufferCacheSlice(headerOffset, headerOffset + Constants.HEADER_SIZE);
ustarHeader = new UstarHeader(headerBuffer);
nextOffset = TarUtility.advanceSectorOffsetUnclamped(nextOffset);
header = new TarHeader({
ustar: ustarHeader,
pax: paxHeader,
preamble: preambleHeader
});
if (isGlobalPax) {
this.mGlobalPaxHeaders.push(header);
}
}
return { header, headerOffset, contentOffset: nextOffset };
});
}
}