@obsidize/tar-browserify
Version:
Browser-based tar utility for packing and unpacking tar files (stream-capable)
203 lines (202 loc) • 7.54 kB
JavaScript
import { TarUtility } from '../common/tar-utility';
import { TarHeader } from '../header/tar-header';
/**
* Container for metadata and content of a tarball entry.
*
* Here, we consider an "entry" to be a tuple of:
* 1. The parsed USTAR header sector content (AKA TarHeader)
* 2. The aggregate of the proceeding file content sectors, based on the header's file size attribute
*/
export class ArchiveEntry {
constructor(options = {}) {
let { header, headerAttributes, headerByteLength, content, offset, context } = options;
if (!header)
header = TarHeader.fromAttributes(headerAttributes || {});
if (!content)
content = null;
if (!offset)
offset = 0;
if (!context)
context = null;
const contentLength = TarUtility.sizeofUint8Array(content);
// The fileSize field metadata must always be in sync between the content and the header
if (!header.pax && header.fileSize !== contentLength && contentLength > 0) {
header.ustar.fileSize = contentLength;
}
// run this last since toUint8Array() also syncs checksums
if (!headerByteLength)
headerByteLength = header.toUint8Array().byteLength;
this.mHeader = header;
this.mHeaderByteLength = headerByteLength;
this.mContent = content;
this.mOffset = offset;
this.mContext = context;
}
static isArchiveEntry(v) {
return !!(v && v instanceof ArchiveEntry);
}
// =================================================================
// TarHeader Interface Fields
// =================================================================
get fileName() {
return this.header.fileName;
}
get fileSize() {
return this.header.fileSize;
}
get fileMode() {
return this.header.fileMode;
}
get ownerUserId() {
return this.header.ownerUserId;
}
get groupUserId() {
return this.header.groupUserId;
}
get lastModified() {
return this.header.lastModified;
}
get headerChecksum() {
return this.header.headerChecksum;
}
get linkedFileName() {
return this.header.linkedFileName;
}
get typeFlag() {
return this.header.typeFlag;
}
get ustarIndicator() {
return this.header.ustarIndicator;
}
get ustarVersion() {
return this.header.ustarVersion;
}
get ownerUserName() {
return this.header.ownerUserName;
}
get ownerGroupName() {
return this.header.ownerGroupName;
}
get deviceMajorNumber() {
return this.header.deviceMajorNumber;
}
get deviceMinorNumber() {
return this.header.deviceMinorNumber;
}
get fileNamePrefix() {
return this.header.fileNamePrefix;
}
// =================================================================
// Introspection Fields
// =================================================================
/**
* The header metadata parsed out for this entry.
* If you are attempting to read the content of this entry,
* do not modify this instance.
*/
get header() {
return this.mHeader;
}
/**
* The file content for this entry.
* This may be null for entries loaded asynchronously, or
* for non-file entries like directories.
*/
get content() {
return this.mContent;
}
/**
* The starting absolute index (inclusive) in the source buffer that this entry was parsed from.
* Returns zero by default if this was not parsed by a source buffer.
*/
get sourceOffset() {
return this.mOffset;
}
/**
* The size in bytes of the header in the source buffer that this entry was parsed from.
* Returns zero by default if this was not parsed by a source buffer.
*/
get sourceHeaderByteLength() {
return this.mHeaderByteLength;
}
/**
* The context (if any) from which this entry was parsed.
* The context will include global data about things such as
* the origin of the archive and global pax headers.
*/
get sourceContext() {
return this.mContext;
}
isDirectory() {
return this.header.isDirectoryHeader;
}
isFile() {
return this.header.isFileHeader;
}
/**
* Convenience for decoding the current content buffer as a string.
* Note that if the content was not loaded for whatever reason, this
* will return an empty string.
* @returns The decoded string data from the currently assigned content,
* or an empty string if there is no content assigned.
*/
text() {
return TarUtility.decodeString(this.content);
}
/**
* Only necessary if this entry was extracted from an async buffer, since the entry
* does not hold the content of async buffers by default.
*
* If the entry was extracted synchronously, its content will be available via the "content" property.
*
* Do not use this on entries that have not been parsed from a source buffer,
* otherwise it will very likely return garbage data.
*
* @param buffer - the source to read content from
* @param offset - the _relative_ offset of the content to read;
* setting this to 42 will start reading at the 42nd byte index within the content block
* @param length - the number of bytes to read after the offset
*/
async readContentFrom(buffer, offset = 0, length = 0) {
const fileSize = this.fileSize;
const contentStartIndex = this.sourceOffset + this.sourceHeaderByteLength;
const contentEndIndex = contentStartIndex + fileSize;
const absoluteOffset = contentStartIndex + TarUtility.clamp(offset, 0, fileSize);
const bytesRemaining = Math.max(0, contentEndIndex - absoluteOffset);
const normalizedLength = length > 0 ? Math.min(length, bytesRemaining) : bytesRemaining;
return buffer.read(absoluteOffset, normalizedLength);
}
/**
* @returns This instance serialized as a single slice for a tar buffer
*/
toUint8Array() {
const headerBytes = this.header.toUint8Array();
const contentLength = this.content?.byteLength ?? 0;
const outputLength = TarUtility.roundUpSectorOffset(headerBytes.byteLength + contentLength);
const result = new Uint8Array(outputLength);
result.set(headerBytes, 0);
if (contentLength > 0) {
result.set(this.content, headerBytes.byteLength);
}
return result;
}
/**
* Overridden to prevent circular reference errors / huge memory spikes that would
* include the underlying content by default.
*/
toJSON() {
const { header, fileName: name, fileSize: size, content } = this;
const isFile = this.isFile();
const isDirectory = this.isDirectory();
const type = isFile ? 'file' : isDirectory ? 'directory' : 'complex';
const contentType = content ? 'Uint8Array[' + content.byteLength + ']' : 'null';
return {
name,
size,
type,
header,
contentType,
content: TarUtility.getDebugHexString(content),
};
}
}