2 var support = require('./support');
3 var utils = require('./utils');
4 var crc32 = require('./crc32');
5 var signature = require('./signature');
6 var defaults = require('./defaults');
7 var base64 = require('./base64');
8 var compressions = require('./compressions');
9 var CompressedObject = require('./compressedObject');
10 var nodeBuffer = require('./nodeBuffer');
11 var utf8 = require('./utf8');
12 var StringWriter = require('./stringWriter');
13 var Uint8ArrayWriter = require('./uint8ArrayWriter');
16 * Returns the raw data of a ZipObject, decompress the content if necessary.
17 * @param {ZipObject} file the file to use.
18 * @return {String|ArrayBuffer|Uint8Array|Buffer} the data.
20 var getRawData = function(file) {
21 if (file._data instanceof CompressedObject) {
22 file._data = file._data.getContent();
23 file.options.binary = true;
24 file.options.base64 = false;
26 if (utils.getTypeOf(file._data) === "uint8array") {
27 var copy = file._data;
28 // when reading an arraybuffer, the CompressedObject mechanism will keep it and subarray() a Uint8Array.
29 // if we request a file in the same format, we might get the same Uint8Array or its ArrayBuffer (the original zip file).
30 file._data = new Uint8Array(copy.length);
31 // with an empty Uint8Array, Opera fails with a "Offset larger than array size"
32 if (copy.length !== 0) {
33 file._data.set(copy, 0);
41 * Returns the data of a ZipObject in a binary form. If the content is an unicode string, encode it.
42 * @param {ZipObject} file the file to use.
43 * @return {String|ArrayBuffer|Uint8Array|Buffer} the data.
45 var getBinaryData = function(file) {
46 var result = getRawData(file),
47 type = utils.getTypeOf(result);
48 if (type === "string") {
49 if (!file.options.binary) {
51 // unicode string => binary string is a painful process, check if we can avoid it.
52 if (support.nodebuffer) {
53 return nodeBuffer(result, "utf-8");
56 return file.asBinary();
62 * Transform this._data into a string.
63 * @param {function} filter a function String -> String, applied if not null on the result.
64 * @return {String} the string representing this._data.
66 var dataToString = function(asUTF8) {
67 var result = getRawData(this);
68 if (result === null || typeof result === "undefined") {
71 // if the data is a base64 string, we decode it before checking the encoding !
72 if (this.options.base64) {
73 result = base64.decode(result);
75 if (asUTF8 && this.options.binary) {
76 // JSZip.prototype.utf8decode supports arrays as input
77 // skip to array => string step, utf8decode will do it.
78 result = out.utf8decode(result);
81 // no utf8 transformation, do the array => string step.
82 result = utils.transformTo("string", result);
85 if (!asUTF8 && !this.options.binary) {
86 result = utils.transformTo("string", out.utf8encode(result));
91 * A simple object representing a file in the zip file.
93 * @param {string} name the name of the file
94 * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data
95 * @param {Object} options the options of the file
97 var ZipObject = function(name, data, options) {
99 this.dir = options.dir;
100 this.date = options.date;
101 this.comment = options.comment;
102 this.unixPermissions = options.unixPermissions;
103 this.dosPermissions = options.dosPermissions;
106 this.options = options;
109 * This object contains initial values for dir and date.
110 * With them, we can check if the user changed the deprecated metadata in
111 * `ZipObject#options` or not.
113 this._initialMetadata = {
119 ZipObject.prototype = {
121 * Return the content as UTF8 string.
122 * @return {string} the UTF8 string.
125 return dataToString.call(this, true);
128 * Returns the binary content.
129 * @return {string} the content as binary.
131 asBinary: function() {
132 return dataToString.call(this, false);
135 * Returns the content as a nodejs Buffer.
136 * @return {Buffer} the content as a Buffer.
138 asNodeBuffer: function() {
139 var result = getBinaryData(this);
140 return utils.transformTo("nodebuffer", result);
143 * Returns the content as an Uint8Array.
144 * @return {Uint8Array} the content as an Uint8Array.
146 asUint8Array: function() {
147 var result = getBinaryData(this);
148 return utils.transformTo("uint8array", result);
151 * Returns the content as an ArrayBuffer.
152 * @return {ArrayBuffer} the content as an ArrayBufer.
154 asArrayBuffer: function() {
155 return this.asUint8Array().buffer;
160 * Transform an integer into a string in hexadecimal.
162 * @param {number} dec the number to convert.
163 * @param {number} bytes the number of bytes to generate.
164 * @returns {string} the result.
166 var decToHex = function(dec, bytes) {
169 for (i = 0; i < bytes; i++) {
170 hex += String.fromCharCode(dec & 0xff);
177 * Merge the objects passed as parameters into a new one.
179 * @param {...Object} var_args All objects to merge.
180 * @return {Object} a new object with the data of the others.
182 var extend = function() {
183 var result = {}, i, attr;
184 for (i = 0; i < arguments.length; i++) { // arguments is not enumerable in some browsers
185 for (attr in arguments[i]) {
186 if (arguments[i].hasOwnProperty(attr) && typeof result[attr] === "undefined") {
187 result[attr] = arguments[i][attr];
195 * Transforms the (incomplete) options from the user into the complete
196 * set of options to create a file.
198 * @param {Object} o the options from the user.
199 * @return {Object} the complete set of options.
201 var prepareFileAttrs = function(o) {
203 if (o.base64 === true && (o.binary === null || o.binary === undefined)) {
206 o = extend(o, defaults);
207 o.date = o.date || new Date();
208 if (o.compression !== null) o.compression = o.compression.toUpperCase();
214 * Add a file in the current folder.
216 * @param {string} name the name of the file
217 * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data of the file
218 * @param {Object} o the options of the file
219 * @return {Object} the new file.
221 var fileAdd = function(name, data, o) {
222 // be sure sub folders exist
223 var dataType = utils.getTypeOf(data),
226 o = prepareFileAttrs(o);
228 if (typeof o.unixPermissions === "string") {
229 o.unixPermissions = parseInt(o.unixPermissions, 8);
232 // UNX_IFDIR 0040000 see zipinfo.c
233 if (o.unixPermissions && (o.unixPermissions & 0x4000)) {
237 if (o.dosPermissions && (o.dosPermissions & 0x0010)) {
242 name = forceTrailingSlash(name);
245 if (o.createFolders && (parent = parentFolder(name))) {
246 folderAdd.call(this, parent, true);
249 if (o.dir || data === null || typeof data === "undefined") {
255 else if (dataType === "string") {
256 if (o.binary && !o.base64) {
257 // optimizedBinaryString == true means that the file has already been filtered with a 0xFF mask
258 if (o.optimizedBinaryString !== true) {
259 // this is a string, not in a base64 format.
260 // Be sure that this is a correct "binary string"
261 data = utils.string2binary(data);
265 else { // arraybuffer, uint8array, ...
269 if (!dataType && !(data instanceof CompressedObject)) {
270 throw new Error("The data of '" + name + "' is in an unsupported format !");
273 // special case : it's way easier to work with Uint8Array than with ArrayBuffer
274 if (dataType === "arraybuffer") {
275 data = utils.transformTo("uint8array", data);
279 var object = new ZipObject(name, data, o);
280 this.files[name] = object;
285 * Find the parent folder of the path.
287 * @param {string} path the path to use
288 * @return {string} the parent folder, or ""
290 var parentFolder = function (path) {
291 if (path.slice(-1) == '/') {
292 path = path.substring(0, path.length - 1);
294 var lastSlash = path.lastIndexOf('/');
295 return (lastSlash > 0) ? path.substring(0, lastSlash) : "";
300 * Returns the path with a slash at the end.
302 * @param {String} path the path to check.
303 * @return {String} the path with a trailing slash.
305 var forceTrailingSlash = function(path) {
306 // Check the name ends with a /
307 if (path.slice(-1) != "/") {
308 path += "/"; // IE doesn't like substr(-1)
313 * Add a (sub) folder in the current folder.
315 * @param {string} name the folder's name
316 * @param {boolean=} [createFolders] If true, automatically create sub
317 * folders. Defaults to false.
318 * @return {Object} the new folder.
320 var folderAdd = function(name, createFolders) {
321 createFolders = (typeof createFolders !== 'undefined') ? createFolders : false;
323 name = forceTrailingSlash(name);
325 // Does this folder already exist?
326 if (!this.files[name]) {
327 fileAdd.call(this, name, null, {
329 createFolders: createFolders
332 return this.files[name];
336 * Generate a JSZip.CompressedObject for a given zipOject.
337 * @param {ZipObject} file the object to read.
338 * @param {JSZip.compression} compression the compression to use.
339 * @param {Object} compressionOptions the options to use when compressing.
340 * @return {JSZip.CompressedObject} the compressed result.
342 var generateCompressedObjectFrom = function(file, compression, compressionOptions) {
343 var result = new CompressedObject(),
346 // the data has not been decompressed, we might reuse things !
347 if (file._data instanceof CompressedObject) {
348 result.uncompressedSize = file._data.uncompressedSize;
349 result.crc32 = file._data.crc32;
351 if (result.uncompressedSize === 0 || file.dir) {
352 compression = compressions['STORE'];
353 result.compressedContent = "";
356 else if (file._data.compressionMethod === compression.magic) {
357 result.compressedContent = file._data.getCompressedContent();
360 content = file._data.getContent();
361 // need to decompress / recompress
362 result.compressedContent = compression.compress(utils.transformTo(compression.compressInputType, content), compressionOptions);
366 // have uncompressed data
367 content = getBinaryData(file);
368 if (!content || content.length === 0 || file.dir) {
369 compression = compressions['STORE'];
372 result.uncompressedSize = content.length;
373 result.crc32 = crc32(content);
374 result.compressedContent = compression.compress(utils.transformTo(compression.compressInputType, content), compressionOptions);
377 result.compressedSize = result.compressedContent.length;
378 result.compressionMethod = compression.magic;
387 * Generate the UNIX part of the external file attributes.
388 * @param {Object} unixPermissions the unix permissions or null.
389 * @param {Boolean} isDir true if the entry is a directory, false otherwise.
390 * @return {Number} a 32 bit integer.
392 * adapted from http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute :
394 * TTTTsstrwxrwxrwx0000000000ADVSHR
395 * ^^^^____________________________ file type, see zipinfo.c (UNX_*)
396 * ^^^_________________________ setuid, setgid, sticky
397 * ^^^^^^^^^________________ permissions
398 * ^^^^^^^^^^______ not used ?
399 * ^^^^^^ DOS attribute bits : Archive, Directory, Volume label, System file, Hidden, Read only
401 var generateUnixExternalFileAttr = function (unixPermissions, isDir) {
403 var result = unixPermissions;
404 if (!unixPermissions) {
405 // I can't use octal values in strict mode, hence the hexa.
408 result = isDir ? 0x41fd : 0x81b4;
411 return (result & 0xFFFF) << 16;
415 * Generate the DOS part of the external file attributes.
416 * @param {Object} dosPermissions the dos permissions or null.
417 * @param {Boolean} isDir true if the entry is a directory, false otherwise.
418 * @return {Number} a 32 bit integer.
427 var generateDosExternalFileAttr = function (dosPermissions, isDir) {
429 // the dir flag is already set for compatibility
431 return (dosPermissions || 0) & 0x3F;
435 * Generate the various parts used in the construction of the final zip file.
436 * @param {string} name the file name.
437 * @param {ZipObject} file the file content.
438 * @param {JSZip.CompressedObject} compressedObject the compressed object.
439 * @param {number} offset the current offset from the start of the zip file.
440 * @param {String} platform let's pretend we are this platform (change platform dependents fields)
441 * @return {object} the zip parts.
443 var generateZipParts = function(name, file, compressedObject, offset, platform) {
444 var data = compressedObject.compressedContent,
445 utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)),
446 comment = file.comment || "",
447 utfEncodedComment = utils.transformTo("string", utf8.utf8encode(comment)),
448 useUTF8ForFileName = utfEncodedFileName.length !== file.name.length,
449 useUTF8ForComment = utfEncodedComment.length !== comment.length,
454 unicodePathExtraField = "",
455 unicodeCommentExtraField = "",
459 // handle the deprecated options.dir
460 if (file._initialMetadata.dir !== file.dir) {
466 // handle the deprecated options.date
467 if(file._initialMetadata.date !== file.date) {
474 var versionMadeBy = 0;
476 // dos or unix, we set the dos dir flag
477 extFileAttr |= 0x00010;
479 if(platform === "UNIX") {
480 versionMadeBy = 0x031E; // UNIX, version 3.0
481 extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir);
482 } else { // DOS or other, fallback to DOS
483 versionMadeBy = 0x0014; // DOS, version 2.0
484 extFileAttr |= generateDosExternalFileAttr(file.dosPermissions, dir);
488 // @see http://www.delorie.com/djgpp/doc/rbinter/it/52/13.html
489 // @see http://www.delorie.com/djgpp/doc/rbinter/it/65/16.html
490 // @see http://www.delorie.com/djgpp/doc/rbinter/it/66/16.html
492 dosTime = date.getHours();
493 dosTime = dosTime << 6;
494 dosTime = dosTime | date.getMinutes();
495 dosTime = dosTime << 5;
496 dosTime = dosTime | date.getSeconds() / 2;
498 dosDate = date.getFullYear() - 1980;
499 dosDate = dosDate << 4;
500 dosDate = dosDate | (date.getMonth() + 1);
501 dosDate = dosDate << 5;
502 dosDate = dosDate | date.getDate();
504 if (useUTF8ForFileName) {
505 // set the unicode path extra field. unzip needs at least one extra
506 // field to correctly handle unicode path, so using the path is as good
507 // as any other information. This could improve the situation with
508 // other archive managers too.
509 // This field is usually used without the utf8 flag, with a non
510 // unicode path in the header (winrar, winzip). This helps (a bit)
511 // with the messy Windows' default compressed folders feature but
512 // breaks on p7zip which doesn't seek the unicode path extra field.
513 // So for now, UTF-8 everywhere !
514 unicodePathExtraField =
518 decToHex(crc32(utfEncodedFileName), 4) +
523 // Info-ZIP Unicode Path Extra Field
526 decToHex(unicodePathExtraField.length, 2) +
528 unicodePathExtraField;
531 if(useUTF8ForComment) {
533 unicodeCommentExtraField =
537 decToHex(this.crc32(utfEncodedComment), 4) +
542 // Info-ZIP Unicode Path Extra Field
545 decToHex(unicodeCommentExtraField.length, 2) +
547 unicodeCommentExtraField;
552 // version needed to extract
553 header += "\x0A\x00";
554 // general purpose bit flag
555 // set bit 11 if utf8
556 header += (useUTF8ForFileName || useUTF8ForComment) ? "\x00\x08" : "\x00\x00";
557 // compression method
558 header += compressedObject.compressionMethod;
559 // last mod file time
560 header += decToHex(dosTime, 2);
561 // last mod file date
562 header += decToHex(dosDate, 2);
564 header += decToHex(compressedObject.crc32, 4);
566 header += decToHex(compressedObject.compressedSize, 4);
568 header += decToHex(compressedObject.uncompressedSize, 4);
570 header += decToHex(utfEncodedFileName.length, 2);
571 // extra field length
572 header += decToHex(extraFields.length, 2);
575 var fileRecord = signature.LOCAL_FILE_HEADER + header + utfEncodedFileName + extraFields;
577 var dirRecord = signature.CENTRAL_FILE_HEADER +
578 // version made by (00: DOS)
579 decToHex(versionMadeBy, 2) +
580 // file header (common to file and central directory)
582 // file comment length
583 decToHex(utfEncodedComment.length, 2) +
586 // internal file attributes TODO
588 // external file attributes
589 decToHex(extFileAttr, 4) +
590 // relative offset of local header
591 decToHex(offset, 4) +
600 fileRecord: fileRecord,
601 dirRecord: dirRecord,
602 compressedObject: compressedObject
607 // return the actual prototype of JSZip
610 * Read an existing zip and merge the data in the current JSZip object.
611 * The implementation is in jszip-load.js, don't forget to include it.
612 * @param {String|ArrayBuffer|Uint8Array|Buffer} stream The stream to load
613 * @param {Object} options Options for loading the stream.
614 * options.base64 : is the stream in base64 ? default : false
615 * @return {JSZip} the current JSZip object
617 load: function(stream, options) {
618 throw new Error("Load method is not defined. Is the file jszip-load.js included ?");
622 * Filter nested files/folders with the specified function.
623 * @param {Function} search the predicate to use :
624 * function (relativePath, file) {...}
625 * It takes 2 arguments : the relative path and the file.
626 * @return {Array} An array of matching elements.
628 filter: function(search) {
630 filename, relativePath, file, fileClone;
631 for (filename in this.files) {
632 if (!this.files.hasOwnProperty(filename)) {
635 file = this.files[filename];
636 // return a new object, don't let the user mess with our internal objects :)
637 fileClone = new ZipObject(file.name, file._data, extend(file.options));
638 relativePath = filename.slice(this.root.length, filename.length);
639 if (filename.slice(0, this.root.length) === this.root && // the file is in the current root
640 search(relativePath, fileClone)) { // and the file matches the function
641 result.push(fileClone);
648 * Add a file to the zip file, or search a file.
649 * @param {string|RegExp} name The name of the file to add (if data is defined),
650 * the name of the file to find (if no data) or a regex to match files.
651 * @param {String|ArrayBuffer|Uint8Array|Buffer} data The file data, either raw or base64 encoded
652 * @param {Object} o File options
653 * @return {JSZip|Object|Array} this JSZip object (when adding a file),
654 * a file (when searching by string) or an array of files (when searching by regex).
656 file: function(name, data, o) {
657 if (arguments.length === 1) {
658 if (utils.isRegExp(name)) {
660 return this.filter(function(relativePath, file) {
661 return !file.dir && regexp.test(relativePath);
665 return this.filter(function(relativePath, file) {
666 return !file.dir && relativePath === name;
670 else { // more than one argument : we have data !
671 name = this.root + name;
672 fileAdd.call(this, name, data, o);
678 * Add a directory to the zip file, or search.
679 * @param {String|RegExp} arg The name of the directory to add, or a regex to search folders.
680 * @return {JSZip} an object with the new directory as the root, or an array containing matching folders.
682 folder: function(arg) {
687 if (utils.isRegExp(arg)) {
688 return this.filter(function(relativePath, file) {
689 return file.dir && arg.test(relativePath);
693 // else, name is a new folder
694 var name = this.root + arg;
695 var newFolder = folderAdd.call(this, name);
697 // Allow chaining by returning a new object with this folder as the root
698 var ret = this.clone();
699 ret.root = newFolder.name;
704 * Delete a file, or a directory and all sub-files, from the zip
705 * @param {string} name the name of the file to delete
706 * @return {JSZip} this JSZip object
708 remove: function(name) {
709 name = this.root + name;
710 var file = this.files[name];
712 // Look for any folders
713 if (name.slice(-1) != "/") {
716 file = this.files[name];
719 if (file && !file.dir) {
721 delete this.files[name];
723 // maybe a folder, delete recursively
724 var kids = this.filter(function(relativePath, file) {
725 return file.name.slice(0, name.length) === name;
727 for (var i = 0; i < kids.length; i++) {
728 delete this.files[kids[i].name];
736 * Generate the complete zip file
737 * @param {Object} options the options to generate the zip file :
738 * - base64, (deprecated, use type instead) true to generate base64.
739 * - compression, "STORE" by default.
740 * - type, "base64" by default. Values are : string, base64, uint8array, arraybuffer, blob.
741 * @return {String|Uint8Array|ArrayBuffer|Buffer|Blob} the zip file
743 generate: function(options) {
744 options = extend(options || {}, {
746 compression: "STORE",
747 compressionOptions : null,
751 mimeType: 'application/zip'
754 utils.checkSupport(options.type);
756 // accept nodejs `process.platform`
758 options.platform === 'darwin' ||
759 options.platform === 'freebsd' ||
760 options.platform === 'linux' ||
761 options.platform === 'sunos'
763 options.platform = "UNIX";
765 if (options.platform === 'win32') {
766 options.platform = "DOS";
771 centralDirLength = 0,
773 utfEncodedComment = utils.transformTo("string", this.utf8encode(options.comment || this.comment || ""));
775 // first, generate all the zip parts.
776 for (var name in this.files) {
777 if (!this.files.hasOwnProperty(name)) {
780 var file = this.files[name];
782 var compressionName = file.options.compression || options.compression.toUpperCase();
783 var compression = compressions[compressionName];
785 throw new Error(compressionName + " is not a valid compression method !");
787 var compressionOptions = file.options.compressionOptions || options.compressionOptions || {};
789 var compressedObject = generateCompressedObjectFrom.call(this, file, compression, compressionOptions);
791 var zipPart = generateZipParts.call(this, name, file, compressedObject, localDirLength, options.platform);
792 localDirLength += zipPart.fileRecord.length + compressedObject.compressedSize;
793 centralDirLength += zipPart.dirRecord.length;
794 zipData.push(zipPart);
799 // end of central dir signature
800 dirEnd = signature.CENTRAL_DIRECTORY_END +
801 // number of this disk
803 // number of the disk with the start of the central directory
805 // total number of entries in the central directory on this disk
806 decToHex(zipData.length, 2) +
807 // total number of entries in the central directory
808 decToHex(zipData.length, 2) +
809 // size of the central directory 4 bytes
810 decToHex(centralDirLength, 4) +
811 // offset of start of central directory with respect to the starting disk number
812 decToHex(localDirLength, 4) +
813 // .ZIP file comment length
814 decToHex(utfEncodedComment.length, 2) +
819 // we have all the parts (and the total length)
820 // time to create a writer !
821 var typeName = options.type.toLowerCase();
822 if(typeName==="uint8array"||typeName==="arraybuffer"||typeName==="blob"||typeName==="nodebuffer") {
823 writer = new Uint8ArrayWriter(localDirLength + centralDirLength + dirEnd.length);
825 writer = new StringWriter(localDirLength + centralDirLength + dirEnd.length);
828 for (i = 0; i < zipData.length; i++) {
829 writer.append(zipData[i].fileRecord);
830 writer.append(zipData[i].compressedObject.compressedContent);
832 for (i = 0; i < zipData.length; i++) {
833 writer.append(zipData[i].dirRecord);
836 writer.append(dirEnd);
838 var zip = writer.finalize();
842 switch(options.type.toLowerCase()) {
843 // case "zip is an Uint8Array"
847 return utils.transformTo(options.type.toLowerCase(), zip);
849 return utils.arrayBuffer2Blob(utils.transformTo("arraybuffer", zip), options.mimeType);
850 // case "zip is a string"
852 return (options.base64) ? base64.encode(zip) : zip;
853 default : // case "string" :
861 * This method will be removed in a future version without replacement.
863 crc32: function (input, crc) {
864 return crc32(input, crc);
869 * This method will be removed in a future version without replacement.
871 utf8encode: function (string) {
872 return utils.transformTo("string", utf8.utf8encode(string));
877 * This method will be removed in a future version without replacement.
879 utf8decode: function (input) {
880 return utf8.utf8decode(input);
883 module.exports = out;