|
/** Minimal implementation of the TAR file format. |
|
* Reference: |
|
* https://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5 |
|
*/ |
|
export class Tar { |
|
private data: Map<string, Blob> = new Map(); |
|
|
|
/** Add a file to the archive. |
|
* @param {string} fileName - file name (including directory structure) |
|
* @param {Blob} contents |
|
* @memberof Tar |
|
*/ |
|
public add(fileName: string, contents: Blob) { |
|
this.data.set(fileName, contents); |
|
} |
|
|
|
/** Remove a file from the archive. |
|
* @param {string} fileName - file name (including directory structure) |
|
* @memberof Tar |
|
*/ |
|
public remove(fileName: string) { |
|
this.data.delete(fileName); |
|
} |
|
|
|
/** Serialize the archive as a Blob. |
|
* @memberof Tar |
|
*/ |
|
public serialize(): Blob { |
|
// Map.entries doesn't exist in IE |
|
let entries: Array<[string, Blob]>; |
|
if (this.data.entries) { |
|
entries = [...this.data.entries()]; |
|
} else { |
|
entries = []; |
|
this.data.forEach((value: Blob, key: string) => entries.push([key, value])); |
|
} |
|
// add two empty 512byte records |
|
return new Blob([ |
|
new Blob(entries.map(([fileName, contents]) => this.makeTarEntry(fileName, contents))), |
|
new ArrayBuffer(1024)], { |
|
type: "application/x-tar", |
|
}); |
|
} |
|
|
|
/** Create a tar file entry based on file name and contents. |
|
* @private |
|
* @param {string} fileName |
|
* @param {Blob} contents |
|
* @memberof Tar |
|
*/ |
|
private makeTarEntry = (fileName: string, contents: Blob): Blob => { |
|
// complete Blob to 512 byte |
|
const returnValue = new Blob([ |
|
this.makeTarHeader(fileName, contents.size), |
|
contents, |
|
new ArrayBuffer(512 - contents.size % 512)]); |
|
return returnValue; |
|
} |
|
|
|
/** Create a tar header record based on file name and the file size. |
|
* All metadata are preset. |
|
* @private |
|
* @param {string} fileName - file name (including directory structure) |
|
* @param {number} dataSize - size of the file in bytes. |
|
* @memberof Tar |
|
*/ |
|
private makeTarHeader = (fileName: string, dataSize: number): Blob => { |
|
let view: Int8Array; |
|
const name = new ArrayBuffer(100); |
|
const mode = new ArrayBuffer(8); |
|
const uid = new ArrayBuffer(8); |
|
const gid = new ArrayBuffer(8); |
|
const size = new ArrayBuffer(12); |
|
const mktime = new ArrayBuffer(12); |
|
const chksum = new ArrayBuffer(8); |
|
const typeflag = new ArrayBuffer(1); |
|
const linkname = new ArrayBuffer(100); |
|
// UStar fields |
|
const magic = new ArrayBuffer(6); |
|
const version = new ArrayBuffer(2); |
|
const uname = new ArrayBuffer(32); |
|
const gname = new ArrayBuffer(32); |
|
const devmajor = new ArrayBuffer(8); |
|
const devminor = new ArrayBuffer(8); |
|
const prefix = new ArrayBuffer(155); |
|
const padding = new ArrayBuffer(12); |
|
|
|
view = new Int8Array(name); |
|
fileName.split("").forEach((char, index) => { view[index] = char.charCodeAt(0); }); |
|
|
|
view = new Int8Array(mode); |
|
"000666 ".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(uid); |
|
"001000 ".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(gid); |
|
"001000 ".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
// size |
|
this.setOctalValue(size, dataSize); |
|
// set final byte to a space |
|
view = new Int8Array(size); |
|
view[11] = 32; |
|
|
|
view = new Int8Array(magic); |
|
"ustar\0".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(version); |
|
"00".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(uname); |
|
"nobody".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(gname); |
|
"nobody".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(devmajor); |
|
"000000 ".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(devminor); |
|
"000000 ".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(typeflag); |
|
view[0] = 0; |
|
// mktime |
|
view = new Int8Array(mktime); |
|
// "13354520076 ".split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
Math.floor((1 * Number(new Date()) / 1000)).toString(8) |
|
.split("").forEach((char, index) => view[index] = char.charCodeAt(0)); |
|
|
|
view = new Int8Array(typeflag); |
|
view[0] = 48; // ASCII 0 |
|
|
|
// set checksum |
|
const checkSum = this.calculateChecksum([name, mode, uid, gid, size, mktime, typeflag, linkname, |
|
magic, version, uname, gname, devmajor, devminor, prefix, padding]); |
|
this.setOctalValue(chksum, checkSum); |
|
|
|
// final byte is ASCII space, penultimate byte is ASCII NUL |
|
view = new Int8Array(chksum); |
|
view[6] = 0; |
|
view[7] = 32; |
|
|
|
return new Blob([ |
|
name, |
|
mode, |
|
uid, |
|
gid, |
|
size, |
|
mktime, |
|
chksum, |
|
typeflag, |
|
linkname, |
|
magic, |
|
version, |
|
uname, |
|
gname, |
|
devmajor, |
|
devminor, |
|
prefix, |
|
padding, |
|
]); |
|
} |
|
|
|
/** Set the content of an ArrayBuffer to the octal ASCII value of the value parameter |
|
* @private |
|
* @param {ArrayBuffer} target - Set the value of this ArrayBuffer |
|
* @param {number} value - Set the ArrayBuffer to this value |
|
* @memberof Tar |
|
*/ |
|
private setOctalValue = (target: ArrayBuffer, value: number) => { |
|
const view = new Int8Array(target); |
|
// initialize content with ASCII zeroes |
|
// -2 so that the final byte is a delimiter (NUL or space, depending on file vesion) |
|
for (let index = 0; index < (view.length - 2); index++) { view[index] = "0".charCodeAt(0); } |
|
// fill in the digits |
|
for (let index = (target.byteLength - 2); value > 0; index--) { |
|
const digit = value % 8; |
|
view[index] = String(digit).charCodeAt(0); |
|
value = (value - digit) / 8; |
|
} |
|
} |
|
|
|
/** Calculate the checksum for the provided tar header fields. |
|
* This is set in octal ASCII digits. |
|
* @private |
|
* @param {ArrayBuffer[]} headerFields |
|
* Calculate a tar header checksum based on these fields. |
|
* These fields must contain all the other header record elements |
|
* (except for the checksum field itself) |
|
* @memberof Tar |
|
*/ |
|
private calculateChecksum = (headerFields: ArrayBuffer[]): number => { |
|
const sum = (previous: number, current: number) => previous + current; |
|
// 32 * 8, because the checksum is initially assumed to be 8 ASCII space characters. |
|
const retValue = (32 * 8 + headerFields |
|
.map( |
|
(buffer) => |
|
(new Int8Array(buffer)) |
|
.reduce(sum)) |
|
.reduce(sum)) * 8; |
|
// * 8 is to shift everything by one octal digit |
|
// (thus the last digit can be set to the delimiter NUL / space) |
|
return retValue; |
|
} |
|
} |