diff --git a/documentation/api_jszip/file_data.md b/documentation/api_jszip/file_data.md index e3e1d697..34c7b259 100644 --- a/documentation/api_jszip/file_data.md +++ b/documentation/api_jszip/file_data.md @@ -25,10 +25,35 @@ compression | string | null | If set, specifies compression method to use fo comment | string | null | The comment for this file. optimizedBinaryString | boolean | `false` | Set to true if (and only if) the input is a "binary string" and has already been prepared with a 0xFF mask. createFolders | boolean | `false` | Set to true if folders in the file path should be automatically created, otherwise there will only be virtual folders that represent the path to the file. +unixPermissions | object | null | The UNIX permissions of the file, if any. +dosPermissions | object | null | The DOS permissions of the file, if any. You shouldn't update the data given to this method : it is kept as it so any update will impact the stored data. +__For the permissions__ : + +The `*Permissions` fields has the following structure (the value is the default behavior) : + +```js +unixPermissions : { + executable : false, + readOnly : false +} + +dosPermissions : { + hidden : false, + readOnly : false +} +``` + +The field `unixPermissions` also accepts a number (the 2 bytes file attributes) : +you can use the `mode` attribute of [nodejs' fs.Stats](http://nodejs.org/api/fs.html#fs_class_fs_stats). +In that case, the executable/readOnly boolean will be extracted from the "user" +part (ignoring the "group" and "other"). + +See also [the platform option of generate()]({{site.baseurl}}/documentation/api_jszip/generate.html). + __Returns__ : The current JSZip object, for chaining. __Throws__ : An exception if the data is not in a supported format. @@ -65,6 +90,11 @@ zip.file("animals.txt", "dog,platypus\n").file("people.txt", "james,sebastian\n" zip.file("folder/file.txt", "file in folder", {createFolders: true}); // In this case, the "folder" folder WILL have a 'D'irectory attribute and a Method property of "store". // It will exist whether or not "file.txt" is present. -``` - +zip.file("script.sh", "echo 'hello world'", { + unixPermissions : { + executable : true + } +}); +// when generated with platform:UNIX, the script.sh file will be executable +``` diff --git a/documentation/api_jszip/generate.md b/documentation/api_jszip/generate.md index 95b0ec25..6dd5dc00 100644 --- a/documentation/api_jszip/generate.md +++ b/documentation/api_jszip/generate.md @@ -16,6 +16,7 @@ options.compression | string | `STORE` (no compression) | the default file comp options.type | string | `base64` | The type of zip to return, see below for the other types. options.comment | string | | The comment to use for the zip file. options.mimeType | string | `application/zip` | mime-type for the generated file. Useful when you need to generate a file with a different extension, ie: ".ods". +options.platform | string | `DOS` | The platform to use when generating the zip file. Possible values for `type` : @@ -36,6 +37,11 @@ comment. If not set, JSZip will use the field `comment` on its `options`. +Possible values for `platform` : `DOS` and `UNIX`. It also accepts nodejs +`process.platform` values. +When using `DOS`, the attribute `dosPermissions` of each file is used. +When using `UNIX`, the attribute `unixPermissions` of each file is used. + __Returns__ : The generated zip file. __Throws__ : An exception if the asked `type` is not available in the browser, diff --git a/lib/defaults.js b/lib/defaults.js index 67ce2a69..2dd6d95c 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -6,3 +6,5 @@ exports.createFolders = false; exports.date = null; exports.compression = null; exports.comment = null; +exports.unixPermissions = null; +exports.dosPermissions = null; diff --git a/lib/load.js b/lib/load.js index 5b5f0953..e0031101 100644 --- a/lib/load.js +++ b/lib/load.js @@ -18,6 +18,8 @@ module.exports = function(data, options) { date: input.date, dir: input.dir, comment : input.fileComment.length ? input.fileComment : null, + unixPermissions : input.unixPermissions, + dosPermissions : input.dosPermissions, createFolders: options.createFolders }); } diff --git a/lib/object.js b/lib/object.js index 51469075..afe96b57 100644 --- a/lib/object.js +++ b/lib/object.js @@ -99,6 +99,8 @@ var ZipObject = function(name, data, options) { this.dir = options.dir; this.date = options.date; this.comment = options.comment; + this.unixPermissions = options.unixPermissions; + this.dosPermissions = options.dosPermissions; this._data = data; this.options = options; @@ -256,6 +258,15 @@ var fileAdd = function(name, data, o) { } } + if (typeof o.unixPermissions === "number") { + o.unixPermissions = { + // executable for the owner + executable: !!(o.unixPermissions & 0x0040), + // NOT writable for the owner + readOnly: (o.unixPermissions & 0x0080) === 0 + }; + } + var object = new ZipObject(name, data, o); this.files[name] = object; return object; @@ -348,15 +359,92 @@ var generateCompressedObjectFrom = function(file, compression) { return result; }; + + + +/** + * 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) { + + // I can't use octal values in strict mode, hence the hexa. + var umask = 0x12; // 022 + + var permissions = 0x1FF; // 0777 + + permissions &= ~umask; + + if (!(unixPermissions && unixPermissions.executable) && !isDir) { + permissions &= 0x1B6; // 0666 + } + if (unixPermissions && unixPermissions.readOnly) { + permissions &= 0x16D; // 0555 + } + + var extFileAttr = permissions << 16; + + if (isDir) { + extFileAttr |= 0x4000 << 16; // UNX_IFDIR 0040000 see zipinfo.c + } else { + extFileAttr |= 0x8000 << 16; // UNX_IFREG 0100000 see zipinfo.c + } + + return extFileAttr; +}; + +/** + * 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, isDir) { + + var permissions = 0; + + if (!dosPermissions) { + return permissions; + } + + if (dosPermissions.hidden) { + permissions |= 0x0002; + } + if (dosPermissions.readOnly) { + permissions |= 0x0001; + } + + return permissions; +}; + /** * Generate the various parts used in the construction of the final zip file. * @param {string} name the file name. * @param {ZipObject} file the file content. * @param {JSZip.CompressedObject} compressedObject the compressed object. * @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) * @return {object} the zip parts. */ -var generateZipParts = function(name, file, compressedObject, offset) { +var generateZipParts = function(name, file, compressedObject, offset, platform) { var data = compressedObject.compressedContent, utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)), comment = file.comment || "", @@ -386,6 +474,20 @@ var generateZipParts = function(name, file, compressedObject, offset) { date = o.date; } + var extFileAttr = 0; + var versionMadeBy = 0; + if (dir) { + // dos or unix, we set the dos dir flag + extFileAttr |= 0x00010; + } + if(platform === "UNIX") { + versionMadeBy = 0x0314; // UNIX, version 2.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 @@ -478,7 +580,7 @@ var generateZipParts = function(name, file, compressedObject, offset) { var dirRecord = signature.CENTRAL_FILE_HEADER + // version made by (00: DOS) - "\x14\x00" + + decToHex(versionMadeBy, 2) + // file header (common to file and central directory) header + // file comment length @@ -488,7 +590,7 @@ var generateZipParts = function(name, file, compressedObject, offset) { // internal file attributes TODO "\x00\x00" + // external file attributes - (dir === true ? "\x10\x00\x00\x00" : "\x00\x00\x00\x00") + + decToHex(extFileAttr, 4) + // relative offset of local header decToHex(offset, 4) + // file name @@ -647,12 +749,26 @@ var out = { base64: true, compression: "STORE", type: "base64", + platform: "DOS", comment: null, mimeType: 'application/zip' }); utils.checkSupport(options.type); + // accept nodejs `process.platform` + if( + options.platform === 'darwin' || + options.platform === 'freebsd' || + options.platform === 'linux' || + options.platform === 'sunos' + ) { + options.platform = "UNIX"; + } + if (options.platform === 'win32') { + options.platform = "DOS"; + } + var zipData = [], localDirLength = 0, centralDirLength = 0, @@ -674,7 +790,7 @@ var out = { var compressedObject = generateCompressedObjectFrom.call(this, file, compression); - var zipPart = generateZipParts.call(this, name, file, compressedObject, localDirLength); + var zipPart = generateZipParts.call(this, name, file, compressedObject, localDirLength, options.platform); localDirLength += zipPart.fileRecord.length + compressedObject.compressedSize; centralDirLength += zipPart.dirRecord.length; zipData.push(zipPart); diff --git a/lib/zipEntries.js b/lib/zipEntries.js index 842b58a6..e5f89dc6 100644 --- a/lib/zipEntries.js +++ b/lib/zipEntries.js @@ -109,6 +109,7 @@ ZipEntries.prototype = { this.checkSignature(sig.LOCAL_FILE_HEADER); file.readLocalPart(this.reader); file.handleUTF8(); + file.processAttributes(); } }, /** diff --git a/lib/zipEntry.js b/lib/zipEntry.js index 4e324f75..9383413f 100644 --- a/lib/zipEntry.js +++ b/lib/zipEntry.js @@ -3,6 +3,10 @@ var StringReader = require('./stringReader'); var utils = require('./utils'); var CompressedObject = require('./compressedObject'); var jszipProto = require('./object'); + +var MADE_BY_DOS = 0x00; +var MADE_BY_UNIX = 0x03; + // class ZipEntry {{{ /** * An entry in the zip file. @@ -129,7 +133,7 @@ ZipEntry.prototype = { * @param {DataReader} reader the reader to use. */ readCentralPart: function(reader) { - this.versionMadeBy = reader.readString(2); + this.versionMadeBy = reader.readInt(2); this.versionNeeded = reader.readInt(2); this.bitFlag = reader.readInt(2); this.compressionMethod = reader.readString(2); @@ -153,10 +157,40 @@ ZipEntry.prototype = { this.readExtraFields(reader); this.parseZIP64ExtraField(reader); this.fileComment = reader.readString(this.fileCommentLength); + }, - // warning, this is true only for zip with madeBy == DOS (plateform dependent feature) - this.dir = this.externalFileAttributes & 0x00000010 ? true : false; + /** + * Parse the external file attributes and get the unix/dos permissions. + */ + processAttributes: function () { + this.unixPermissions = null; + this.dosPermissions = null; + var madeBy = this.versionMadeBy >> 8; + + if(madeBy === MADE_BY_DOS) { + this.dosPermissions = { + // Bit 1 Hidden + hidden: !!(this.externalFileAttributes & 0x0002), + // Bit 0 Read-Only + readOnly: !!(this.externalFileAttributes & 0x0001) + }; + // Bit 5 Archive + this.dir = !!(this.externalFileAttributes & 0x0010); + } + + if(madeBy === MADE_BY_UNIX) { + var fullFilePermissions = this.externalFileAttributes >> 16; + this.unixPermissions = fullFilePermissions; + // the octal permissions are in (fullFilePermissions & 0x01FF).toString(8); + this.dir = !!(fullFilePermissions & 0x4000); + } + + // fail safe : if the name ends with a / it probably means a folder + if (!this.dir && this.fileName.slice(-1) === '/') { + this.dir = true; + } }, + /** * Parse the ZIP64 extra field and merge the info in the current ZipEntry. * @param {DataReader} reader the reader to use. diff --git a/test/ref/permissions/linux_7z.zip b/test/ref/permissions/linux_7z.zip new file mode 100644 index 00000000..f2f47374 Binary files /dev/null and b/test/ref/permissions/linux_7z.zip differ diff --git a/test/ref/permissions/linux_ark.zip b/test/ref/permissions/linux_ark.zip new file mode 100644 index 00000000..1937fcca Binary files /dev/null and b/test/ref/permissions/linux_ark.zip differ diff --git a/test/ref/permissions/linux_file_roller-ubuntu.zip b/test/ref/permissions/linux_file_roller-ubuntu.zip new file mode 100644 index 00000000..f32f2222 Binary files /dev/null and b/test/ref/permissions/linux_file_roller-ubuntu.zip differ diff --git a/test/ref/permissions/linux_file_roller-xubuntu.zip b/test/ref/permissions/linux_file_roller-xubuntu.zip new file mode 100644 index 00000000..728da5ab Binary files /dev/null and b/test/ref/permissions/linux_file_roller-xubuntu.zip differ diff --git a/test/ref/permissions/linux_zip.zip b/test/ref/permissions/linux_zip.zip new file mode 100644 index 00000000..66bea723 Binary files /dev/null and b/test/ref/permissions/linux_zip.zip differ diff --git a/test/ref/permissions/mac_finder.zip b/test/ref/permissions/mac_finder.zip new file mode 100644 index 00000000..99d5badb Binary files /dev/null and b/test/ref/permissions/mac_finder.zip differ diff --git a/test/ref/permissions/windows_7z.zip b/test/ref/permissions/windows_7z.zip new file mode 100644 index 00000000..fbb71bb6 Binary files /dev/null and b/test/ref/permissions/windows_7z.zip differ diff --git a/test/ref/permissions/windows_compressed_folders.zip b/test/ref/permissions/windows_compressed_folders.zip new file mode 100644 index 00000000..c1e46c0d Binary files /dev/null and b/test/ref/permissions/windows_compressed_folders.zip differ diff --git a/test/ref/permissions/windows_izarc.zip b/test/ref/permissions/windows_izarc.zip new file mode 100644 index 00000000..7422401f Binary files /dev/null and b/test/ref/permissions/windows_izarc.zip differ diff --git a/test/ref/permissions/windows_winrar.zip b/test/ref/permissions/windows_winrar.zip new file mode 100644 index 00000000..9439a112 Binary files /dev/null and b/test/ref/permissions/windows_winrar.zip differ diff --git a/test/test.js b/test/test.js index 69e2a24f..8731abbe 100644 --- a/test/test.js +++ b/test/test.js @@ -1231,6 +1231,83 @@ test("A folder stays a folder", function () { ok(reloaded.files['folder/'].options.dir, "the folder is marked as a folder, deprecated API"); }); +// mkdir dir dir_ro +// touch file file_ro file_exe file_ro_exe +// chmod -w dir_ro file_ro file_ro_exe +// chmod +x file_exe file_ro_exe +// then : +// zip -r linux_zip.zip . +// 7z a -r linux_7z.zip . +// ... +function assertUnixPermissions(file){ + function doAsserts(fileName, dir, exec, ro) { + equal(zip.files[fileName].dosPermissions, null, fileName + ", no DOS permissions"); + equal(zip.files[fileName].dir, dir, fileName + " dir flag"); + equal(zip.files[fileName].unixPermissions.executable, exec, fileName + " executable flag"); + equal(zip.files[fileName].unixPermissions.readOnly, ro, fileName + " readOnly flag"); + } + + var zip = new JSZip(file); + doAsserts("dir/", true, true, false); + doAsserts("dir_ro/", true, true, true); + doAsserts("file", false, false, false); + doAsserts("file_ro", false, false, true); + doAsserts("file_exe", false, true, false); + doAsserts("file_ro_exe", false, true, true); +} + +function assertDosPermissions(file){ + function doAsserts(fileName, dir, hidden, ro) { + equal(zip.files[fileName].unixPermissions, null, fileName + ", no UNIX permissions"); + equal(zip.files[fileName].dir, dir, fileName + " dir flag"); + equal(zip.files[fileName].dosPermissions.hidden, hidden, fileName + " hidden flag"); + equal(zip.files[fileName].dosPermissions.readOnly, ro, fileName + " readOnly flag"); + } + + var zip = new JSZip(file); + if (zip.files["dir/"]) { + doAsserts("dir/", true, false, false); + } + if (zip.files["dir_hidden/"]) { + doAsserts("dir_hidden/", true, true, false); + } + doAsserts("file", false, false, false); + doAsserts("file_ro", false, false, true); + doAsserts("file_hidden", false, true, false); + doAsserts("file_ro_hidden", false, true, true); +} +function reloadAndAssertUnixPermissions(file){ + var zip = new JSZip(file); + assertUnixPermissions(zip.generate({type:"string", platform:"UNIX"})); +} +function reloadAndAssertDosPermissions(file){ + var zip = new JSZip(file); + assertDosPermissions(zip.generate({type:"string", platform:"DOS"})); +} +testZipFile("permissions on linux : file created by zip", "ref/permissions/linux_zip.zip", assertUnixPermissions); +testZipFile("permissions on linux : file created by zip, reloaded", "ref/permissions/linux_zip.zip", reloadAndAssertUnixPermissions); +testZipFile("permissions on linux : file created by 7z", "ref/permissions/linux_7z.zip", assertUnixPermissions); +testZipFile("permissions on linux : file created by 7z, reloaded", "ref/permissions/linux_7z.zip", reloadAndAssertUnixPermissions); +testZipFile("permissions on linux : file created by file-roller on ubuntu", "ref/permissions/linux_file_roller-ubuntu.zip", assertUnixPermissions); +testZipFile("permissions on linux : file created by file-roller on ubuntu, reloaded", "ref/permissions/linux_file_roller-ubuntu.zip", reloadAndAssertUnixPermissions); +testZipFile("permissions on linux : file created by file-roller on xubuntu", "ref/permissions/linux_file_roller-xubuntu.zip", assertUnixPermissions); +testZipFile("permissions on linux : file created by file-roller on xubuntu, reloaded", "ref/permissions/linux_file_roller-xubuntu.zip", reloadAndAssertUnixPermissions); +testZipFile("permissions on linux : file created by ark", "ref/permissions/linux_ark.zip", assertUnixPermissions); +testZipFile("permissions on linux : file created by ark, reloaded", "ref/permissions/linux_ark.zip", reloadAndAssertUnixPermissions); +testZipFile("permissions on mac : file created by finder", "ref/permissions/mac_finder.zip", assertUnixPermissions); +testZipFile("permissions on mac : file created by finder, reloaded", "ref/permissions/mac_finder.zip", reloadAndAssertUnixPermissions); + + + +testZipFile("permissions on windows : file created by the compressed folders feature", "ref/permissions/windows_compressed_folders.zip", assertDosPermissions); +testZipFile("permissions on windows : file created by the compressed folders feature, reloaded", "ref/permissions/windows_compressed_folders.zip", reloadAndAssertDosPermissions); +testZipFile("permissions on windows : file created by 7z", "ref/permissions/windows_7z.zip", assertDosPermissions); +testZipFile("permissions on windows : file created by 7z, reloaded", "ref/permissions/windows_7z.zip", reloadAndAssertDosPermissions); +testZipFile("permissions on windows : file created by izarc", "ref/permissions/windows_izarc.zip", assertDosPermissions); +testZipFile("permissions on windows : file created by izarc, reloaded", "ref/permissions/windows_izarc.zip", reloadAndAssertDosPermissions); +testZipFile("permissions on windows : file created by winrar", "ref/permissions/windows_winrar.zip", assertDosPermissions); +testZipFile("permissions on windows : file created by winrar, reloaded", "ref/permissions/windows_winrar.zip", reloadAndAssertDosPermissions); + // }}} Load file QUnit.module("Load complex files"); // {{{