UNPKG

@obsidize/tar-browserify

Version:

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

266 lines (265 loc) 8.86 kB
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, }; } }