├── .gitattributes ├── .gitignore ├── test ├── assets │ ├── file-mode-pack │ │ ├── store.zip │ │ ├── deflate.zip │ │ ├── spec.json │ │ └── generate.js │ ├── main-test-pack │ │ ├── store.zip │ │ ├── deflate.zip │ │ ├── spec.json │ │ └── generate.js │ └── restrict-pack │ │ └── escape.zip └── test.js ├── .travis.yml ├── changelog.md ├── .editorconfig ├── lib ├── signatures.js ├── file-details.js ├── extractors.js ├── structures.js └── decompress-zip.js ├── license ├── package.json ├── .jshintrc ├── Gruntfile.js ├── bin └── decompress-zip └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /npm-debug.log 3 | /coverage 4 | /test/assets/**/extracted 5 | -------------------------------------------------------------------------------- /test/assets/file-mode-pack/store.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bower/decompress-zip/HEAD/test/assets/file-mode-pack/store.zip -------------------------------------------------------------------------------- /test/assets/main-test-pack/store.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bower/decompress-zip/HEAD/test/assets/main-test-pack/store.zip -------------------------------------------------------------------------------- /test/assets/restrict-pack/escape.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bower/decompress-zip/HEAD/test/assets/restrict-pack/escape.zip -------------------------------------------------------------------------------- /test/assets/file-mode-pack/deflate.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bower/decompress-zip/HEAD/test/assets/file-mode-pack/deflate.zip -------------------------------------------------------------------------------- /test/assets/main-test-pack/deflate.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bower/decompress-zip/HEAD/test/assets/main-test-pack/deflate.zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'iojs' 5 | - '0.12' 6 | - '0.10' 7 | before_script: 8 | - grunt travis 9 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 0.3.2 and 0.2.2 2 | 3 | - Fix Zip Slip Vulnerability: https://snyk.io/research/zip-slip-vulnerability 4 | 5 | # 0.3.0 6 | 7 | - Enable file mode preservation 8 | 9 | # 0.2.1 10 | 11 | - Update graceful-fs to 4.x 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /lib/signatures.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | LOCAL_FILE_HEADER: 0x04034b50, 3 | DATA_DESCRIPTOR_RECORD: 0x08074b50, 4 | ARCHIVE_EXTRA_DATA: 0x08064b50, 5 | CENTRAL_FILE_HEADER: 0x02014b50, 6 | HEADER: 0x05054b50, 7 | ZIP64_END_OF_CENTRAL_DIRECTORY: 0x06064b50, 8 | ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR: 0x07064b50, 9 | END_OF_CENTRAL_DIRECTORY: 0x06054b50 10 | }; 11 | -------------------------------------------------------------------------------- /test/assets/file-mode-pack/spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extracted", 3 | "type": "dir", 4 | "mode": 16877, 5 | "size": 6, 6 | "children": [ 7 | { 8 | "name": "dir1", 9 | "type": "dir", 10 | "mode": 16877, 11 | "size": 0, 12 | "children": [], 13 | "sha1": null 14 | }, 15 | { 16 | "name": "dir2", 17 | "type": "dir", 18 | "mode": 16841, 19 | "size": 0, 20 | "children": [], 21 | "sha1": null 22 | }, 23 | { 24 | "name": "file1", 25 | "type": "file", 26 | "size": 3, 27 | "sha1": "a9993e364706816aba3e25717850c26c9cd0d89d", 28 | "mode": 33261 29 | }, 30 | { 31 | "name": "file2", 32 | "type": "file", 33 | "size": 3, 34 | "sha1": "66b27417d37e024c46526c2f6d358a754fc552f3", 35 | "mode": 33225 36 | } 37 | ], 38 | "sha1": "ac6f83cc555959a5817f979deb9e845d4d6962d2" 39 | } -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Bower team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test/assets/main-test-pack/spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extracted", 3 | "type": "dir", 4 | "size": 2098176, 5 | "children": [ 6 | { 7 | "name": "dir1", 8 | "type": "dir", 9 | "size": 2098176, 10 | "children": [ 11 | { 12 | "name": "dir2", 13 | "type": "dir", 14 | "size": 2098176, 15 | "children": [ 16 | { 17 | "name": "0B", 18 | "type": "file", 19 | "size": 0, 20 | "sha1": null 21 | }, 22 | { 23 | "name": "1KiB", 24 | "type": "file", 25 | "size": 1024, 26 | "sha1": "5b00669c480d5cffbdfa8bdba99561160f2d1b77" 27 | }, 28 | { 29 | "name": "2MiB", 30 | "type": "file", 31 | "size": 2097152, 32 | "sha1": "3394ba403303c0784f836bdb1ee13a4bfd14e6de" 33 | } 34 | ], 35 | "sha1": "a4e89ec3ee096347221e9823f55b207256cedfcd" 36 | } 37 | ], 38 | "sha1": "8117a33e003049889f6446abf29f813d59e932dd" 39 | }, 40 | { 41 | "name": "empty", 42 | "type": "dir", 43 | "size": 0, 44 | "children": [], 45 | "sha1": null 46 | } 47 | ], 48 | "sha1": "7ee8998d84b7092c18b07d3853b8103a18a722ae" 49 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decompress-zip", 3 | "version": "0.3.2", 4 | "description": "Extract files from a ZIP archive", 5 | "main": "lib/decompress-zip.js", 6 | "scripts": { 7 | "test": "grunt test" 8 | }, 9 | "bin": "bin/decompress-zip", 10 | "repository": "bower/decompress-zip", 11 | "engines": { 12 | "node": ">=0.10.0" 13 | }, 14 | "keywords": [ 15 | "zip", 16 | "unzip", 17 | "tar", 18 | "untar", 19 | "compress", 20 | "decompress", 21 | "archive", 22 | "extract", 23 | "zlib" 24 | ], 25 | "author": "Bower", 26 | "license": "MIT", 27 | "dependencies": { 28 | "binary": "^0.3.0", 29 | "graceful-fs": "^4.1.3", 30 | "mkpath": "^0.1.0", 31 | "nopt": "^3.0.1", 32 | "q": "^1.1.2", 33 | "readable-stream": "^1.1.8", 34 | "touch": "0.0.3" 35 | }, 36 | "devDependencies": { 37 | "archiver": "^0.13.1", 38 | "chai": "^1.10.0", 39 | "coveralls": "^2.11.2", 40 | "fs-jetpack": "^0.5.3", 41 | "grunt": "^0.4.1", 42 | "grunt-cli": "^0.1.13", 43 | "grunt-contrib-jshint": "^0.11.0", 44 | "grunt-contrib-watch": "^0.6.1", 45 | "grunt-exec": "^0.4.2", 46 | "grunt-simple-mocha": "^0.4.0", 47 | "istanbul": "^0.3.5", 48 | "mocha": "^2.1.0", 49 | "tmp": "0.0.24" 50 | }, 51 | "files": [ 52 | "bin", 53 | "lib" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "console", 4 | "describe", 5 | "it", 6 | "after", 7 | "afterEach", 8 | "before", 9 | "beforeEach" 10 | ], 11 | 12 | "indent": 4, 13 | "node": true, 14 | "devel": true, 15 | 16 | "bitwise": false, 17 | "curly": false, 18 | "eqeqeq": true, 19 | "forin": false, 20 | "immed": true, 21 | "latedef": false, 22 | "newcap": true, 23 | "noarg": true, 24 | "noempty": false, 25 | "nonew": true, 26 | "plusplus": false, 27 | "regexp": false, 28 | "undef": true, 29 | "unused": "vars", 30 | "quotmark": "single", 31 | "strict": false, 32 | "trailing": true, 33 | "camelcase": true, 34 | 35 | "asi": false, 36 | "boss": true, 37 | "debug": false, 38 | "eqnull": true, 39 | "es5": false, 40 | "esnext": false, 41 | "evil": false, 42 | "expr": false, 43 | "funcscope": false, 44 | "globalstrict": false, 45 | "iterator": false, 46 | "lastsemic": false, 47 | "laxbreak": true, 48 | "laxcomma": false, 49 | "loopfunc": true, 50 | "multistr": false, 51 | "onecase": true, 52 | "regexdash": false, 53 | "scripturl": false, 54 | "smarttabs": false, 55 | "shadow": false, 56 | "sub": false, 57 | "supernew": true, 58 | "validthis": false, 59 | 60 | "nomen": false, 61 | "white": true 62 | } 63 | -------------------------------------------------------------------------------- /lib/file-details.js: -------------------------------------------------------------------------------- 1 | // Objects with this prototype are used as the public representation of a file 2 | var path = require('path'); 3 | 4 | var FileDetails = function (directoryEntry) { 5 | // TODO: Add 'extra field' support 6 | 7 | this._offset = 0; 8 | this._maxSize = 0; 9 | 10 | this.parent = path.dirname(directoryEntry.fileName); 11 | this.filename = path.basename(directoryEntry.fileName); 12 | this.path = path.normalize(directoryEntry.fileName); 13 | 14 | this.type = directoryEntry.fileAttributes.type; 15 | this.mode = directoryEntry.fileAttributes.mode; 16 | this.compressionMethod = directoryEntry.compressionMethod; 17 | this.modified = directoryEntry.modifiedTime; 18 | this.crc32 = directoryEntry.crc32; 19 | this.compressedSize = directoryEntry.compressedSize; 20 | this.uncompressedSize = directoryEntry.uncompressedSize; 21 | this.comment = directoryEntry.fileComment; 22 | 23 | this.flags = { 24 | encrypted: directoryEntry.generalPurposeFlags.encrypted, 25 | compressionFlag1: directoryEntry.generalPurposeFlags.compressionFlag1, 26 | compressionFlag2: directoryEntry.generalPurposeFlags.compressionFlag2, 27 | useDataDescriptor: directoryEntry.generalPurposeFlags.useDataDescriptor, 28 | enhancedDeflating: directoryEntry.generalPurposeFlags.enhancedDeflating, 29 | compressedPatched: directoryEntry.generalPurposeFlags.compressedPatched, 30 | strongEncryption: directoryEntry.generalPurposeFlags.strongEncryption, 31 | utf8: directoryEntry.generalPurposeFlags.utf8, 32 | encryptedCD: directoryEntry.generalPurposeFlags.encryptedCD 33 | }; 34 | 35 | }; 36 | 37 | module.exports = FileDetails; 38 | -------------------------------------------------------------------------------- /test/assets/file-mode-pack/generate.js: -------------------------------------------------------------------------------- 1 | // This script generates zip files inside this folder. 2 | 3 | 'use strict'; 4 | 5 | var jetpack = require('fs-jetpack'); 6 | var archiver = require('archiver'); 7 | 8 | var mainDir = jetpack.cwd(__dirname); 9 | 10 | // ------------------------------------------------------- 11 | // Generate files and folders which we next will compress 12 | // ------------------------------------------------------- 13 | 14 | var extractedDir = mainDir.dir('extracted', { empty: true, mode: '755' }); 15 | extractedDir 16 | .dir('dir1', { mode: '755' }) 17 | .cwd('..') 18 | .dir('dir2', { mode: '711' }) 19 | .cwd('..') 20 | .file('file1', { content: 'abc', mode: '755' }) 21 | .file('file2', { content: 'xyz', mode: '711' }); 22 | 23 | // ------------------------------------------------------- 24 | // Generate spec file 25 | // ------------------------------------------------------- 26 | 27 | // Generate spec file to which we can later compare 28 | // our extracted stuff during tests. 29 | var spec = mainDir.inspectTree('extracted', { checksum: 'sha1', mode: true }); 30 | mainDir.write('spec.json', spec, { jsonIndent: 2 }); 31 | 32 | // ------------------------------------------------------- 33 | // Compress to zip files 34 | // ------------------------------------------------------- 35 | 36 | var compress = function (dest, useStore) { 37 | var output = mainDir.createWriteStream(dest); 38 | output.on('close', function () { 39 | console.log('Archive ' + dest + ' created.'); 40 | }); 41 | var archive = archiver('zip', { store: useStore }); 42 | archive.on('error', function (err){ 43 | console.log(err); 44 | }); 45 | archive.pipe(output); 46 | archive.bulk([ 47 | { expand: true, cwd: extractedDir.path(), src: ['**'] } 48 | ]); 49 | archive.finalize(); 50 | }; 51 | 52 | compress('store.zip', true); 53 | compress('deflate.zip', false); 54 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function (grunt) { 3 | grunt.initConfig({ 4 | jshint: { 5 | options: { 6 | jshintrc: '.jshintrc' 7 | }, 8 | files: [ 9 | 'Gruntfile.js', 10 | 'bin/*', 11 | 'lib/**/*.js', 12 | 'test/*.js' 13 | ] 14 | }, 15 | simplemocha: { 16 | options: { 17 | reporter: 'spec', 18 | timeout: '5000' 19 | }, 20 | full: { 21 | src: [ 22 | 'test/*.js' 23 | ] 24 | }, 25 | short: { 26 | options: { 27 | reporter: 'dot' 28 | }, 29 | src: [ 30 | '<%= simplemocha.full.src %>' 31 | ] 32 | } 33 | }, 34 | exec: { 35 | coverage: { 36 | command: 'node node_modules/istanbul/lib/cli.js cover --dir ./coverage node_modules/mocha/bin/_mocha -- -R dot test/*.js' 37 | }, 38 | coveralls: { 39 | command: 'node node_modules/.bin/coveralls < coverage/lcov.info' 40 | } 41 | }, 42 | watch: { 43 | files: [ 44 | '<%= jshint.files %>' 45 | ], 46 | tasks: [ 47 | 'jshint', 48 | 'simplemocha:short' 49 | ] 50 | } 51 | }); 52 | 53 | grunt.loadNpmTasks('grunt-contrib-jshint'); 54 | grunt.loadNpmTasks('grunt-contrib-watch'); 55 | grunt.loadNpmTasks('grunt-simple-mocha'); 56 | grunt.loadNpmTasks('grunt-exec'); 57 | 58 | grunt.registerTask('test', ['jshint', 'simplemocha:full']); 59 | grunt.registerTask('coverage', 'exec:coverage'); 60 | grunt.registerTask('travis', ['exec:coverage', 'exec:coveralls']); 61 | grunt.registerTask('default', 'test'); 62 | }; 63 | -------------------------------------------------------------------------------- /test/assets/main-test-pack/generate.js: -------------------------------------------------------------------------------- 1 | // This script generates zip files inside this folder. 2 | 3 | 'use strict'; 4 | 5 | var jetpack = require('fs-jetpack'); 6 | var archiver = require('archiver'); 7 | 8 | var mainDir = jetpack.cwd(__dirname); 9 | 10 | // ------------------------------------------------------- 11 | // Generate files and folders which we next will compress 12 | // ------------------------------------------------------- 13 | 14 | var fillWithSillyBytes = function (buf) { 15 | // Very predictable pattern leads to high compression 16 | // and not blowing up the size of this repo. 17 | for (var i = 0; i < buf.length; i += 1) { 18 | buf[i] = i % 256; 19 | } 20 | return buf; 21 | }; 22 | 23 | var zero = new Buffer(0); 24 | var oneKib = fillWithSillyBytes(new Buffer(1024)); 25 | var twoMib = fillWithSillyBytes(new Buffer(1024 * 1024 * 2)); 26 | 27 | var extractedDir = mainDir.dir('extracted', { empty: true }); 28 | extractedDir 29 | .dir('empty') 30 | .cwd('..') 31 | .dir('dir1') 32 | .dir('dir2') 33 | .file('0B', { content: zero }) 34 | .file('1KiB', { content: oneKib }) 35 | .file('2MiB', { content: twoMib }); 36 | 37 | // ------------------------------------------------------- 38 | // Generate spec file 39 | // ------------------------------------------------------- 40 | 41 | // Generate spec file to which we can later compare 42 | // our extracted stuff during tests. 43 | var spec = mainDir.inspectTree('extracted', { checksum: 'sha1' }); 44 | mainDir.write('spec.json', spec, { jsonIndent: 2 }); 45 | 46 | // ------------------------------------------------------- 47 | // Compress to zip files 48 | // ------------------------------------------------------- 49 | 50 | var compress = function (dest, useStore) { 51 | var output = mainDir.createWriteStream(dest); 52 | output.on('close', function () { 53 | console.log('Archive ' + dest + ' created.'); 54 | }); 55 | var archive = archiver('zip', { store: useStore }); 56 | archive.on('error', function (err){ 57 | console.log(err); 58 | }); 59 | archive.pipe(output); 60 | archive.bulk([ 61 | { expand: true, cwd: extractedDir.path(), src: ['**'] } 62 | ]); 63 | archive.finalize(); 64 | }; 65 | 66 | compress('store.zip', true); 67 | compress('deflate.zip', false); 68 | -------------------------------------------------------------------------------- /bin/decompress-zip: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | var nopt = require('nopt'); 4 | var path = require('path'); 5 | var version = require('../package.json').version; 6 | 7 | var knownOptions = { 8 | 'list': Boolean, 9 | 'extract': Boolean, 10 | 'path': path 11 | }; 12 | 13 | var shortcuts = { 14 | 'x': ['--extract'], 15 | 'l': ['--list'], 16 | 'p': ['--path'], 17 | 'v': ['--version'] 18 | }; 19 | 20 | var parsedOptions = nopt(knownOptions, shortcuts); 21 | 22 | var pad = function (string, length) { 23 | string = String(string); 24 | 25 | if (length <= string.length) { 26 | return string; 27 | } 28 | 29 | return string + (new Array(length - string.length).join(' ')); 30 | }; 31 | 32 | var octal = function (number, digits) { 33 | var result = ''; 34 | 35 | for (var i = 0; i < digits; i++) { 36 | result = (number & 0x07) + result; 37 | number >>= 3; 38 | } 39 | 40 | return result; 41 | }; 42 | 43 | var DecompressZip = require('../lib/decompress-zip'); 44 | var zip = new DecompressZip(parsedOptions.argv.remain[0]); 45 | 46 | zip.on('file', function (file) { 47 | console.log([octal(file.mode, 4), pad(file.type, 13), pad(file.compressedSize, 10), pad(file.uncompressedSize, 10), file.path].join(' ')); 48 | }); 49 | 50 | zip.on('list', function (fileList) { 51 | // console.log(fileList); 52 | }); 53 | 54 | zip.on('extract', function (result) { 55 | console.log(result); 56 | }); 57 | 58 | zip.on('error', function (error) { 59 | console.error(error.message, error.stack); 60 | }); 61 | 62 | if (parsedOptions.version) { 63 | console.log('version ' + version); 64 | } else if (parsedOptions.list) { 65 | console.log('Mode Type Zip size Full size Path'); 66 | console.log('---- ---- -------- --------- ----'); 67 | zip.list(); 68 | } else if (parsedOptions.extract) { 69 | var options = {}; 70 | 71 | if (parsedOptions.path) { 72 | options.path = parsedOptions.path; 73 | } 74 | 75 | zip.extract(options); 76 | } else { 77 | console.log('Usage: decompress-zip '); 78 | console.log(' -x, --extract extract the given file'); 79 | console.log(' -l, --list list the contents of the given file'); 80 | console.log(' -v, --version extract the given file'); 81 | console.log(' -p, --path extract the file into '); 82 | console.log(' -h, --help show this message'); 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # decompress-zip [![Build Status](https://travis-ci.org/bower/decompress-zip.svg?branch=master)](https://travis-ci.org/bower/decompress-zip) [![Coverage Status](https://coveralls.io/repos/bower/decompress-zip/badge.png?branch=master)](https://coveralls.io/r/bower/decompress-zip?branch=master) 2 | 3 | > Extract files from a ZIP archive 4 | 5 | 6 | ## Usage 7 | 8 | ### .extract(options) 9 | 10 | Extracts the contents of the ZIP archive `file`. 11 | 12 | Returns an EventEmitter with two possible events - `error` on an error, and `extract` when the extraction has completed. The value passed to the `extract` event is a basic log of each file and how it was compressed. 13 | 14 | **Options** 15 | - **path** *String* - Path to extract into (default `.`) 16 | - **follow** *Boolean* - If true, rather than create stored symlinks as symlinks make a shallow copy of the target instead (default `false`) 17 | - **filter** *Function* - A function that will be called once for each file in the archive. It takes one argument which is an object containing details of the file. Return true for any file that you want to extract, and false otherwise. (default `null`) 18 | - **strip** *Number* - Remove leading folders in the path structure. Equivalent to `--strip-components` for tar. 19 | - **restrict** *Boolean* - If true, will restrict files from being created outside `options.path`. Setting to `false` has significant security [implications](https://snyk.io/research/zip-slip-vulnerability) if you are extracting untrusted data. (default `true`) 20 | 21 | ```js 22 | var DecompressZip = require('decompress-zip'); 23 | var unzipper = new DecompressZip(filename) 24 | 25 | unzipper.on('error', function (err) { 26 | console.log('Caught an error'); 27 | }); 28 | 29 | unzipper.on('extract', function (log) { 30 | console.log('Finished extracting'); 31 | }); 32 | 33 | unzipper.on('progress', function (fileIndex, fileCount) { 34 | console.log('Extracted file ' + (fileIndex + 1) + ' of ' + fileCount); 35 | }); 36 | 37 | unzipper.extract({ 38 | path: 'some/path', 39 | filter: function (file) { 40 | return file.type !== "SymbolicLink"; 41 | } 42 | }); 43 | ``` 44 | 45 | If `path` does not exist, decompress-zip will attempt to create it first. 46 | 47 | ### .list() 48 | 49 | Much like extract, except: 50 | - the success event is `list` 51 | - the data for the event is an array of paths 52 | - no files are actually extracted 53 | - there are no options 54 | 55 | ```js 56 | var DecompressZip = require('decompress-zip'); 57 | var unzipper = new DecompressZip(filename) 58 | 59 | unzipper.on('error', function (err) { 60 | console.log('Caught an error'); 61 | }); 62 | 63 | unzipper.on('list', function (files) { 64 | console.log('The archive contains:'); 65 | console.log(files); 66 | }); 67 | 68 | unzipper.list(); 69 | ``` 70 | 71 | 72 | ## License 73 | 74 | MIT © Bower team 75 | -------------------------------------------------------------------------------- /lib/extractors.js: -------------------------------------------------------------------------------- 1 | var stream = require('stream'); 2 | if (!stream.Readable) { 3 | var stream = require('readable-stream'); 4 | } 5 | var fs = require('graceful-fs'); 6 | var Q = require('q'); 7 | var path = require('path'); 8 | var zlib = require('zlib'); 9 | var touch = Q.denodeify(require('touch')); 10 | var mkpath = Q.denodeify(require('mkpath')); 11 | var writeFile = Q.denodeify(fs.writeFile); 12 | var inflateRaw = Q.denodeify(zlib.inflateRaw); 13 | var symlink = Q.denodeify(fs.symlink); 14 | var stat = Q.denodeify(fs.stat); 15 | 16 | // Use a cache of promises for building the directory tree. This allows us to 17 | // correctly queue up file extractions for after their path has been created, 18 | // avoid trying to create the path twice and still be async. 19 | var mkdir = function (dir, cache, mode) { 20 | dir = path.normalize(path.resolve(process.cwd(), dir) + path.sep); 21 | if (mode === undefined) { 22 | mode = parseInt('777', 8) & (~process.umask()); 23 | } 24 | 25 | if (!cache[dir]) { 26 | var parent; 27 | 28 | if (fs.existsSync(dir)) { 29 | parent = new Q(); 30 | } else { 31 | parent = mkdir(path.dirname(dir), cache, mode); 32 | } 33 | 34 | cache[dir] = parent.then(function () { 35 | return mkpath(dir, mode); 36 | }); 37 | } 38 | 39 | return cache[dir]; 40 | }; 41 | 42 | // Utility methods for writing output files 43 | var extractors = { 44 | folder: function (folder, destination, zip) { 45 | return mkdir(destination, zip.dirCache, folder.mode) 46 | .then(function () { 47 | return {folder: folder.path}; 48 | }); 49 | }, 50 | store: function (file, destination, zip) { 51 | var writer; 52 | 53 | if (file.uncompressedSize === 0) { 54 | writer = touch.bind(null, destination); 55 | } else if (file.uncompressedSize <= zip.chunkSize) { 56 | writer = function () { 57 | return zip.getBuffer(file._offset, file._offset + file.uncompressedSize) 58 | .then(function (buffer) { 59 | return writeFile(destination, buffer, { mode: file.mode }); 60 | }); 61 | }; 62 | } else { 63 | var input = new stream.Readable(); 64 | input.wrap(fs.createReadStream(zip.filename, {start: file._offset, end: file._offset + file.uncompressedSize - 1})); 65 | writer = pipePromise.bind(null, input, destination, { mode: file.mode }); 66 | } 67 | 68 | return mkdir(path.dirname(destination), zip.dirCache) 69 | .then(writer) 70 | .then(function () { 71 | return {stored: file.path}; 72 | }); 73 | }, 74 | deflate: function (file, destination, zip) { 75 | // For Deflate you don't actually need to specify the end offset - and 76 | // in fact many ZIP files don't include compressed file sizes for 77 | // Deflated files so we don't even know what the end offset is. 78 | 79 | return mkdir(path.dirname(destination), zip.dirCache) 80 | .then(function () { 81 | if (file._maxSize <= zip.chunkSize) { 82 | return zip.getBuffer(file._offset, file._offset + file._maxSize) 83 | .then(inflateRaw) 84 | .then(function (buffer) { 85 | return writeFile(destination, buffer, { mode: file.mode }); 86 | }); 87 | } else { 88 | // For node 0.8 we need to create the Zlib stream and attach 89 | // handlers in the same tick of the event loop, which is why we do 90 | // the creation in here 91 | var input = new stream.Readable(); 92 | input.wrap(fs.createReadStream(zip.filename, {start: file._offset})); 93 | var inflater = input.pipe(zlib.createInflateRaw({highWaterMark: 32 * 1024})); 94 | 95 | return pipePromise(inflater, destination, { mode: file.mode }); 96 | } 97 | }) 98 | .then(function () { 99 | return {deflated: file.path}; 100 | }); 101 | }, 102 | symlink: function (file, destination, zip, basePath) { 103 | var parent = path.dirname(destination); 104 | return mkdir(parent, zip.dirCache) 105 | .then(function () { 106 | return getLinkLocation(file, destination, zip, basePath); 107 | }) 108 | .then(function (linkTo) { 109 | return symlink(path.resolve(parent, linkTo), destination) 110 | .then(function () { 111 | return {symlink: file.path, linkTo: linkTo}; 112 | }); 113 | }); 114 | }, 115 | // Make a shallow copy of the file/directory this symlink points to instead 116 | // of actually creating a link 117 | copy: function (file, destination, zip, basePath) { 118 | var type; 119 | var parent = path.dirname(destination); 120 | 121 | return mkdir(parent, zip.dirCache) 122 | .then(function () { 123 | return getLinkLocation(file, destination, zip, basePath); 124 | }) 125 | .then(function (linkTo) { 126 | return stat(path.resolve(parent, linkTo)) 127 | .then(function (stats) { 128 | if (stats.isFile()) { 129 | type = 'File'; 130 | var input = new stream.Readable(); 131 | input.wrap(fs.createReadStream(path.resolve(parent, linkTo))); 132 | return pipePromise(input, destination); 133 | } else if (stats.isDirectory()) { 134 | type = 'Directory'; 135 | return mkdir(destination, zip.dirCache); 136 | } else { 137 | throw new Error('Could not follow symlink to unknown file type'); 138 | } 139 | }) 140 | .then(function () { 141 | return {copy: file.path, original: linkTo, type: type}; 142 | }); 143 | }); 144 | } 145 | }; 146 | 147 | var getLinkLocation = function (file, destination, zip, basePath) { 148 | var parent = path.dirname(destination); 149 | return zip.getBuffer(file._offset, file._offset + file.uncompressedSize) 150 | .then(function (buffer) { 151 | var linkTo = buffer.toString(); 152 | var fullLink = path.resolve(parent, linkTo); 153 | 154 | if (path.relative(basePath, fullLink).slice(0, 2) === '..') { 155 | throw new Error('Symlink links outside archive'); 156 | } 157 | 158 | return linkTo; 159 | }); 160 | }; 161 | 162 | var pipePromise = function (input, destination, options) { 163 | var deferred = Q.defer(); 164 | var output = fs.createWriteStream(destination, options); 165 | var errorHandler = function (error) { 166 | deferred.reject(error); 167 | }; 168 | 169 | input.on('error', errorHandler); 170 | output.on('error', errorHandler); 171 | 172 | // For node 0.8 we can't just use the 'finish' event of the pipe 173 | input.on('end', function () { 174 | output.end(function () { 175 | deferred.resolve(); 176 | }); 177 | }); 178 | 179 | input.pipe(output, {end: false}); 180 | 181 | return deferred.promise; 182 | }; 183 | 184 | module.exports = extractors; 185 | -------------------------------------------------------------------------------- /lib/structures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var binary = require('binary'); 3 | 4 | var convertDateTime = function (dosDate, dosTime) { 5 | var year = ((dosDate >> 9) & 0x7F) + 1980; 6 | var month = (dosDate >> 5) & 0x0F; 7 | var day = dosDate & 0x1F; 8 | 9 | var hour = (dosTime >> 11); 10 | var minute = (dosTime >> 5) & 0x3F; 11 | var second = (dosTime & 0x1F) * 2; 12 | 13 | var result = new Date(year, month - 1, day, hour, minute, second, 0); 14 | 15 | return result; 16 | }; 17 | 18 | var convertGeneralPurposeFlags = function (value) { 19 | var bits = []; 20 | 21 | for (var i = 0; i < 16; i++) { 22 | bits[i] = (value >> i) & 1; 23 | } 24 | 25 | return { 26 | encrypted: !!bits[0], 27 | compressionFlag1: !!bits[1], 28 | compressionFlag2: !!bits[2], 29 | useDataDescriptor: !!bits[3], 30 | enhancedDeflating: !!bits[4], 31 | compressedPatched: !!bits[5], 32 | strongEncryption: !!bits[6], 33 | utf8: !!bits[11], 34 | encryptedCD: !!bits[13] 35 | }; 36 | }; 37 | 38 | var parseExternalFileAttributes = function (externalAttributes, platform) { 39 | var types = { 40 | // In theory, any of these could be set. Realistically, though, it will 41 | // be regular, directory or symlink 42 | 1: 'NamedPipe', 43 | 2: 'Character', 44 | 4: 'Directory', 45 | 6: 'Block', 46 | 8: 'File', 47 | 10: 'SymbolicLink', 48 | 12: 'Socket' 49 | }; 50 | 51 | switch (platform) { 52 | 53 | case 3: // Unix 54 | return { 55 | platform: 'Unix', 56 | type: types[(externalAttributes >> 28) & 0x0F] || 'File', // default to File 57 | mode: (externalAttributes >> 16) & 0xFFF 58 | }; 59 | 60 | // case 0: // MSDOS 61 | default: 62 | if (platform !== 0) { 63 | console.warn('Possibly unsupported ZIP platform type, ' + platform); 64 | } 65 | 66 | var attribs = { 67 | A: (externalAttributes >> 5) & 0x01, 68 | D: (externalAttributes >> 4) & 0x01, 69 | V: (externalAttributes >> 3) & 0x01, 70 | S: (externalAttributes >> 2) & 0x01, 71 | H: (externalAttributes >> 1) & 0x01, 72 | R: externalAttributes & 0x01 73 | }; 74 | 75 | // With no better guidance we'll make the default permissions ugo+r 76 | var mode = parseInt('0444', 8); 77 | 78 | if (attribs.D) { 79 | mode |= parseInt('0111', 8); // Set the execute bit 80 | } 81 | 82 | if (!attribs.R) { 83 | mode |= parseInt('0222', 8); // Set the write bit 84 | } 85 | 86 | mode &= ~process.umask(); 87 | 88 | return { 89 | platform: 'DOS', 90 | type: attribs.D ? 'Directory' : 'File', 91 | mode: mode 92 | }; 93 | } 94 | }; 95 | 96 | var readEndRecord = function (buffer) { 97 | var data = binary.parse(buffer) 98 | .word32lu('signature') 99 | .word16lu('diskNumber') 100 | .word16lu('directoryStartDisk') 101 | .word16lu('directoryEntryCountDisk') 102 | .word16lu('directoryEntryCount') 103 | .word32lu('directorySize') 104 | .word32lu('directoryOffset') 105 | .word16lu('commentLength') 106 | .buffer('comment', 'commentLength') 107 | .vars; 108 | 109 | data.comment = data.comment.toString(); 110 | 111 | return data; 112 | }; 113 | 114 | var directorySort = function (a, b) { 115 | return a.relativeOffsetOfLocalHeader - b.relativeOffsetOfLocalHeader; 116 | }; 117 | 118 | var readDirectory = function (buffer) { 119 | var directory = []; 120 | var current; 121 | var index = 0; 122 | 123 | while (index < buffer.length) { 124 | current = binary.parse(buffer.slice(index, index + 46)) 125 | .word32lu('signature') 126 | .word8lu('creatorSpecVersion') 127 | .word8lu('creatorPlatform') 128 | .word8lu('requiredSpecVersion') 129 | .word8lu('requiredPlatform') 130 | .word16lu('generalPurposeBitFlag') 131 | .word16lu('compressionMethod') 132 | .word16lu('lastModFileTime') 133 | .word16lu('lastModFileDate') 134 | .word32lu('crc32') 135 | .word32lu('compressedSize') 136 | .word32lu('uncompressedSize') 137 | .word16lu('fileNameLength') 138 | .word16lu('extraFieldLength') 139 | .word16lu('fileCommentLength') 140 | .word16lu('diskNumberStart') 141 | .word16lu('internalFileAttributes') 142 | .word32lu('externalFileAttributes') 143 | .word32lu('relativeOffsetOfLocalHeader') 144 | .vars; 145 | 146 | index += 46; 147 | 148 | current.generalPurposeFlags = convertGeneralPurposeFlags(current.generalPurposeBitFlag); 149 | current.fileAttributes = parseExternalFileAttributes(current.externalFileAttributes, current.creatorPlatform); 150 | 151 | current.modifiedTime = convertDateTime(current.lastModFileDate, current.lastModFileTime); 152 | current.fileName = current.extraField = current.fileComment = ''; 153 | current.headerLength = 46 + current.fileNameLength + current.extraFieldLength + current.fileCommentLength; 154 | 155 | if (current.fileNameLength > 0) { 156 | current.fileName = buffer.slice(index, index + current.fileNameLength).toString(); 157 | index += current.fileNameLength; 158 | } 159 | 160 | if (current.extraFieldLength > 0) { 161 | current.extraField = buffer.slice(index, index + current.extraFieldLength).toString(); 162 | index += current.extraFieldLength; 163 | } 164 | 165 | if (current.fileCommentLength > 0) { 166 | current.fileComment = buffer.slice(index, index + current.fileCommentLength).toString(); 167 | index += current.fileCommentLength; 168 | } 169 | 170 | if (current.fileAttributes.type !== 'Directory' && current.fileName.substr(-1) === '/') { 171 | // TODO: check that this is a reasonable check 172 | current.fileAttributes.type = 'Directory'; 173 | } 174 | 175 | directory.push(current); 176 | } 177 | 178 | directory.sort(directorySort); 179 | 180 | return directory; 181 | }; 182 | 183 | var readFileEntry = function (buffer) { 184 | var index = 0; 185 | 186 | var fileEntry = binary.parse(buffer.slice(index, 30)) 187 | .word32lu('signature') 188 | .word16lu('versionNeededToExtract') 189 | .word16lu('generalPurposeBitFlag') 190 | .word16lu('compressionMethod') 191 | .word16lu('lastModFileTime') 192 | .word16lu('lastModFileDate') 193 | .word32lu('crc32') 194 | .word32lu('compressedSize') 195 | .word32lu('uncompressedSize') 196 | .word16lu('fileNameLength') 197 | .word16lu('extraFieldLength') 198 | .vars; 199 | 200 | index += 30; 201 | 202 | fileEntry.fileName = fileEntry.extraField = ''; 203 | 204 | fileEntry.entryLength = 30 + fileEntry.fileNameLength + fileEntry.extraFieldLength; 205 | 206 | if (fileEntry.entryLength > structures.maxFileEntrySize) { 207 | throw new Error('File entry unexpectedly large: ' + fileEntry.entryLength + ' (max: ' + structures.maxFileEntrySize + ')'); 208 | } 209 | 210 | if (fileEntry.fileNameLength > 0) { 211 | fileEntry.fileName = buffer.slice(index, index + fileEntry.fileNameLength).toString(); 212 | index += fileEntry.fileNameLength; 213 | } 214 | 215 | if (fileEntry.extraFieldLength > 0) { 216 | fileEntry.extraField = buffer.slice(index, index + fileEntry.extraFieldLength).toString(); 217 | index += fileEntry.extraFieldLength; 218 | } 219 | 220 | return fileEntry; 221 | }; 222 | 223 | var structures = module.exports = { 224 | readEndRecord: readEndRecord, 225 | readDirectory: readDirectory, 226 | readFileEntry: readFileEntry, 227 | maxFileEntrySize: 4096 228 | }; 229 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var jetpack = require('fs-jetpack'); 5 | var tmp = require('tmp'); 6 | var DecompressZip = require('../lib/decompress-zip'); 7 | 8 | var assetsDir = jetpack.cwd(__dirname, 'assets'); 9 | 10 | var samples = []; 11 | 12 | // ------------------------------------------ 13 | // main-test-pack 14 | // Most common stuff you may want to extract. 15 | 16 | // Default deflate algorithm 17 | samples.push({ 18 | file: 'main-test-pack/deflate.zip', 19 | treeInspect: 'main-test-pack/spec.json', 20 | inspectOptions: {} 21 | }); 22 | // "Store" (no compression, just merge stuff together) 23 | samples.push({ 24 | file: 'main-test-pack/store.zip', 25 | treeInspect: 'main-test-pack/spec.json', 26 | inspectOptions: {} 27 | }); 28 | 29 | // ------------------------------------------ 30 | // file-mode-pack 31 | // Test if files preserve theirs unix file mode when extracted. 32 | // This test doesn't make sense on Windows platform, so exlude it there. 33 | if (process.platform !== 'win32') { 34 | // Default deflate algorithm 35 | samples.push({ 36 | file: 'file-mode-pack/deflate.zip', 37 | treeInspect: 'file-mode-pack/spec.json', 38 | inspectOptions: { mode: true } 39 | }); 40 | // "Store" (no compression, just merge stuff together) 41 | samples.push({ 42 | file: 'file-mode-pack/store.zip', 43 | treeInspect: 'file-mode-pack/spec.json', 44 | inspectOptions: { mode: true } 45 | }); 46 | } 47 | 48 | describe('Smoke test', function () { 49 | it('should find the public interface', function () { 50 | assert.isFunction(DecompressZip, 'constructor is a function'); 51 | assert.isFunction(DecompressZip.prototype.list, 'decompress.list is a function'); 52 | assert.isFunction(DecompressZip.prototype.extract, 'decompress.extract is a function'); 53 | }); 54 | }); 55 | 56 | describe('Extract', function () { 57 | describe('errors', function () { 58 | var tmpDir; 59 | 60 | beforeEach(function (done) { 61 | tmp.dir({unsafeCleanup: true}, function (err, dir) { 62 | if (err) { 63 | throw err; 64 | } 65 | 66 | tmpDir = jetpack.cwd(dir, 'extracted'); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('should emit an error when the file does not exist', function (done) { 72 | var zip = new DecompressZip('/my/non/existant/file.zip'); 73 | 74 | zip.on('extract', function () { 75 | assert(false, '"extract" event should not fire'); 76 | done(); 77 | }); 78 | 79 | zip.on('error', function (error) { 80 | assert(true, '"error" event should fire'); 81 | done(); 82 | }); 83 | 84 | zip.extract({path: tmpDir.path()}); 85 | }); 86 | 87 | it('should emit an error when stripping deeper than the path structure', function (done) { 88 | var zip = new DecompressZip(assetsDir.path(samples[0].file)); 89 | 90 | zip.on('extract', function () { 91 | assert(false, '"extract" event should not fire'); 92 | done(); 93 | }); 94 | 95 | zip.on('error', function (error) { 96 | assert(true, '"error" event should fire'); 97 | done(); 98 | }); 99 | 100 | zip.extract({path: tmpDir.path(), strip: 3}); 101 | }); 102 | 103 | it('should emit a progress event on each file', function (done) { 104 | var zip = new DecompressZip(assetsDir.path(samples[0].file)); 105 | var numProgressEvents = 0; 106 | var numTotalFiles = 6; 107 | 108 | zip.on('progress', function (i, numFiles) { 109 | assert.equal(numFiles, numTotalFiles, '"progress" event should include the correct number of files'); 110 | assert(typeof i === 'number', '"progress" event should include the number of the current file'); 111 | numProgressEvents++; 112 | }); 113 | 114 | zip.on('extract', function () { 115 | assert(true, '"extract" event should fire'); 116 | assert.equal(numProgressEvents, numTotalFiles, 'there should be a "progress" event for every file'); 117 | done(); 118 | }); 119 | 120 | zip.on('error', done); 121 | 122 | zip.extract({path: tmpDir.path()}); 123 | }); 124 | it('should emit an error when a file attempts to escape the current working directory', function (done) { 125 | var zip = new DecompressZip(assetsDir + 'restrict-pack/escape.zip'); 126 | zip.on('extract', function () { 127 | assert(false, '"extract" event should not fire'); 128 | done(); 129 | }); 130 | 131 | zip.on('error', function (error) { 132 | assert(true, '"error" event should fire'); 133 | done(); 134 | }); 135 | 136 | zip.extract({path: tmpDir.path(), strip: 3}); 137 | }); 138 | }); 139 | 140 | describe('directory creation', function () { 141 | var tmpDir; 142 | var rmdirSync; 143 | before(function (done) { 144 | tmp.dir({unsafeCleanup: true}, function (err, dir, cleanupCallback) { 145 | if (err) { 146 | throw err; 147 | } 148 | 149 | tmpDir = jetpack.cwd(dir, 'extracted'); 150 | rmdirSync = cleanupCallback; 151 | done(); 152 | }); 153 | }); 154 | 155 | it('should create necessary directories, even on 2nd run', function (done) { 156 | var zip = new DecompressZip(assetsDir.path(samples[0].file)); 157 | zip.on('error', done); 158 | zip.on('extract', function () { 159 | rmdirSync(tmpDir.path()); 160 | var zip2 = new DecompressZip(assetsDir.path(samples[0].file)); 161 | zip2.on('error', done); 162 | zip2.on('extract', function () { 163 | done(); 164 | }); 165 | zip2.extract({path: tmpDir.path()}); 166 | }); 167 | 168 | zip.extract({path: tmpDir.path()}); 169 | }); 170 | }); 171 | 172 | samples.forEach(function (sample) { 173 | describe(sample.file, function () { 174 | var tmpDir; 175 | 176 | before(function (done) { 177 | tmp.dir({unsafeCleanup: true}, function (err, dir) { 178 | if (err) { 179 | throw err; 180 | } 181 | 182 | tmpDir = jetpack.dir(dir + '/extracted', { mode: '755' }); 183 | done(); 184 | }); 185 | }); 186 | 187 | it('should extract without any errors', function (done) { 188 | var zip = new DecompressZip(assetsDir.path(sample.file)); 189 | 190 | zip.on('extract', function () { 191 | assert(true, 'success callback should be called'); 192 | done(); 193 | }); 194 | 195 | zip.on('error', function () { 196 | assert(false, 'error callback should not be called'); 197 | done(); 198 | }); 199 | 200 | zip.extract({path: tmpDir.path()}); 201 | }); 202 | 203 | it('extracted files should match the spec', function (done) { 204 | var options = sample.inspectOptions; 205 | options.checksum = 'sha1'; 206 | 207 | tmpDir.inspectTreeAsync('.', options) 208 | .then(function (inspect) { 209 | var validInspect = assetsDir.read(sample.treeInspect, 'json'); 210 | assert.deepEqual(inspect, validInspect, 'extracted files are matching the spec'); 211 | done(); 212 | }) 213 | .catch(done); 214 | }); 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /lib/decompress-zip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // The zip file spec is at http://www.pkware.com/documents/casestudies/APPNOTE.TXT 4 | // TODO: There is fair chunk of the spec that I have ignored. Need to add 5 | // assertions everywhere to make sure that we are not dealing with a ZIP type 6 | // that I haven't designed for. Things like spanning archives, non-DEFLATE 7 | // compression, encryption, etc. 8 | var fs = require('graceful-fs'); 9 | var Q = require('q'); 10 | var path = require('path'); 11 | var util = require('util'); 12 | var events = require('events'); 13 | var structures = require('./structures'); 14 | var signatures = require('./signatures'); 15 | var extractors = require('./extractors'); 16 | var FileDetails = require('./file-details'); 17 | 18 | var fstat = Q.denodeify(fs.fstat); 19 | var read = Q.denodeify(fs.read); 20 | var fopen = Q.denodeify(fs.open); 21 | 22 | function DecompressZip(filename) { 23 | events.EventEmitter.call(this); 24 | 25 | this.filename = filename; 26 | this.stats = null; 27 | this.fd = null; 28 | this.chunkSize = 1024 * 1024; // Buffer up to 1Mb at a time 29 | this.dirCache = {}; 30 | 31 | // When we need a resource, we should check if there is a promise for it 32 | // already and use that. If the promise is already fulfilled we don't do the 33 | // async work again and we get to queue up dependant tasks. 34 | this._p = {}; // _p instead of _promises because it is a lot easier to read 35 | } 36 | 37 | util.inherits(DecompressZip, events.EventEmitter); 38 | 39 | DecompressZip.prototype.openFile = function () { 40 | return fopen(this.filename, 'r'); 41 | }; 42 | 43 | DecompressZip.prototype.closeFile = function () { 44 | if (this.fd) { 45 | fs.closeSync(this.fd); 46 | this.fd = null; 47 | } 48 | }; 49 | 50 | DecompressZip.prototype.statFile = function (fd) { 51 | this.fd = fd; 52 | return fstat(fd); 53 | }; 54 | 55 | DecompressZip.prototype.list = function () { 56 | var self = this; 57 | 58 | this.getFiles() 59 | .then(function (files) { 60 | var result = []; 61 | 62 | files.forEach(function (file) { 63 | result.push(file.path); 64 | }); 65 | 66 | self.emit('list', result); 67 | }) 68 | .fail(function (error) { 69 | self.emit('error', error); 70 | }) 71 | .fin(self.closeFile.bind(self)); 72 | 73 | return this; 74 | }; 75 | 76 | DecompressZip.prototype.extract = function (options) { 77 | var self = this; 78 | 79 | options = options || {}; 80 | options.path = options.path || process.cwd(); 81 | options.filter = options.filter || null; 82 | options.follow = !!options.follow; 83 | options.strip = +options.strip || 0; 84 | options.restrict = options.restrict !== false; 85 | 86 | 87 | this.getFiles() 88 | .then(function (files) { 89 | var copies = []; 90 | if (options.restrict) { 91 | files = files.map(function (file) { 92 | var destination = path.join(options.path, file.path); 93 | // The destination path must not be outside options.path 94 | if (destination.indexOf(options.path) !== 0) { 95 | throw new Error('You cannot extract a file outside of the target path'); 96 | } 97 | return file; 98 | }); 99 | } 100 | if (options.filter) { 101 | files = files.filter(options.filter); 102 | } 103 | 104 | if (options.follow) { 105 | copies = files.filter(function (file) { 106 | return file.type === 'SymbolicLink'; 107 | }); 108 | files = files.filter(function (file) { 109 | return file.type !== 'SymbolicLink'; 110 | }); 111 | } 112 | 113 | if (options.strip) { 114 | files = files.map(function (file) { 115 | if (file.type !== 'Directory') { 116 | // we don't use `path.sep` as we're using `/` in Windows too 117 | var dir = file.parent.split('/'); 118 | var filename = file.filename; 119 | 120 | if (options.strip > dir.length) { 121 | throw new Error('You cannot strip more levels than there are directories'); 122 | } else { 123 | dir = dir.slice(options.strip); 124 | } 125 | 126 | file.path = path.join(dir.join(path.sep), filename); 127 | return file; 128 | } 129 | }); 130 | } 131 | 132 | return self.extractFiles(files, options) 133 | .then(self.extractFiles.bind(self, copies, options)); 134 | }) 135 | .then(function (results) { 136 | self.emit('extract', results); 137 | }) 138 | .fail(function (error) { 139 | self.emit('error', error); 140 | }) 141 | .fin(self.closeFile.bind(self)); 142 | 143 | return this; 144 | }; 145 | 146 | // Utility methods 147 | DecompressZip.prototype.getSearchBuffer = function (stats) { 148 | var size = Math.min(stats.size, this.chunkSize); 149 | this.stats = stats; 150 | return this.getBuffer(stats.size - size, stats.size); 151 | }; 152 | 153 | DecompressZip.prototype.getBuffer = function (start, end) { 154 | var size = end - start; 155 | return read(this.fd, new Buffer(size), 0, size, start) 156 | .then(function (result) { 157 | return result[1]; 158 | }); 159 | }; 160 | 161 | DecompressZip.prototype.findEndOfDirectory = function (buffer) { 162 | var index = buffer.length - 3; 163 | var chunk = ''; 164 | 165 | // Apparently the ZIP spec is not very good and it is impossible to 166 | // guarantee that you have read a zip file correctly, or to determine 167 | // the location of the CD without hunting. 168 | // Search backwards through the buffer, as it is very likely to be near the 169 | // end of the file. 170 | while (index > Math.max(buffer.length - this.chunkSize, 0) && chunk !== signatures.END_OF_CENTRAL_DIRECTORY) { 171 | index--; 172 | chunk = buffer.readUInt32LE(index); 173 | } 174 | 175 | if (chunk !== signatures.END_OF_CENTRAL_DIRECTORY) { 176 | throw new Error('Could not find the End of Central Directory Record'); 177 | } 178 | 179 | return buffer.slice(index); 180 | }; 181 | 182 | // Directory here means the ZIP Central Directory, not a folder 183 | DecompressZip.prototype.readDirectory = function (recordBuffer) { 184 | var record = structures.readEndRecord(recordBuffer); 185 | 186 | return this.getBuffer(record.directoryOffset, record.directoryOffset + record.directorySize) 187 | .then(structures.readDirectory.bind(null)); 188 | }; 189 | 190 | DecompressZip.prototype.getFiles = function () { 191 | if (!this._p.getFiles) { 192 | this._p.getFiles = this.openFile() 193 | .then(this.statFile.bind(this)) 194 | .then(this.getSearchBuffer.bind(this)) 195 | .then(this.findEndOfDirectory.bind(this)) 196 | .then(this.readDirectory.bind(this)) 197 | .then(this.readFileEntries.bind(this)); 198 | } 199 | 200 | return this._p.getFiles; 201 | }; 202 | 203 | DecompressZip.prototype.readFileEntries = function (directory) { 204 | var promises = []; 205 | var files = []; 206 | var self = this; 207 | 208 | directory.forEach(function (directoryEntry, index) { 209 | var start = directoryEntry.relativeOffsetOfLocalHeader; 210 | var end = Math.min(self.stats.size, start + structures.maxFileEntrySize); 211 | var fileDetails = new FileDetails(directoryEntry); 212 | 213 | var promise = self.getBuffer(start, end) 214 | .then(structures.readFileEntry.bind(null)) 215 | .then(function (fileEntry) { 216 | var maxSize; 217 | 218 | if (fileDetails.compressedSize > 0) { 219 | maxSize = fileDetails.compressedSize; 220 | } else { 221 | maxSize = self.stats.size; 222 | 223 | if (index < directory.length - 1) { 224 | maxSize = directory[index + 1].relativeOffsetOfLocalHeader; 225 | } 226 | 227 | maxSize -= start + fileEntry.entryLength; 228 | } 229 | 230 | fileDetails._offset = start + fileEntry.entryLength; 231 | fileDetails._maxSize = maxSize; 232 | 233 | self.emit('file', fileDetails); 234 | files[index] = fileDetails; 235 | }); 236 | 237 | promises.push(promise); 238 | }); 239 | 240 | return Q.all(promises) 241 | .then(function () { 242 | return files; 243 | }); 244 | }; 245 | 246 | DecompressZip.prototype.extractFiles = function (files, options, results) { 247 | var promises = []; 248 | var self = this; 249 | 250 | results = results || []; 251 | var fileIndex = 0; 252 | files.forEach(function (file) { 253 | var promise = self.extractFile(file, options) 254 | .then(function (result) { 255 | self.emit('progress', fileIndex++, files.length); 256 | results.push(result); 257 | }); 258 | 259 | promises.push(promise); 260 | }); 261 | 262 | return Q.all(promises) 263 | .then(function () { 264 | return results; 265 | }); 266 | }; 267 | 268 | DecompressZip.prototype.extractFile = function (file, options) { 269 | var destination = path.join(options.path, file.path); 270 | 271 | // Possible compression methods: 272 | // 0 - The file is stored (no compression) 273 | // 1 - The file is Shrunk 274 | // 2 - The file is Reduced with compression factor 1 275 | // 3 - The file is Reduced with compression factor 2 276 | // 4 - The file is Reduced with compression factor 3 277 | // 5 - The file is Reduced with compression factor 4 278 | // 6 - The file is Imploded 279 | // 7 - Reserved for Tokenizing compression algorithm 280 | // 8 - The file is Deflated 281 | // 9 - Enhanced Deflating using Deflate64(tm) 282 | // 10 - PKWARE Data Compression Library Imploding (old IBM TERSE) 283 | // 11 - Reserved by PKWARE 284 | // 12 - File is compressed using BZIP2 algorithm 285 | // 13 - Reserved by PKWARE 286 | // 14 - LZMA (EFS) 287 | // 15 - Reserved by PKWARE 288 | // 16 - Reserved by PKWARE 289 | // 17 - Reserved by PKWARE 290 | // 18 - File is compressed using IBM TERSE (new) 291 | // 19 - IBM LZ77 z Architecture (PFS) 292 | // 97 - WavPack compressed data 293 | // 98 - PPMd version I, Rev 1 294 | 295 | if (file.type === 'Directory') { 296 | return extractors.folder(file, destination, this); 297 | } 298 | 299 | if (file.type === 'File') { 300 | switch (file.compressionMethod) { 301 | case 0: 302 | return extractors.store(file, destination, this); 303 | 304 | case 8: 305 | return extractors.deflate(file, destination, this); 306 | 307 | default: 308 | throw new Error('Unsupported compression type'); 309 | } 310 | } 311 | 312 | if (file.type === 'SymbolicLink') { 313 | if (options.follow) { 314 | return extractors.copy(file, destination, this, options.path); 315 | } else { 316 | return extractors.symlink(file, destination, this, options.path); 317 | } 318 | } 319 | 320 | throw new Error('Unsupported file type "' + file.type + '"'); 321 | }; 322 | 323 | module.exports = DecompressZip; 324 | --------------------------------------------------------------------------------