@obsidize/tar-browserify
Version:
Browser-based tar utility for packing and unpacking tar files (stream-capable)
293 lines (292 loc) • 10 kB
JavaScript
import { Constants } from '../../common/constants';
import { TarUtility } from '../../common/tar-utility';
import { UstarHeaderField } from '../ustar/ustar-header-field';
import { PaxTarHeaderKey } from './pax-tar-header-key';
import { PaxTarHeaderSegment } from './pax-tar-header-segment';
import { PaxTarHeaderUtility } from './pax-tar-header-utility';
/**
* Adds support for extended headers.
* https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_03
*/
export class PaxTarHeader {
constructor(segments = []) {
this.valueMap = {};
for (const segment of segments) {
this.valueMap[segment.key] = segment;
}
}
static from(buffer, offset = 0) {
const segments = PaxTarHeader.parseSegments(buffer, offset);
return new PaxTarHeader(segments);
}
static fromAttributes(attributes) {
const segments = PaxTarHeader.parseSegmentsFromAttributes(attributes);
return new PaxTarHeader(segments);
}
static serializeAttributes(attributes) {
const segments = PaxTarHeader.parseSegmentsFromAttributes(attributes);
return PaxTarHeader.serialize(segments);
}
static parseSegmentsFromAttributes(attributes) {
if (!TarUtility.isObject(attributes)) {
return [];
}
const segments = [];
for (const [key, value] of Object.entries(attributes)) {
const strVal = TarUtility.isString(value) ? value : String(value);
segments.push(new PaxTarHeaderSegment(key, strVal));
}
return segments;
}
static serialize(segments) {
if (!Array.isArray(segments) || segments.length <= 0) {
return new Uint8Array(0);
}
let totalLength = 0;
let segmentBuffers = [];
for (const segment of segments) {
const encodedSegment = segment.toUint8Array();
segmentBuffers.push(encodedSegment);
totalLength += encodedSegment.byteLength;
}
const resultBuffer = new Uint8Array(totalLength);
let offset = 0;
for (const segmentBuffer of segmentBuffers) {
resultBuffer.set(segmentBuffer, offset);
offset += segmentBuffer.byteLength;
}
return resultBuffer;
}
/**
* Wraps the given file name (if necessary) with the 'PaxHeader' metadata indicator.
* If the indicator already exists in the given file name, this does nothing.
*/
static wrapFileName(fileName) {
if (!TarUtility.isString(fileName) || fileName.includes(Constants.PAX_HEADER_PREFIX)) {
return fileName;
}
let sepIndex = fileName.lastIndexOf('/');
if (sepIndex >= 0) {
return PaxTarHeader.insertPaxAt(fileName, '/', sepIndex);
}
sepIndex = fileName.lastIndexOf('\\');
if (sepIndex >= 0) {
return PaxTarHeader.insertPaxAt(fileName, '\\', sepIndex);
}
return PaxTarHeader.makeTopLevelPrefix(fileName, '/', 0);
}
static insertPaxAt(fileName, separator, offset) {
const maxLength = UstarHeaderField.fileName.size;
if (fileName.length < maxLength) {
return fileName.substring(0, offset)
+ separator + Constants.PAX_HEADER_PREFIX
+ fileName.substring(offset);
}
return PaxTarHeader.makeTopLevelPrefix(fileName, '/', offset + 1);
}
static makeTopLevelPrefix(fileName, separator, offset) {
const maxLength = UstarHeaderField.fileName.size;
// Dark magic observed from existing tar files
let result = Constants.PAX_HEADER_PREFIX + separator + fileName.substring(offset);
if (result.length > maxLength) {
// Dark magic observed from existing tar files
result = result.substring(0, maxLength - 2) + '\0\0';
}
return result;
}
static parseSegments(buffer, offset) {
const result = [];
let cursor = offset;
let next = PaxTarHeaderSegment.tryParse(buffer, cursor);
while (next !== null) {
result.push(next);
cursor += next.bytes.byteLength;
next = PaxTarHeaderSegment.tryParse(buffer, cursor);
}
return result;
}
/**
* See `PaxTarHeaderKey.ACCESS_TIME` for more info
*/
get accessTime() {
return this.getFloat(PaxTarHeaderKey.ACCESS_TIME);
}
/**
* See `PaxTarHeaderKey.CHARSET` for more info
*/
get charset() {
return this.get(PaxTarHeaderKey.CHARSET);
}
/**
* See `PaxTarHeaderKey.COMMENT` for more info
*/
get comment() {
return this.get(PaxTarHeaderKey.COMMENT);
}
/**
* See `PaxTarHeaderKey.GROUP_ID` for more info
*/
get groupId() {
return this.getInt(PaxTarHeaderKey.GROUP_ID);
}
/**
* See `PaxTarHeaderKey.GROUP_NAME` for more info
*/
get groupName() {
return this.get(PaxTarHeaderKey.GROUP_NAME);
}
/**
* See `PaxTarHeaderKey.HDR_CHARSET` for more info
*/
get hdrCharset() {
return this.get(PaxTarHeaderKey.HDR_CHARSET);
}
/**
* See `PaxTarHeaderKey.LINK_PATH` for more info
*/
get linkPath() {
return this.get(PaxTarHeaderKey.LINK_PATH);
}
/**
* See `PaxTarHeaderKey.MODIFICATION_TIME` for more info
*/
get modificationTime() {
return this.getFloat(PaxTarHeaderKey.MODIFICATION_TIME);
}
/**
* See `PaxTarHeaderKey.PATH` for more info
*/
get path() {
return this.get(PaxTarHeaderKey.PATH);
}
/**
* See `PaxTarHeaderKey.SIZE` for more info
*/
get size() {
return this.getInt(PaxTarHeaderKey.SIZE);
}
/**
* See `PaxTarHeaderKey.USER_ID` for more info
*/
get userId() {
return this.getInt(PaxTarHeaderKey.USER_ID);
}
/**
* See `PaxTarHeaderKey.USER_NAME` for more info
*/
get userName() {
return this.get(PaxTarHeaderKey.USER_NAME);
}
/**
* Converts modificationTime to standard javascript epoch time.
*/
get lastModified() {
const mtime = this.modificationTime;
return mtime ? TarUtility.paxTimeToDate(mtime) : undefined;
}
/**
* @returns an array of the keys in this header
*/
keys() {
return Object.keys(this.valueMap);
}
/**
* @returns an array of the segments in this header
*/
values() {
return Object.values(this.valueMap);
}
/**
* Removes any unknown or un-standardized keys from this header.
* @returns `this` for operation chaining
*/
clean() {
for (const key of this.keys()) {
if (!PaxTarHeaderUtility.isKnownHeaderKey(key)) {
delete this.valueMap[key];
}
}
return this;
}
// /**
// * @returns The total byte-length of this header in serialized form.
// */
// public calculateByteLength(): number {
// this.calculateByteLengthAttributes();
// return this.mByteLength!;
// }
// /**
// * @returns The total byte-length of this header in serialized form,
// * padded to be a multiple of SECTOR_SIZE.
// */
// public calculateSectorByteLength(): number {
// this.calculateByteLengthAttributes();
// return this.mSectorByteLength!;
// }
// private calculateByteLengthAttributes(): void {
// const bytes = this.toUint8Array();
// this.mByteLength = bytes.byteLength;
// this.mSectorByteLength = TarUtility.roundUpSectorOffset(this.mByteLength);
// }
/**
* @returns true if the value map of this parsed header contains the given key
*/
has(key) {
return TarUtility.isDefined(this.valueMap[key]);
}
/**
* @returns the value parsed from the bytes of this header for the given key,
* or `undefined` if the key did not exist in the header.
*/
get(key) {
var _a;
return (_a = this.valueMap[key]) === null || _a === void 0 ? void 0 : _a.value;
}
/**
* Parse the value for the given key as an int.
* @returns undefined if the key does not exist or the parse operation fails.
*/
getInt(key) {
var _a;
return (_a = this.valueMap[key]) === null || _a === void 0 ? void 0 : _a.intValue;
}
/**
* Parse the value for the given key as a float.
* @returns undefined if the key does not exist or the parse operation fails.
*/
getFloat(key) {
var _a;
return (_a = this.valueMap[key]) === null || _a === void 0 ? void 0 : _a.floatValue;
}
/**
* Serializes the underlying value map of this instance into a set of PAX sectors.
*/
toUint8Array() {
return PaxTarHeader.serialize(this.values());
}
/**
* Adds any necessary padding to the serialized output to ensure the length
* of the output is a multiple of `SECTOR_SIZE`.
*
* See `toUint8Array()` for more info.
*/
toUint8ArrayPadded() {
const serializedBuffer = this.toUint8Array();
let delta = TarUtility.getSectorOffsetDelta(serializedBuffer.byteLength);
if (delta > 0) {
return TarUtility.concatUint8Arrays(serializedBuffer, new Uint8Array(delta));
}
return serializedBuffer;
}
toJSON() {
const { valueMap } = this;
const bytes = this.toUint8Array();
const byteLength = bytes.byteLength;
const content = TarUtility.getDebugHexString(bytes);
return {
valueMap,
byteLength,
content
};
}
}