You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							540 lines
						
					
					
						
							18 KiB
						
					
					
				
			
		
		
	
	
							540 lines
						
					
					
						
							18 KiB
						
					
					
				| "use strict";
 | |
| 
 | |
| var utils = require("../utils");
 | |
| var GenericWorker = require("../stream/GenericWorker");
 | |
| var utf8 = require("../utf8");
 | |
| var crc32 = require("../crc32");
 | |
| var signature = require("../signature");
 | |
| 
 | |
| /**
 | |
|  * Transform an integer into a string in hexadecimal.
 | |
|  * @private
 | |
|  * @param {number} dec the number to convert.
 | |
|  * @param {number} bytes the number of bytes to generate.
 | |
|  * @returns {string} the result.
 | |
|  */
 | |
| var decToHex = function(dec, bytes) {
 | |
|     var hex = "", i;
 | |
|     for (i = 0; i < bytes; i++) {
 | |
|         hex += String.fromCharCode(dec & 0xff);
 | |
|         dec = dec >>> 8;
 | |
|     }
 | |
|     return hex;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Generate the UNIX part of the external file attributes.
 | |
|  * @param {Object} unixPermissions the unix permissions or null.
 | |
|  * @param {Boolean} isDir true if the entry is a directory, false otherwise.
 | |
|  * @return {Number} a 32 bit integer.
 | |
|  *
 | |
|  * adapted from http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute :
 | |
|  *
 | |
|  * TTTTsstrwxrwxrwx0000000000ADVSHR
 | |
|  * ^^^^____________________________ file type, see zipinfo.c (UNX_*)
 | |
|  *     ^^^_________________________ setuid, setgid, sticky
 | |
|  *        ^^^^^^^^^________________ permissions
 | |
|  *                 ^^^^^^^^^^______ not used ?
 | |
|  *                           ^^^^^^ DOS attribute bits : Archive, Directory, Volume label, System file, Hidden, Read only
 | |
|  */
 | |
| var generateUnixExternalFileAttr = function (unixPermissions, isDir) {
 | |
| 
 | |
|     var result = unixPermissions;
 | |
|     if (!unixPermissions) {
 | |
|         // I can't use octal values in strict mode, hence the hexa.
 | |
|         //  040775 => 0x41fd
 | |
|         // 0100664 => 0x81b4
 | |
|         result = isDir ? 0x41fd : 0x81b4;
 | |
|     }
 | |
|     return (result & 0xFFFF) << 16;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Generate the DOS part of the external file attributes.
 | |
|  * @param {Object} dosPermissions the dos permissions or null.
 | |
|  * @param {Boolean} isDir true if the entry is a directory, false otherwise.
 | |
|  * @return {Number} a 32 bit integer.
 | |
|  *
 | |
|  * Bit 0     Read-Only
 | |
|  * Bit 1     Hidden
 | |
|  * Bit 2     System
 | |
|  * Bit 3     Volume Label
 | |
|  * Bit 4     Directory
 | |
|  * Bit 5     Archive
 | |
|  */
 | |
| var generateDosExternalFileAttr = function (dosPermissions) {
 | |
|     // the dir flag is already set for compatibility
 | |
|     return (dosPermissions || 0)  & 0x3F;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Generate the various parts used in the construction of the final zip file.
 | |
|  * @param {Object} streamInfo the hash with information about the compressed file.
 | |
|  * @param {Boolean} streamedContent is the content streamed ?
 | |
|  * @param {Boolean} streamingEnded is the stream finished ?
 | |
|  * @param {number} offset the current offset from the start of the zip file.
 | |
|  * @param {String} platform let's pretend we are this platform (change platform dependents fields)
 | |
|  * @param {Function} encodeFileName the function to encode the file name / comment.
 | |
|  * @return {Object} the zip parts.
 | |
|  */
 | |
| var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName) {
 | |
|     var file = streamInfo["file"],
 | |
|         compression = streamInfo["compression"],
 | |
|         useCustomEncoding = encodeFileName !== utf8.utf8encode,
 | |
|         encodedFileName = utils.transformTo("string", encodeFileName(file.name)),
 | |
|         utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)),
 | |
|         comment = file.comment,
 | |
|         encodedComment = utils.transformTo("string", encodeFileName(comment)),
 | |
|         utfEncodedComment = utils.transformTo("string", utf8.utf8encode(comment)),
 | |
|         useUTF8ForFileName = utfEncodedFileName.length !== file.name.length,
 | |
|         useUTF8ForComment = utfEncodedComment.length !== comment.length,
 | |
|         dosTime,
 | |
|         dosDate,
 | |
|         extraFields = "",
 | |
|         unicodePathExtraField = "",
 | |
|         unicodeCommentExtraField = "",
 | |
|         dir = file.dir,
 | |
|         date = file.date;
 | |
| 
 | |
| 
 | |
|     var dataInfo = {
 | |
|         crc32 : 0,
 | |
|         compressedSize : 0,
 | |
|         uncompressedSize : 0
 | |
|     };
 | |
| 
 | |
|     // if the content is streamed, the sizes/crc32 are only available AFTER
 | |
|     // the end of the stream.
 | |
|     if (!streamedContent || streamingEnded) {
 | |
|         dataInfo.crc32 = streamInfo["crc32"];
 | |
|         dataInfo.compressedSize = streamInfo["compressedSize"];
 | |
|         dataInfo.uncompressedSize = streamInfo["uncompressedSize"];
 | |
|     }
 | |
| 
 | |
|     var bitflag = 0;
 | |
|     if (streamedContent) {
 | |
|         // Bit 3: the sizes/crc32 are set to zero in the local header.
 | |
|         // The correct values are put in the data descriptor immediately
 | |
|         // following the compressed data.
 | |
|         bitflag |= 0x0008;
 | |
|     }
 | |
|     if (!useCustomEncoding && (useUTF8ForFileName || useUTF8ForComment)) {
 | |
|         // Bit 11: Language encoding flag (EFS).
 | |
|         bitflag |= 0x0800;
 | |
|     }
 | |
| 
 | |
| 
 | |
|     var extFileAttr = 0;
 | |
|     var versionMadeBy = 0;
 | |
|     if (dir) {
 | |
|         // dos or unix, we set the dos dir flag
 | |
|         extFileAttr |= 0x00010;
 | |
|     }
 | |
|     if(platform === "UNIX") {
 | |
|         versionMadeBy = 0x031E; // UNIX, version 3.0
 | |
|         extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir);
 | |
|     } else { // DOS or other, fallback to DOS
 | |
|         versionMadeBy = 0x0014; // DOS, version 2.0
 | |
|         extFileAttr |= generateDosExternalFileAttr(file.dosPermissions, dir);
 | |
|     }
 | |
| 
 | |
|     // date
 | |
|     // @see http://www.delorie.com/djgpp/doc/rbinter/it/52/13.html
 | |
|     // @see http://www.delorie.com/djgpp/doc/rbinter/it/65/16.html
 | |
|     // @see http://www.delorie.com/djgpp/doc/rbinter/it/66/16.html
 | |
| 
 | |
|     dosTime = date.getUTCHours();
 | |
|     dosTime = dosTime << 6;
 | |
|     dosTime = dosTime | date.getUTCMinutes();
 | |
|     dosTime = dosTime << 5;
 | |
|     dosTime = dosTime | date.getUTCSeconds() / 2;
 | |
| 
 | |
|     dosDate = date.getUTCFullYear() - 1980;
 | |
|     dosDate = dosDate << 4;
 | |
|     dosDate = dosDate | (date.getUTCMonth() + 1);
 | |
|     dosDate = dosDate << 5;
 | |
|     dosDate = dosDate | date.getUTCDate();
 | |
| 
 | |
|     if (useUTF8ForFileName) {
 | |
|         // set the unicode path extra field. unzip needs at least one extra
 | |
|         // field to correctly handle unicode path, so using the path is as good
 | |
|         // as any other information. This could improve the situation with
 | |
|         // other archive managers too.
 | |
|         // This field is usually used without the utf8 flag, with a non
 | |
|         // unicode path in the header (winrar, winzip). This helps (a bit)
 | |
|         // with the messy Windows' default compressed folders feature but
 | |
|         // breaks on p7zip which doesn't seek the unicode path extra field.
 | |
|         // So for now, UTF-8 everywhere !
 | |
|         unicodePathExtraField =
 | |
|             // Version
 | |
|             decToHex(1, 1) +
 | |
|             // NameCRC32
 | |
|             decToHex(crc32(encodedFileName), 4) +
 | |
|             // UnicodeName
 | |
|             utfEncodedFileName;
 | |
| 
 | |
|         extraFields +=
 | |
|             // Info-ZIP Unicode Path Extra Field
 | |
|             "\x75\x70" +
 | |
|             // size
 | |
|             decToHex(unicodePathExtraField.length, 2) +
 | |
|             // content
 | |
|             unicodePathExtraField;
 | |
|     }
 | |
| 
 | |
|     if(useUTF8ForComment) {
 | |
| 
 | |
|         unicodeCommentExtraField =
 | |
|             // Version
 | |
|             decToHex(1, 1) +
 | |
|             // CommentCRC32
 | |
|             decToHex(crc32(encodedComment), 4) +
 | |
|             // UnicodeName
 | |
|             utfEncodedComment;
 | |
| 
 | |
|         extraFields +=
 | |
|             // Info-ZIP Unicode Path Extra Field
 | |
|             "\x75\x63" +
 | |
|             // size
 | |
|             decToHex(unicodeCommentExtraField.length, 2) +
 | |
|             // content
 | |
|             unicodeCommentExtraField;
 | |
|     }
 | |
| 
 | |
|     var header = "";
 | |
| 
 | |
|     // version needed to extract
 | |
|     header += "\x0A\x00";
 | |
|     // general purpose bit flag
 | |
|     header += decToHex(bitflag, 2);
 | |
|     // compression method
 | |
|     header += compression.magic;
 | |
|     // last mod file time
 | |
|     header += decToHex(dosTime, 2);
 | |
|     // last mod file date
 | |
|     header += decToHex(dosDate, 2);
 | |
|     // crc-32
 | |
|     header += decToHex(dataInfo.crc32, 4);
 | |
|     // compressed size
 | |
|     header += decToHex(dataInfo.compressedSize, 4);
 | |
|     // uncompressed size
 | |
|     header += decToHex(dataInfo.uncompressedSize, 4);
 | |
|     // file name length
 | |
|     header += decToHex(encodedFileName.length, 2);
 | |
|     // extra field length
 | |
|     header += decToHex(extraFields.length, 2);
 | |
| 
 | |
| 
 | |
|     var fileRecord = signature.LOCAL_FILE_HEADER + header + encodedFileName + extraFields;
 | |
| 
 | |
|     var dirRecord = signature.CENTRAL_FILE_HEADER +
 | |
|         // version made by (00: DOS)
 | |
|         decToHex(versionMadeBy, 2) +
 | |
|         // file header (common to file and central directory)
 | |
|         header +
 | |
|         // file comment length
 | |
|         decToHex(encodedComment.length, 2) +
 | |
|         // disk number start
 | |
|         "\x00\x00" +
 | |
|         // internal file attributes TODO
 | |
|         "\x00\x00" +
 | |
|         // external file attributes
 | |
|         decToHex(extFileAttr, 4) +
 | |
|         // relative offset of local header
 | |
|         decToHex(offset, 4) +
 | |
|         // file name
 | |
|         encodedFileName +
 | |
|         // extra field
 | |
|         extraFields +
 | |
|         // file comment
 | |
|         encodedComment;
 | |
| 
 | |
|     return {
 | |
|         fileRecord: fileRecord,
 | |
|         dirRecord: dirRecord
 | |
|     };
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Generate the EOCD record.
 | |
|  * @param {Number} entriesCount the number of entries in the zip file.
 | |
|  * @param {Number} centralDirLength the length (in bytes) of the central dir.
 | |
|  * @param {Number} localDirLength the length (in bytes) of the local dir.
 | |
|  * @param {String} comment the zip file comment as a binary string.
 | |
|  * @param {Function} encodeFileName the function to encode the comment.
 | |
|  * @return {String} the EOCD record.
 | |
|  */
 | |
| var generateCentralDirectoryEnd = function (entriesCount, centralDirLength, localDirLength, comment, encodeFileName) {
 | |
|     var dirEnd = "";
 | |
|     var encodedComment = utils.transformTo("string", encodeFileName(comment));
 | |
| 
 | |
|     // end of central dir signature
 | |
|     dirEnd = signature.CENTRAL_DIRECTORY_END +
 | |
|         // number of this disk
 | |
|         "\x00\x00" +
 | |
|         // number of the disk with the start of the central directory
 | |
|         "\x00\x00" +
 | |
|         // total number of entries in the central directory on this disk
 | |
|         decToHex(entriesCount, 2) +
 | |
|         // total number of entries in the central directory
 | |
|         decToHex(entriesCount, 2) +
 | |
|         // size of the central directory   4 bytes
 | |
|         decToHex(centralDirLength, 4) +
 | |
|         // offset of start of central directory with respect to the starting disk number
 | |
|         decToHex(localDirLength, 4) +
 | |
|         // .ZIP file comment length
 | |
|         decToHex(encodedComment.length, 2) +
 | |
|         // .ZIP file comment
 | |
|         encodedComment;
 | |
| 
 | |
|     return dirEnd;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Generate data descriptors for a file entry.
 | |
|  * @param {Object} streamInfo the hash generated by a worker, containing information
 | |
|  * on the file entry.
 | |
|  * @return {String} the data descriptors.
 | |
|  */
 | |
| var generateDataDescriptors = function (streamInfo) {
 | |
|     var descriptor = "";
 | |
|     descriptor = signature.DATA_DESCRIPTOR +
 | |
|         // crc-32                          4 bytes
 | |
|         decToHex(streamInfo["crc32"], 4) +
 | |
|         // compressed size                 4 bytes
 | |
|         decToHex(streamInfo["compressedSize"], 4) +
 | |
|         // uncompressed size               4 bytes
 | |
|         decToHex(streamInfo["uncompressedSize"], 4);
 | |
| 
 | |
|     return descriptor;
 | |
| };
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * A worker to concatenate other workers to create a zip file.
 | |
|  * @param {Boolean} streamFiles `true` to stream the content of the files,
 | |
|  * `false` to accumulate it.
 | |
|  * @param {String} comment the comment to use.
 | |
|  * @param {String} platform the platform to use, "UNIX" or "DOS".
 | |
|  * @param {Function} encodeFileName the function to encode file names and comments.
 | |
|  */
 | |
| function ZipFileWorker(streamFiles, comment, platform, encodeFileName) {
 | |
|     GenericWorker.call(this, "ZipFileWorker");
 | |
|     // The number of bytes written so far. This doesn't count accumulated chunks.
 | |
|     this.bytesWritten = 0;
 | |
|     // The comment of the zip file
 | |
|     this.zipComment = comment;
 | |
|     // The platform "generating" the zip file.
 | |
|     this.zipPlatform = platform;
 | |
|     // the function to encode file names and comments.
 | |
|     this.encodeFileName = encodeFileName;
 | |
|     // Should we stream the content of the files ?
 | |
|     this.streamFiles = streamFiles;
 | |
|     // If `streamFiles` is false, we will need to accumulate the content of the
 | |
|     // files to calculate sizes / crc32 (and write them *before* the content).
 | |
|     // This boolean indicates if we are accumulating chunks (it will change a lot
 | |
|     // during the lifetime of this worker).
 | |
|     this.accumulate = false;
 | |
|     // The buffer receiving chunks when accumulating content.
 | |
|     this.contentBuffer = [];
 | |
|     // The list of generated directory records.
 | |
|     this.dirRecords = [];
 | |
|     // The offset (in bytes) from the beginning of the zip file for the current source.
 | |
|     this.currentSourceOffset = 0;
 | |
|     // The total number of entries in this zip file.
 | |
|     this.entriesCount = 0;
 | |
|     // the name of the file currently being added, null when handling the end of the zip file.
 | |
|     // Used for the emitted metadata.
 | |
|     this.currentFile = null;
 | |
| 
 | |
| 
 | |
| 
 | |
|     this._sources = [];
 | |
| }
 | |
| utils.inherits(ZipFileWorker, GenericWorker);
 | |
| 
 | |
| /**
 | |
|  * @see GenericWorker.push
 | |
|  */
 | |
| ZipFileWorker.prototype.push = function (chunk) {
 | |
| 
 | |
|     var currentFilePercent = chunk.meta.percent || 0;
 | |
|     var entriesCount = this.entriesCount;
 | |
|     var remainingFiles = this._sources.length;
 | |
| 
 | |
|     if(this.accumulate) {
 | |
|         this.contentBuffer.push(chunk);
 | |
|     } else {
 | |
|         this.bytesWritten += chunk.data.length;
 | |
| 
 | |
|         GenericWorker.prototype.push.call(this, {
 | |
|             data : chunk.data,
 | |
|             meta : {
 | |
|                 currentFile : this.currentFile,
 | |
|                 percent : entriesCount ? (currentFilePercent + 100 * (entriesCount - remainingFiles - 1)) / entriesCount : 100
 | |
|             }
 | |
|         });
 | |
|     }
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * The worker started a new source (an other worker).
 | |
|  * @param {Object} streamInfo the streamInfo object from the new source.
 | |
|  */
 | |
| ZipFileWorker.prototype.openedSource = function (streamInfo) {
 | |
|     this.currentSourceOffset = this.bytesWritten;
 | |
|     this.currentFile = streamInfo["file"].name;
 | |
| 
 | |
|     var streamedContent = this.streamFiles && !streamInfo["file"].dir;
 | |
| 
 | |
|     // don't stream folders (because they don't have any content)
 | |
|     if(streamedContent) {
 | |
|         var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);
 | |
|         this.push({
 | |
|             data : record.fileRecord,
 | |
|             meta : {percent:0}
 | |
|         });
 | |
|     } else {
 | |
|         // we need to wait for the whole file before pushing anything
 | |
|         this.accumulate = true;
 | |
|     }
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * The worker finished a source (an other worker).
 | |
|  * @param {Object} streamInfo the streamInfo object from the finished source.
 | |
|  */
 | |
| ZipFileWorker.prototype.closedSource = function (streamInfo) {
 | |
|     this.accumulate = false;
 | |
|     var streamedContent = this.streamFiles && !streamInfo["file"].dir;
 | |
|     var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);
 | |
| 
 | |
|     this.dirRecords.push(record.dirRecord);
 | |
|     if(streamedContent) {
 | |
|         // after the streamed file, we put data descriptors
 | |
|         this.push({
 | |
|             data : generateDataDescriptors(streamInfo),
 | |
|             meta : {percent:100}
 | |
|         });
 | |
|     } else {
 | |
|         // the content wasn't streamed, we need to push everything now
 | |
|         // first the file record, then the content
 | |
|         this.push({
 | |
|             data : record.fileRecord,
 | |
|             meta : {percent:0}
 | |
|         });
 | |
|         while(this.contentBuffer.length) {
 | |
|             this.push(this.contentBuffer.shift());
 | |
|         }
 | |
|     }
 | |
|     this.currentFile = null;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @see GenericWorker.flush
 | |
|  */
 | |
| ZipFileWorker.prototype.flush = function () {
 | |
| 
 | |
|     var localDirLength = this.bytesWritten;
 | |
|     for(var i = 0; i < this.dirRecords.length; i++) {
 | |
|         this.push({
 | |
|             data : this.dirRecords[i],
 | |
|             meta : {percent:100}
 | |
|         });
 | |
|     }
 | |
|     var centralDirLength = this.bytesWritten - localDirLength;
 | |
| 
 | |
|     var dirEnd = generateCentralDirectoryEnd(this.dirRecords.length, centralDirLength, localDirLength, this.zipComment, this.encodeFileName);
 | |
| 
 | |
|     this.push({
 | |
|         data : dirEnd,
 | |
|         meta : {percent:100}
 | |
|     });
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Prepare the next source to be read.
 | |
|  */
 | |
| ZipFileWorker.prototype.prepareNextSource = function () {
 | |
|     this.previous = this._sources.shift();
 | |
|     this.openedSource(this.previous.streamInfo);
 | |
|     if (this.isPaused) {
 | |
|         this.previous.pause();
 | |
|     } else {
 | |
|         this.previous.resume();
 | |
|     }
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @see GenericWorker.registerPrevious
 | |
|  */
 | |
| ZipFileWorker.prototype.registerPrevious = function (previous) {
 | |
|     this._sources.push(previous);
 | |
|     var self = this;
 | |
| 
 | |
|     previous.on("data", function (chunk) {
 | |
|         self.processChunk(chunk);
 | |
|     });
 | |
|     previous.on("end", function () {
 | |
|         self.closedSource(self.previous.streamInfo);
 | |
|         if(self._sources.length) {
 | |
|             self.prepareNextSource();
 | |
|         } else {
 | |
|             self.end();
 | |
|         }
 | |
|     });
 | |
|     previous.on("error", function (e) {
 | |
|         self.error(e);
 | |
|     });
 | |
|     return this;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @see GenericWorker.resume
 | |
|  */
 | |
| ZipFileWorker.prototype.resume = function () {
 | |
|     if(!GenericWorker.prototype.resume.call(this)) {
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     if (!this.previous && this._sources.length) {
 | |
|         this.prepareNextSource();
 | |
|         return true;
 | |
|     }
 | |
|     if (!this.previous && !this._sources.length && !this.generatedError) {
 | |
|         this.end();
 | |
|         return true;
 | |
|     }
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @see GenericWorker.error
 | |
|  */
 | |
| ZipFileWorker.prototype.error = function (e) {
 | |
|     var sources = this._sources;
 | |
|     if(!GenericWorker.prototype.error.call(this, e)) {
 | |
|         return false;
 | |
|     }
 | |
|     for(var i = 0; i < sources.length; i++) {
 | |
|         try {
 | |
|             sources[i].error(e);
 | |
|         } catch(e) {
 | |
|             // the `error` exploded, nothing to do
 | |
|         }
 | |
|     }
 | |
|     return true;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @see GenericWorker.lock
 | |
|  */
 | |
| ZipFileWorker.prototype.lock = function () {
 | |
|     GenericWorker.prototype.lock.call(this);
 | |
|     var sources = this._sources;
 | |
|     for(var i = 0; i < sources.length; i++) {
 | |
|         sources[i].lock();
 | |
|     }
 | |
| };
 | |
| 
 | |
| module.exports = ZipFileWorker;
 |