UNPKG

@obsidize/tar-browserify

Version:

Browser-based tar utility for packing and unpacking tar files (stream-capable)

236 lines (235 loc) 10.5 kB
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 }; }); } }