TrinetraLabs/Placebo_AI
0
1"use strict";2 3var utils = require("../utils");4var GenericWorker = require("../stream/GenericWorker");5var utf8 = require("../utf8");6var crc32 = require("../crc32");7var signature = require("../signature");8 9/**10 * Transform an integer into a string in hexadecimal.11 * @private12 * @param {number} dec the number to convert.13 * @param {number} bytes the number of bytes to generate.14 * @returns {string} the result.15 */16var decToHex = function(dec, bytes) {17 var hex = "", i;18 for (i = 0; i < bytes; i++) {19 hex += String.fromCharCode(dec & 0xff);20 dec = dec >>> 8;21 }22 return hex;23};24 25/**26 * Generate the UNIX part of the external file attributes.27 * @param {Object} unixPermissions the unix permissions or null.28 * @param {Boolean} isDir true if the entry is a directory, false otherwise.29 * @return {Number} a 32 bit integer.30 *31 * adapted from http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute :32 *33 * TTTTsstrwxrwxrwx0000000000ADVSHR34 * ^^^^____________________________ file type, see zipinfo.c (UNX_*)35 * ^^^_________________________ setuid, setgid, sticky36 * ^^^^^^^^^________________ permissions37 * ^^^^^^^^^^______ not used ?38 * ^^^^^^ DOS attribute bits : Archive, Directory, Volume label, System file, Hidden, Read only39 */40var generateUnixExternalFileAttr = function (unixPermissions, isDir) {41 42 var result = unixPermissions;43 if (!unixPermissions) {44 // I can't use octal values in strict mode, hence the hexa.45 // 040775 => 0x41fd46 // 0100664 => 0x81b447 result = isDir ? 0x41fd : 0x81b4;48 }49 return (result & 0xFFFF) << 16;50};51 52/**53 * Generate the DOS part of the external file attributes.54 * @param {Object} dosPermissions the dos permissions or null.55 * @param {Boolean} isDir true if the entry is a directory, false otherwise.56 * @return {Number} a 32 bit integer.57 *58 * Bit 0 Read-Only59 * Bit 1 Hidden60 * Bit 2 System61 * Bit 3 Volume Label62 * Bit 4 Directory63 * Bit 5 Archive64 */65var generateDosExternalFileAttr = function (dosPermissions) {66 // the dir flag is already set for compatibility67 return (dosPermissions || 0) & 0x3F;68};69 70/**71 * Generate the various parts used in the construction of the final zip file.72 * @param {Object} streamInfo the hash with information about the compressed file.73 * @param {Boolean} streamedContent is the content streamed ?74 * @param {Boolean} streamingEnded is the stream finished ?75 * @param {number} offset the current offset from the start of the zip file.76 * @param {String} platform let's pretend we are this platform (change platform dependents fields)77 * @param {Function} encodeFileName the function to encode the file name / comment.78 * @return {Object} the zip parts.79 */80var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName) {81 var file = streamInfo["file"],82 compression = streamInfo["compression"],83 useCustomEncoding = encodeFileName !== utf8.utf8encode,84 encodedFileName = utils.transformTo("string", encodeFileName(file.name)),85 utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)),86 comment = file.comment,87 encodedComment = utils.transformTo("string", encodeFileName(comment)),88 utfEncodedComment = utils.transformTo("string", utf8.utf8encode(comment)),89 useUTF8ForFileName = utfEncodedFileName.length !== file.name.length,90 useUTF8ForComment = utfEncodedComment.length !== comment.length,91 dosTime,92 dosDate,93 extraFields = "",94 unicodePathExtraField = "",95 unicodeCommentExtraField = "",96 dir = file.dir,97 date = file.date;98 99 100 var dataInfo = {101 crc32 : 0,102 compressedSize : 0,103 uncompressedSize : 0104 };105 106 // if the content is streamed, the sizes/crc32 are only available AFTER107 // the end of the stream.108 if (!streamedContent || streamingEnded) {109 dataInfo.crc32 = streamInfo["crc32"];110 dataInfo.compressedSize = streamInfo["compressedSize"];111 dataInfo.uncompressedSize = streamInfo["uncompressedSize"];112 }113 114 var bitflag = 0;115 if (streamedContent) {116 // Bit 3: the sizes/crc32 are set to zero in the local header.117 // The correct values are put in the data descriptor immediately118 // following the compressed data.119 bitflag |= 0x0008;120 }121 if (!useCustomEncoding && (useUTF8ForFileName || useUTF8ForComment)) {122 // Bit 11: Language encoding flag (EFS).123 bitflag |= 0x0800;124 }125 126 127 var extFileAttr = 0;128 var versionMadeBy = 0;129 if (dir) {130 // dos or unix, we set the dos dir flag131 extFileAttr |= 0x00010;132 }133 if(platform === "UNIX") {134 versionMadeBy = 0x031E; // UNIX, version 3.0135 extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir);136 } else { // DOS or other, fallback to DOS137 versionMadeBy = 0x0014; // DOS, version 2.0138 extFileAttr |= generateDosExternalFileAttr(file.dosPermissions, dir);139 }140 141 // date142 // @see http://www.delorie.com/djgpp/doc/rbinter/it/52/13.html143 // @see http://www.delorie.com/djgpp/doc/rbinter/it/65/16.html144 // @see http://www.delorie.com/djgpp/doc/rbinter/it/66/16.html145 146 dosTime = date.getUTCHours();147 dosTime = dosTime << 6;148 dosTime = dosTime | date.getUTCMinutes();149 dosTime = dosTime << 5;150 dosTime = dosTime | date.getUTCSeconds() / 2;151 152 dosDate = date.getUTCFullYear() - 1980;153 dosDate = dosDate << 4;154 dosDate = dosDate | (date.getUTCMonth() + 1);155 dosDate = dosDate << 5;156 dosDate = dosDate | date.getUTCDate();157 158 if (useUTF8ForFileName) {159 // set the unicode path extra field. unzip needs at least one extra160 // field to correctly handle unicode path, so using the path is as good161 // as any other information. This could improve the situation with162 // other archive managers too.163 // This field is usually used without the utf8 flag, with a non164 // unicode path in the header (winrar, winzip). This helps (a bit)165 // with the messy Windows' default compressed folders feature but166 // breaks on p7zip which doesn't seek the unicode path extra field.167 // So for now, UTF-8 everywhere !168 unicodePathExtraField =169 // Version170 decToHex(1, 1) +171 // NameCRC32172 decToHex(crc32(encodedFileName), 4) +173 // UnicodeName174 utfEncodedFileName;175 176 extraFields +=177 // Info-ZIP Unicode Path Extra Field178 "\x75\x70" +179 // size180 decToHex(unicodePathExtraField.length, 2) +181 // content182 unicodePathExtraField;183 }184 185 if(useUTF8ForComment) {186 187 unicodeCommentExtraField =188 // Version189 decToHex(1, 1) +190 // CommentCRC32191 decToHex(crc32(encodedComment), 4) +192 // UnicodeName193 utfEncodedComment;194 195 extraFields +=196 // Info-ZIP Unicode Path Extra Field197 "\x75\x63" +198 // size199 decToHex(unicodeCommentExtraField.length, 2) +200 // content201 unicodeCommentExtraField;202 }203 204 var header = "";205 206 // version needed to extract207 header += "\x0A\x00";208 // general purpose bit flag209 header += decToHex(bitflag, 2);210 // compression method211 header += compression.magic;212 // last mod file time213 header += decToHex(dosTime, 2);214 // last mod file date215 header += decToHex(dosDate, 2);216 // crc-32217 header += decToHex(dataInfo.crc32, 4);218 // compressed size219 header += decToHex(dataInfo.compressedSize, 4);220 // uncompressed size221 header += decToHex(dataInfo.uncompressedSize, 4);222 // file name length223 header += decToHex(encodedFileName.length, 2);224 // extra field length225 header += decToHex(extraFields.length, 2);226 227 228 var fileRecord = signature.LOCAL_FILE_HEADER + header + encodedFileName + extraFields;229 230 var dirRecord = signature.CENTRAL_FILE_HEADER +231 // version made by (00: DOS)232 decToHex(versionMadeBy, 2) +233 // file header (common to file and central directory)234 header +235 // file comment length236 decToHex(encodedComment.length, 2) +237 // disk number start238 "\x00\x00" +239 // internal file attributes TODO240 "\x00\x00" +241 // external file attributes242 decToHex(extFileAttr, 4) +243 // relative offset of local header244 decToHex(offset, 4) +245 // file name246 encodedFileName +247 // extra field248 extraFields +249 // file comment250 encodedComment;251 252 return {253 fileRecord: fileRecord,254 dirRecord: dirRecord255 };256};257 258/**259 * Generate the EOCD record.260 * @param {Number} entriesCount the number of entries in the zip file.261 * @param {Number} centralDirLength the length (in bytes) of the central dir.262 * @param {Number} localDirLength the length (in bytes) of the local dir.263 * @param {String} comment the zip file comment as a binary string.264 * @param {Function} encodeFileName the function to encode the comment.265 * @return {String} the EOCD record.266 */267var generateCentralDirectoryEnd = function (entriesCount, centralDirLength, localDirLength, comment, encodeFileName) {268 var dirEnd = "";269 var encodedComment = utils.transformTo("string", encodeFileName(comment));270 271 // end of central dir signature272 dirEnd = signature.CENTRAL_DIRECTORY_END +273 // number of this disk274 "\x00\x00" +275 // number of the disk with the start of the central directory276 "\x00\x00" +277 // total number of entries in the central directory on this disk278 decToHex(entriesCount, 2) +279 // total number of entries in the central directory280 decToHex(entriesCount, 2) +281 // size of the central directory 4 bytes282 decToHex(centralDirLength, 4) +283 // offset of start of central directory with respect to the starting disk number284 decToHex(localDirLength, 4) +285 // .ZIP file comment length286 decToHex(encodedComment.length, 2) +287 // .ZIP file comment288 encodedComment;289 290 return dirEnd;291};292 293/**294 * Generate data descriptors for a file entry.295 * @param {Object} streamInfo the hash generated by a worker, containing information296 * on the file entry.297 * @return {String} the data descriptors.298 */299var generateDataDescriptors = function (streamInfo) {300 var descriptor = "";301 descriptor = signature.DATA_DESCRIPTOR +302 // crc-32 4 bytes303 decToHex(streamInfo["crc32"], 4) +304 // compressed size 4 bytes305 decToHex(streamInfo["compressedSize"], 4) +306 // uncompressed size 4 bytes307 decToHex(streamInfo["uncompressedSize"], 4);308 309 return descriptor;310};311 312 313/**314 * A worker to concatenate other workers to create a zip file.315 * @param {Boolean} streamFiles `true` to stream the content of the files,316 * `false` to accumulate it.317 * @param {String} comment the comment to use.318 * @param {String} platform the platform to use, "UNIX" or "DOS".319 * @param {Function} encodeFileName the function to encode file names and comments.320 */321function ZipFileWorker(streamFiles, comment, platform, encodeFileName) {322 GenericWorker.call(this, "ZipFileWorker");323 // The number of bytes written so far. This doesn't count accumulated chunks.324 this.bytesWritten = 0;325 // The comment of the zip file326 this.zipComment = comment;327 // The platform "generating" the zip file.328 this.zipPlatform = platform;329 // the function to encode file names and comments.330 this.encodeFileName = encodeFileName;331 // Should we stream the content of the files ?332 this.streamFiles = streamFiles;333 // If `streamFiles` is false, we will need to accumulate the content of the334 // files to calculate sizes / crc32 (and write them *before* the content).335 // This boolean indicates if we are accumulating chunks (it will change a lot336 // during the lifetime of this worker).337 this.accumulate = false;338 // The buffer receiving chunks when accumulating content.339 this.contentBuffer = [];340 // The list of generated directory records.341 this.dirRecords = [];342 // The offset (in bytes) from the beginning of the zip file for the current source.343 this.currentSourceOffset = 0;344 // The total number of entries in this zip file.345 this.entriesCount = 0;346 // the name of the file currently being added, null when handling the end of the zip file.347 // Used for the emitted metadata.348 this.currentFile = null;349 350 351 352 this._sources = [];353}354utils.inherits(ZipFileWorker, GenericWorker);355 356/**357 * @see GenericWorker.push358 */359ZipFileWorker.prototype.push = function (chunk) {360 361 var currentFilePercent = chunk.meta.percent || 0;362 var entriesCount = this.entriesCount;363 var remainingFiles = this._sources.length;364 365 if(this.accumulate) {366 this.contentBuffer.push(chunk);367 } else {368 this.bytesWritten += chunk.data.length;369 370 GenericWorker.prototype.push.call(this, {371 data : chunk.data,372 meta : {373 currentFile : this.currentFile,374 percent : entriesCount ? (currentFilePercent + 100 * (entriesCount - remainingFiles - 1)) / entriesCount : 100375 }376 });377 }378};379 380/**381 * The worker started a new source (an other worker).382 * @param {Object} streamInfo the streamInfo object from the new source.383 */384ZipFileWorker.prototype.openedSource = function (streamInfo) {385 this.currentSourceOffset = this.bytesWritten;386 this.currentFile = streamInfo["file"].name;387 388 var streamedContent = this.streamFiles && !streamInfo["file"].dir;389 390 // don't stream folders (because they don't have any content)391 if(streamedContent) {392 var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);393 this.push({394 data : record.fileRecord,395 meta : {percent:0}396 });397 } else {398 // we need to wait for the whole file before pushing anything399 this.accumulate = true;400 }401};402 403/**404 * The worker finished a source (an other worker).405 * @param {Object} streamInfo the streamInfo object from the finished source.406 */407ZipFileWorker.prototype.closedSource = function (streamInfo) {408 this.accumulate = false;409 var streamedContent = this.streamFiles && !streamInfo["file"].dir;410 var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);411 412 this.dirRecords.push(record.dirRecord);413 if(streamedContent) {414 // after the streamed file, we put data descriptors415 this.push({416 data : generateDataDescriptors(streamInfo),417 meta : {percent:100}418 });419 } else {420 // the content wasn't streamed, we need to push everything now421 // first the file record, then the content422 this.push({423 data : record.fileRecord,424 meta : {percent:0}425 });426 while(this.contentBuffer.length) {427 this.push(this.contentBuffer.shift());428 }429 }430 this.currentFile = null;431};432 433/**434 * @see GenericWorker.flush435 */436ZipFileWorker.prototype.flush = function () {437 438 var localDirLength = this.bytesWritten;439 for(var i = 0; i < this.dirRecords.length; i++) {440 this.push({441 data : this.dirRecords[i],442 meta : {percent:100}443 });444 }445 var centralDirLength = this.bytesWritten - localDirLength;446 447 var dirEnd = generateCentralDirectoryEnd(this.dirRecords.length, centralDirLength, localDirLength, this.zipComment, this.encodeFileName);448 449 this.push({450 data : dirEnd,451 meta : {percent:100}452 });453};454 455/**456 * Prepare the next source to be read.457 */458ZipFileWorker.prototype.prepareNextSource = function () {459 this.previous = this._sources.shift();460 this.openedSource(this.previous.streamInfo);461 if (this.isPaused) {462 this.previous.pause();463 } else {464 this.previous.resume();465 }466};467 468/**469 * @see GenericWorker.registerPrevious470 */471ZipFileWorker.prototype.registerPrevious = function (previous) {472 this._sources.push(previous);473 var self = this;474 475 previous.on("data", function (chunk) {476 self.processChunk(chunk);477 });478 previous.on("end", function () {479 self.closedSource(self.previous.streamInfo);480 if(self._sources.length) {481 self.prepareNextSource();482 } else {483 self.end();484 }485 });486 previous.on("error", function (e) {487 self.error(e);488 });489 return this;490};491 492/**493 * @see GenericWorker.resume494 */495ZipFileWorker.prototype.resume = function () {496 if(!GenericWorker.prototype.resume.call(this)) {497 return false;498 }499 500 if (!this.previous && this._sources.length) {501 this.prepareNextSource();502 return true;503 }504 if (!this.previous && !this._sources.length && !this.generatedError) {505 this.end();506 return true;507 }508};509 510/**511 * @see GenericWorker.error512 */513ZipFileWorker.prototype.error = function (e) {514 var sources = this._sources;515 if(!GenericWorker.prototype.error.call(this, e)) {516 return false;517 }518 for(var i = 0; i < sources.length; i++) {519 try {520 sources[i].error(e);521 } catch(e) {522 // the `error` exploded, nothing to do523 }524 }525 return true;526};527 528/**529 * @see GenericWorker.lock530 */531ZipFileWorker.prototype.lock = function () {532 GenericWorker.prototype.lock.call(this);533 var sources = this._sources;534 for(var i = 0; i < sources.length; i++) {535 sources[i].lock();536 }537};538 539module.exports = ZipFileWorker;540 