Skip to content

Commit

Permalink
Add support for UNIX / DOS permissions
Browse files Browse the repository at this point in the history
Two new fields on ZipObject, `unixPermissions` and `dosPermissions`, hold the
UNIX or DOS permissions of the file. A new option of `generate()`,
`platform` (DOS or UNIX) controls the use of the permissions.

The default behavior is to generate DOS archives, without any
permissions, like before.

Bonus side-effect : Finder on mac doesn't use the DOS directory flag,
JSZip didn't properly recognize folders until now.

Fix Stuk#194 and Stuk#198.
  • Loading branch information
dduponchel committed Feb 18, 2015
1 parent 8e21585 commit bb4984d
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 9 deletions.
34 changes: 32 additions & 2 deletions documentation/api_jszip/file_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
```
6 changes: 6 additions & 0 deletions documentation/api_jszip/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` :

Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions lib/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ exports.createFolders = false;
exports.date = null;
exports.compression = null;
exports.comment = null;
exports.unixPermissions = null;
exports.dosPermissions = null;
2 changes: 2 additions & 0 deletions lib/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
Expand Down
124 changes: 120 additions & 4 deletions lib/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 || "",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions lib/zipEntries.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ ZipEntries.prototype = {
this.checkSignature(sig.LOCAL_FILE_HEADER);
file.readLocalPart(this.reader);
file.handleUTF8();
file.processAttributes();
}
},
/**
Expand Down
40 changes: 37 additions & 3 deletions lib/zipEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down
Binary file added test/ref/permissions/linux_7z.zip
Binary file not shown.
Binary file added test/ref/permissions/linux_ark.zip
Binary file not shown.
Binary file added test/ref/permissions/linux_file_roller-ubuntu.zip
Binary file not shown.
Binary file added test/ref/permissions/linux_file_roller-xubuntu.zip
Binary file not shown.
Binary file added test/ref/permissions/linux_zip.zip
Binary file not shown.
Binary file added test/ref/permissions/mac_finder.zip
Binary file not shown.
Binary file added test/ref/permissions/windows_7z.zip
Binary file not shown.
Binary file added test/ref/permissions/windows_compressed_folders.zip
Binary file not shown.
Binary file added test/ref/permissions/windows_izarc.zip
Binary file not shown.
Binary file added test/ref/permissions/windows_winrar.zip
Binary file not shown.
Loading

0 comments on commit bb4984d

Please sign in to comment.