Skip to content

Instantly share code, notes, and snippets.

@berdosi
Last active October 12, 2018 18:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save berdosi/247416670475f01250d93ddfb799a34b to your computer and use it in GitHub Desktop.
Save berdosi/247416670475f01250d93ddfb799a34b to your computer and use it in GitHub Desktop.
Minimal implementation for the tar file format, based on https://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5
/** 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;
}
}

ISC License

Copyright (c) 2018, Balint Erdosi erdosib@gmail.com

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

To compile,

  1. Have TypeScript installed
  2. Run tsc tar.ts
import { Tar } from "./tar";
const tar = new Tar();
tar.add("hello.txt", new Blob(["hello world!!!"]));
tar.add("szia.txt", new Blob(["szia világ!!!"]));
tar.add("test/directory/file", new Blob(["some file contents here"]));
console.log(URL.createObjectURL(tar.serialize()));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment