diff --git a/src/js/config/Categories.js b/src/js/config/Categories.js index a1aa43de..61e75568 100755 --- a/src/js/config/Categories.js +++ b/src/js/config/Categories.js @@ -212,6 +212,8 @@ var Categories = [ "Zip", "Unzip", "Bzip2 Decompress", + "Tar", + "Untar", ] }, { diff --git a/src/js/config/OperationConfig.js b/src/js/config/OperationConfig.js index 70a032c6..ba406693 100755 --- a/src/js/config/OperationConfig.js +++ b/src/js/config/OperationConfig.js @@ -3042,5 +3042,26 @@ var OperationConfig = { value: MorseCode.WORD_DELIM_OPTIONS } ] + }, + "Tar": { + description: "Packs the input into a tarball.

No support for multiple files at this time.", + run: Compress.runTar, + inputType: "byteArray", + outputType: "byteArray", + args: [ + { + name: "Filename", + type: "string", + value: Compress.TAR_FILENAME + } + ] + }, + "Untar": { + description: "Unpacks a tarball and displays it per file.", + run: Compress.runUntar, + inputType: "byteArray", + outputType: "html", + args: [ + ] } }; diff --git a/src/js/core/Utils.js b/src/js/core/Utils.js index d3d99cdd..96692e76 100755 --- a/src/js/core/Utils.js +++ b/src/js/core/Utils.js @@ -93,6 +93,42 @@ var Utils = { }, + /** + * Adds trailing bytes to a byteArray. + * + * @author tlwr [toby@toby.codes] + * + * @param {byteArray} arr - byteArray to add trailing bytes to. + * @param {number} numBytes - Maximum width of the array. + * @param {Integer} [padByte=0] - The byte to pad with. + * @returns {byteArray} + * + * @example + * // returns ["a", 0, 0, 0] + * Utils.padBytesRight("a", 4); + * + * // returns ["a", 1, 1, 1] + * Utils.padBytesRight("a", 4, 1); + * + * // returns ["t", "e", "s", "t", 0, 0, 0, 0] + * Utils.padBytesRight("test", 8); + * + * // returns ["t", "e", "s", "t", 1, 1, 1, 1] + * Utils.padBytesRight("test", 8, 1); + */ + padBytesRight: function(arr, numBytes, padByte) { + padByte = padByte || 0; + var paddedBytes = new Array(numBytes); + paddedBytes.fill(padByte); + + Array.prototype.map.call(arr, function(b, i) { + paddedBytes[i] = b; + }); + + return paddedBytes; + }, + + /** * @alias Utils.padLeft */ @@ -929,6 +965,71 @@ var Utils = { }, + /** + * Formats a list of files or directories. + * A File is an object with a "fileName" and optionally a "contents". + * If the fileName ends with "/" and the contents is of length 0 then + * it is considered a directory. + * + * @author tlwr [toby@toby.codes] + * + * @param {Object[]} files + * @returns {html} + */ + displayFilesAsHTML: function(files){ + var formatDirectory = function(file) { + var html = "
" + + "" + + "
"; + return html; + }; + + var formatFile = function(file, i) { + var html = "
" + + "" + + "
" + + "
" + + "
" + Utils.escapeHtml(file.contents) + "
" + + "
" + + "
"; + return html; + }; + + var html = "
" + + files.length + + " file(s) found
\n"; + files.forEach(function(file, i) { + if (typeof file.contents !== "undefined") { + html += formatFile(file, i); + } else { + html += formatDirectory(file); + } + }); + return html; + }, + + /** * Actual modulo function, since % is actually the remainder function in JS. * diff --git a/src/js/operations/Compress.js b/src/js/operations/Compress.js index 741f137c..6e756264 100755 --- a/src/js/operations/Compress.js +++ b/src/js/operations/Compress.js @@ -304,28 +304,29 @@ var Compress = { password: Utils.strToByteArray(args[0]), verify: args[1] }, - file = "", unzip = new Zlib.Unzip(input, options), filenames = unzip.getFilenames(), - output = "
" + filenames.length + " file(s) found
\n"; + files = []; - output += "
"; + filenames.forEach(function(fileName) { + var contents = unzip.decompress(fileName); - window.uzip = unzip; - for (var i = 0; i < filenames.length; i++) { - file = Utils.byteArrayToUtf8(unzip.decompress(filenames[i])); - output += "
" + - "" + - "
" + - "
" + - Utils.escapeHtml(file) + "
"; - } + contents = Utils.byteArrayToUtf8(contents); - return output + "
"; + var file = { + fileName: fileName, + size: contents.length, + }; + + var isDir = contents.length === 0 && fileName.endsWith("/"); + if (!isDir) { + file.contents = contents; + } + + files.push(file); + }); + + return Utils.displayFilesAsHTML(files); }, @@ -346,4 +347,207 @@ var Compress = { return plain; }, + + /** + * @constant + * @default + */ + TAR_FILENAME: "file.txt", + + + /** + * Tar pack operation. + * + * @author tlwr [toby@toby.codes] + * + * @param {byteArray} input + * @param {Object[]} args + * @returns {byteArray} + */ + runTar: function(input, args) { + var Tarball = function() { + this.bytes = new Array(512); + this.position = 0; + }; + + Tarball.prototype.addEmptyBlock = function() { + var filler = new Array(512); + filler.fill(0); + this.bytes = this.bytes.concat(filler); + }; + + Tarball.prototype.writeBytes = function(bytes) { + var self = this; + + if (this.position + bytes.length > this.bytes.length) { + this.addEmptyBlock(); + } + + Array.prototype.forEach.call(bytes, function(b, i) { + if (typeof b.charCodeAt !== "undefined") { + b = b.charCodeAt(); + } + + self.bytes[self.position] = b; + self.position += 1; + }); + }; + + Tarball.prototype.writeEndBlocks = function() { + var numEmptyBlocks = 2; + for (var i = 0; i < numEmptyBlocks; i++) { + this.addEmptyBlock(); + } + }; + + var fileSize = Utils.padLeft(input.length.toString(8), 11, "0"); + var currentUnixTimestamp = Math.floor(Date.now() / 1000); + var lastModTime = Utils.padLeft(currentUnixTimestamp.toString(8), 11, "0"); + + var file = { + fileName: Utils.padBytesRight(args[0], 100), + fileMode: Utils.padBytesRight("0000664", 8), + ownerUID: Utils.padBytesRight("0", 8), + ownerGID: Utils.padBytesRight("0", 8), + size: Utils.padBytesRight(fileSize, 12), + lastModTime: Utils.padBytesRight(lastModTime, 12), + checksum: " ", + type: "0", + linkedFileName: Utils.padBytesRight("", 100), + USTARFormat: Utils.padBytesRight("ustar", 6), + version: "00", + ownerUserName: Utils.padBytesRight("", 32), + ownerGroupName: Utils.padBytesRight("", 32), + deviceMajor: Utils.padBytesRight("", 8), + deviceMinor: Utils.padBytesRight("", 8), + fileNamePrefix: Utils.padBytesRight("", 155), + }; + + var checksum = 0; + for (var key in file) { + var bytes = file[key]; + Array.prototype.forEach.call(bytes, function(b) { + if (typeof b.charCodeAt !== "undefined") { + checksum += b.charCodeAt(); + } else { + checksum += b; + } + }); + } + checksum = Utils.padBytesRight(Utils.padLeft(checksum.toString(8), 7, "0"), 8); + file.checksum = checksum; + + var tarball = new Tarball(); + tarball.writeBytes(file.fileName); + tarball.writeBytes(file.fileMode); + tarball.writeBytes(file.ownerUID); + tarball.writeBytes(file.ownerGID); + tarball.writeBytes(file.size); + tarball.writeBytes(file.lastModTime); + tarball.writeBytes(file.checksum); + tarball.writeBytes(file.type); + tarball.writeBytes(file.linkedFileName); + tarball.writeBytes(file.USTARFormat); + tarball.writeBytes(file.version); + tarball.writeBytes(file.ownerUserName); + tarball.writeBytes(file.ownerGroupName); + tarball.writeBytes(file.deviceMajor); + tarball.writeBytes(file.deviceMinor); + tarball.writeBytes(file.fileNamePrefix); + tarball.writeBytes(Utils.padBytesRight("", 12)); + tarball.writeBytes(input); + tarball.writeEndBlocks(); + + return tarball.bytes; + }, + + + /** + * Untar unpack operation. + * + * @author tlwr [toby@toby.codes] + * + * @param {byteArray} input + * @param {Object[]} args + * @returns {html} + */ + runUntar: function(input, args) { + var Stream = function(input) { + this.bytes = input; + this.position = 0; + }; + + Stream.prototype.readString = function(numBytes) { + var result = ""; + for (var i = this.position; i < this.position + numBytes; i++) { + var currentByte = this.bytes[i]; + if (currentByte === 0) break; + result += String.fromCharCode(currentByte); + } + this.position += numBytes; + return result; + }; + + Stream.prototype.readInt = function(numBytes, base) { + var string = this.readString(numBytes); + return parseInt(string, base); + }; + + Stream.prototype.hasMore = function() { + return this.position < this.bytes.length; + }; + + var stream = new Stream(input), + files = []; + + while (stream.hasMore()) { + var dataPosition = stream.position + 512; + + var file = { + fileName: stream.readString(100), + fileMode: stream.readString(8), + ownerUID: stream.readString(8), + ownerGID: stream.readString(8), + size: parseInt(stream.readString(12), 8), // Octal + lastModTime: new Date(1000 * stream.readInt(12, 8)), // Octal + checksum: stream.readString(8), + type: stream.readString(1), + linkedFileName: stream.readString(100), + USTARFormat: stream.readString(6).indexOf("ustar") >= 0, + }; + + if (file.USTARFormat) { + file.version = stream.readString(2); + file.ownerUserName = stream.readString(32); + file.ownerGroupName = stream.readString(32); + file.deviceMajor = stream.readString(8); + file.deviceMinor = stream.readString(8); + file.filenamePrefix = stream.readString(155); + } + + stream.position = dataPosition; + + if (file.type === "0") { + // File + files.push(file); + var endPosition = stream.position + file.size; + if (file.size % 512 !== 0) { + endPosition += 512 - (file.size % 512); + } + + file.contents = ""; + + while (stream.position < endPosition) { + file.contents += stream.readString(512); + } + } else if (file.type === "5") { + // Directory + files.push(file); + } else { + // Symlink or empty bytes + } + } + + return Utils.displayFilesAsHTML(files); + }, };