@obsidize/tar-browserify
Version:
Browser-based tar utility for packing and unpacking tar files (stream-capable)
266 lines (265 loc) • 8.86 kB
JavaScript
import { Constants } from '../../common/constants';
import { TarUtility } from '../../common/tar-utility';
import { UstarHeaderField } from '../ustar/ustar-header-field';
import { PaxHeaderKey } from './pax-header-key';
import { PaxHeaderSegment } from './pax-header-segment';
import { PaxHeaderUtility } from './pax-header-utility';
/**
* Adds support for extended headers.
* https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_03
*/
export class PaxHeader {
constructor(segments = []) {
this.valueMap = {};
for (const segment of segments) {
this.valueMap[segment.key] = segment;
}
}
static deserialize(buffer, offset = 0) {
const segments = PaxHeader.deserializeSegments(buffer, offset);
return new PaxHeader(segments);
}
static fromAttributes(attributes) {
const segments = PaxHeader.parseSegmentsFromAttributes(attributes);
return new PaxHeader(segments);
}
static serializeAttributes(attributes) {
const segments = PaxHeader.parseSegmentsFromAttributes(attributes);
return PaxHeader.serializeSegments(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 PaxHeaderSegment(key, strVal));
}
return segments;
}
static serializeSegments(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 PaxHeader.insertPaxAt(fileName, '/', sepIndex);
}
sepIndex = fileName.lastIndexOf('\\');
if (sepIndex >= 0) {
return PaxHeader.insertPaxAt(fileName, '\\', sepIndex);
}
return PaxHeader.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 PaxHeader.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 deserializeSegments(buffer, offset) {
const result = [];
let cursor = offset;
let next = PaxHeaderSegment.deserialize(buffer, cursor);
while (next !== null) {
result.push(next);
cursor += next.bytes.byteLength;
next = PaxHeaderSegment.deserialize(buffer, cursor);
}
return result;
}
/**
* See `PaxHeaderKey.ACCESS_TIME` for more info
*/
get accessTime() {
return this.getFloat(PaxHeaderKey.ACCESS_TIME);
}
/**
* See `PaxHeaderKey.CHARSET` for more info
*/
get charset() {
return this.get(PaxHeaderKey.CHARSET);
}
/**
* See `PaxHeaderKey.COMMENT` for more info
*/
get comment() {
return this.get(PaxHeaderKey.COMMENT);
}
/**
* See `PaxHeaderKey.GROUP_ID` for more info
*/
get groupId() {
return this.getInt(PaxHeaderKey.GROUP_ID);
}
/**
* See `PaxHeaderKey.GROUP_NAME` for more info
*/
get groupName() {
return this.get(PaxHeaderKey.GROUP_NAME);
}
/**
* See `PaxHeaderKey.HDR_CHARSET` for more info
*/
get hdrCharset() {
return this.get(PaxHeaderKey.HDR_CHARSET);
}
/**
* See `PaxHeaderKey.LINK_PATH` for more info
*/
get linkPath() {
return this.get(PaxHeaderKey.LINK_PATH);
}
/**
* See `PaxHeaderKey.MODIFICATION_TIME` for more info
*/
get modificationTime() {
return this.getFloat(PaxHeaderKey.MODIFICATION_TIME);
}
/**
* See `PaxHeaderKey.PATH` for more info
*/
get path() {
return this.get(PaxHeaderKey.PATH);
}
/**
* See `PaxHeaderKey.SIZE` for more info
*/
get size() {
return this.getInt(PaxHeaderKey.SIZE);
}
/**
* See `PaxHeaderKey.USER_ID` for more info
*/
get userId() {
return this.getInt(PaxHeaderKey.USER_ID);
}
/**
* See `PaxHeaderKey.USER_NAME` for more info
*/
get userName() {
return this.get(PaxHeaderKey.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 (!PaxHeaderUtility.isKnownHeaderKey(key)) {
delete this.valueMap[key];
}
}
return this;
}
/**
* @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) {
return this.valueMap[key]?.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) {
return this.valueMap[key]?.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) {
return this.valueMap[key]?.floatValue;
}
/**
* Serializes the underlying value map of this instance into a set of PAX sectors.
*/
toUint8Array() {
return PaxHeader.serializeSegments(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: attributes } = this;
const bytes = this.toUint8Array();
const buffer = TarUtility.getDebugBufferJson(bytes);
return {
attributes,
buffer,
};
}
}