diff --git a/index.js b/index.js index 915f2ca..a2669e8 100644 --- a/index.js +++ b/index.js @@ -1,215 +1,175 @@ -const fs = require('fs') +const debug = require('debug')('extract-zip') +const { createWriteStream, promises: fs } = require('fs') +const getStream = require('get-stream') const path = require('path') +const { promisify } = require('util') +const stream = require('stream') const yauzl = require('yauzl') -const concat = require('concat-stream') -const debug = require('debug')('extract-zip') -module.exports = function (zipPath, opts, cb) { - debug('creating target directory', opts.dir) +const openZip = promisify(yauzl.open) +const pipeline = promisify(stream.pipeline) - if (path.isAbsolute(opts.dir) === false) { - return cb(new Error('Target directory is expected to be absolute')) +class Extractor { + constructor (zipPath, opts) { + this.zipPath = zipPath + this.opts = opts } - fs.mkdir(opts.dir, { recursive: true }, function (err) { - if (err) return cb(err) - - fs.realpath(opts.dir, function (err, canonicalDir) { - if (err) return cb(err) - - opts.dir = canonicalDir - - openZip(opts) - }) - }) - - function openZip () { - debug('opening', zipPath, 'with opts', opts) + async extract () { + debug('opening', this.zipPath, 'with opts', this.opts) - yauzl.open(zipPath, { lazyEntries: true }, function (err, zipfile) { - if (err) return cb(err) + this.zipfile = await openZip(this.zipPath, { lazyEntries: true }) + this.canceled = false - let cancelled = false - - zipfile.on('error', function (err) { - if (err) { - cancelled = true - return cb(err) - } + return new Promise((resolve, reject) => { + this.zipfile.on('error', err => { + this.canceled = true + reject(err) }) - zipfile.readEntry() + this.zipfile.readEntry() - zipfile.on('close', function () { - if (!cancelled) { + this.zipfile.on('close', () => { + if (!this.canceled) { debug('zip extraction complete') - cb() + resolve() } }) - zipfile.on('entry', function (entry) { + this.zipfile.on('entry', async entry => { /* istanbul ignore if */ - if (cancelled) { - debug('skipping entry', entry.fileName, { cancelled: cancelled }) + if (this.canceled) { + debug('skipping entry', entry.fileName, { cancelled: this.canceled }) return } debug('zipfile entry', entry.fileName) if (entry.fileName.startsWith('__MACOSX/')) { - zipfile.readEntry() + this.zipfile.readEntry() return } - const destDir = path.dirname(path.join(opts.dir, entry.fileName)) + const destDir = path.dirname(path.join(this.opts.dir, entry.fileName)) - fs.mkdir(destDir, { recursive: true }, function (err) { - /* istanbul ignore if */ - if (err) { - cancelled = true - zipfile.close() - return cb(err) - } + try { + await fs.mkdir(destDir, { recursive: true }) - fs.realpath(destDir, function (err, canonicalDestDir) { - /* istanbul ignore if */ - if (err) { - cancelled = true - zipfile.close() - return cb(err) - } - - const relativeDestDir = path.relative(opts.dir, canonicalDestDir) - - if (relativeDestDir.split(path.sep).indexOf('..') !== -1) { - cancelled = true - zipfile.close() - return cb(new Error('Out of bound path "' + canonicalDestDir + '" found while processing file ' + entry.fileName)) - } - - extractEntry(entry, function (err) { - // if any extraction fails then abort everything - if (err) { - cancelled = true - zipfile.close() - return cb(err) - } - debug('finished processing', entry.fileName) - zipfile.readEntry() - }) - }) - }) - }) + const canonicalDestDir = await fs.realpath(destDir) + const relativeDestDir = path.relative(this.opts.dir, canonicalDestDir) - function extractEntry (entry, done) { - /* istanbul ignore if */ - if (cancelled) { - debug('skipping entry extraction', entry.fileName, { cancelled: cancelled }) - return setImmediate(done) - } + if (relativeDestDir.split(path.sep).includes('..')) { + throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`) + } - if (opts.onEntry) { - opts.onEntry(entry, zipfile) + await this.extractEntry(entry) + debug('finished processing', entry.fileName) + this.zipfile.readEntry() + } catch (err) { + this.canceled = true + this.zipfile.close() + reject(err) } + }) + }) + } - const dest = path.join(opts.dir, entry.fileName) - - // convert external file attr int into a fs stat mode int - let mode = (entry.externalFileAttributes >> 16) & 0xFFFF - // check if it's a symlink or dir (using stat mode constants) - const IFMT = 61440 - const IFDIR = 16384 - const IFLNK = 40960 - const symlink = (mode & IFMT) === IFLNK - let isDir = (mode & IFMT) === IFDIR + async extractEntry (entry) { + /* istanbul ignore if */ + if (this.canceled) { + debug('skipping entry extraction', entry.fileName, { cancelled: this.canceled }) + return + } + + if (this.opts.onEntry) { + this.opts.onEntry(entry, this.zipfile) + } + + const dest = path.join(this.opts.dir, entry.fileName) + + // convert external file attr int into a fs stat mode int + const mode = (entry.externalFileAttributes >> 16) & 0xFFFF + // check if it's a symlink or dir (using stat mode constants) + const IFMT = 61440 + const IFDIR = 16384 + const IFLNK = 40960 + const symlink = (mode & IFMT) === IFLNK + let isDir = (mode & IFMT) === IFDIR + + // Failsafe, borrowed from jsZip + if (!isDir && entry.fileName.endsWith('/')) { + isDir = true + } + + // check for windows weird way of specifying a directory + // /~https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566 + const madeBy = entry.versionMadeBy >> 8 + if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16) + + debug('extracting entry', { filename: entry.fileName, isDir: isDir, isSymlink: symlink }) + + // reverse umask first (~) + const umask = ~process.umask() + // & with processes umask to override invalid perms + const procMode = this.getExtractedMode(mode, isDir) & umask + + // always ensure folders are created + const destDir = isDir ? dest : path.dirname(dest) + + const mkdirOptions = { recursive: true } + if (isDir) { + mkdirOptions.mode = procMode + } + debug('mkdir', { dir: destDir, ...mkdirOptions }) + await fs.mkdir(destDir, mkdirOptions) + if (isDir) return + + debug('opening read stream', dest) + const readStream = await promisify(this.zipfile.openReadStream.bind(this.zipfile))(entry) + + if (symlink) { + const link = await getStream(readStream) + debug('creating symlink', link, dest) + await fs.symlink(link, dest) + } else { + await pipeline(readStream, createWriteStream(dest, { mode: procMode })) + } + } - // Failsafe, borrowed from jsZip - if (!isDir && entry.fileName.slice(-1) === '/') { - isDir = true + getExtractedMode (entryMode, isDir) { + let mode = entryMode + // Set defaults, if necessary + if (mode === 0) { + if (isDir) { + if (this.opts.defaultDirMode) { + mode = parseInt(this.opts.defaultDirMode, 10) } - // check for windows weird way of specifying a directory - // /~https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566 - const madeBy = entry.versionMadeBy >> 8 - if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16) - - // if no mode then default to default modes - if (mode === 0) { - if (isDir) { - if (opts.defaultDirMode) mode = parseInt(opts.defaultDirMode, 10) - if (!mode) mode = 0o755 - } else { - if (opts.defaultFileMode) mode = parseInt(opts.defaultFileMode, 10) - if (!mode) mode = 0o644 - } + if (!mode) { + mode = 0o755 + } + } else { + if (this.opts.defaultFileMode) { + mode = parseInt(this.opts.defaultFileMode, 10) } - debug('extracting entry', { filename: entry.fileName, isDir: isDir, isSymlink: symlink }) - - // reverse umask first (~) - const umask = ~process.umask() - // & with processes umask to override invalid perms - const procMode = mode & umask + if (!mode) { + mode = 0o644 + } + } + } - // always ensure folders are created - const destDir = isDir ? dest : path.dirname(dest) + return mode + } +} - debug('mkdirp', { dir: destDir }) - fs.mkdir(destDir, { recursive: true }, function (err) { - /* istanbul ignore if */ - if (err) { - debug('mkdirp error', destDir, { error: err }) - cancelled = true - return done(err) - } +module.exports = async function (zipPath, opts) { + debug('creating target directory', opts.dir) - if (isDir) return done() - - debug('opening read stream', dest) - zipfile.openReadStream(entry, function (err, readStream) { - /* istanbul ignore if */ - if (err) { - debug('openReadStream error', err) - cancelled = true - return done(err) - } - - readStream.on('error', function (err) { - /* istanbul ignore next */ - console.log('read err', err) - }) - - if (symlink) writeSymlink() - else writeStream() - - function writeStream () { - const writeStream = fs.createWriteStream(dest, { mode: procMode }) - readStream.pipe(writeStream) - - writeStream.on('finish', function () { - done() - }) - - writeStream.on('error', /* istanbul ignore next */ function (err) { - debug('write error', { error: err }) - cancelled = true - return done(err) - }) - } - - // AFAICT the content of the symlink file itself is the symlink target filename string - function writeSymlink () { - readStream.pipe(concat(function (data) { - const link = data.toString() - debug('creating symlink', link, dest) - fs.symlink(link, dest, function (err) { - if (err) cancelled = true - done(err) - }) - })) - } - }) - }) - } - }) + if (!path.isAbsolute(opts.dir)) { + throw new Error('Target directory is expected to be absolute') } + + await fs.mkdir(opts.dir, { recursive: true }) + opts.dir = await fs.realpath(opts.dir) + return new Extractor(zipPath, opts).extract() } diff --git a/package.json b/package.json index 935410c..09d9bf0 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "extract-zip": "cli.js" }, "scripts": { - "coverage": "nyc node test/test.js", + "ava": "ava", + "coverage": "nyc ava", "lint": "standard", - "test": "node test/test.js" + "test": "npm run lint && ava" }, "files": [ "*.js" @@ -26,15 +27,15 @@ "node": ">= 10.12.0" }, "dependencies": { - "concat-stream": "^2.0.0", "debug": "^4.1.1", + "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "devDependencies": { + "ava": "^3.5.1", + "fs-extra": "^9.0.0", "nyc": "^15.0.0", - "rimraf": "^3.0.2", - "standard": "^14.3.3", - "tape": "^4.2.0" + "standard": "^14.3.3" }, "directories": { "test": "test" diff --git a/readme.md b/readme.md index 37f0e04..acc0375 100644 --- a/readme.md +++ b/readme.md @@ -26,18 +26,24 @@ npm install extract-zip -g ## JS API -```js -var extract = require('extract-zip') -extract(source, {dir: target}, function (err) { - // extraction is complete. make sure to handle the err -}) +```javascript +const extract = require('extract-zip') + +async main () { + try { + await extract(source, { dir: target }) + console.log('Extraction complete') + } catch (err) { + // handle any errors + } +} ``` ### Options - `dir` (required) - the path to the directory where the extracted files are written -- `defaultDirMode` - integer - Directory Mode (permissions) will default to `493` (octal `0755` in integer) -- `defaultFileMode` - integer - File Mode (permissions) will default to `420` (octal `0644` in integer) +- `defaultDirMode` - integer - Directory Mode (permissions), defaults to `0o755` +- `defaultFileMode` - integer - File Mode (permissions), defaults to `0o644` - `onEntry` - function - if present, will be called with `(entry, zipfile)`, entry is every entry from the zip file forwarded from the `entry` event from yauzl. `zipfile` is the `yauzl` instance Default modes are only used if no permissions are set in the zip file. diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..a955a21 --- /dev/null +++ b/test/index.js @@ -0,0 +1,163 @@ +const extract = require('../') +const fs = require('fs-extra') +const os = require('os') +const path = require('path') +const test = require('ava') + +const catsZip = path.join(__dirname, 'cats.zip') +const githubZip = path.join(__dirname, 'github.zip') +const noPermissionsZip = path.join(__dirname, 'no-permissions.zip') +const subdirZip = path.join(__dirname, 'file-in-subdir-without-subdir-entry.zip') +const symlinkDestZip = path.join(__dirname, 'symlink-dest.zip') +const symlinkZip = path.join(__dirname, 'symlink.zip') +const brokenZip = path.join(__dirname, 'broken.zip') + +const relativeTarget = './cats' + +async function mkdtemp (t, suffix) { + return fs.mkdtemp(path.join(os.tmpdir(), `extract-zip-${suffix}`)) +} + +async function tempExtract (t, suffix, zipPath) { + const dirPath = await mkdtemp(t, suffix) + await extract(zipPath, { dir: dirPath }) + return dirPath +} + +async function pathExists (t, pathToCheck, message) { + const exists = await fs.pathExists(pathToCheck) + t.true(exists, message) +} + +async function pathDoesntExist (t, pathToCheck, message) { + const exists = await fs.pathExists(pathToCheck) + t.false(exists, message) +} + +async function assertPermissions (t, pathToCheck, expectedMode) { + const stats = await fs.stat(pathToCheck) + const actualMode = (stats.mode & 0o777) + t.is(actualMode, expectedMode) +} + +test('files', async t => { + const dirPath = await tempExtract(t, 'files', catsZip) + await pathExists(t, path.join(dirPath, 'cats', 'gJqEYBs.jpg'), 'file created') +}) + +test('symlinks', async t => { + const dirPath = await tempExtract(t, 'symlinks', catsZip) + const symlink = path.join(dirPath, 'cats', 'orange_symlink') + + await pathExists(t, path.join(dirPath, 'cats'), 'directory created') + await pathExists(t, symlink, `symlink created: ${symlink}`) + + const stats = await fs.lstat(symlink) + t.truthy(stats.isSymbolicLink(), 'symlink is valid') + const linkPath = await fs.readlink(symlink) + t.is(linkPath, 'orange') +}) + +test('directories', async t => { + const dirPath = await tempExtract(t, 'directories', catsZip) + const dirWithContent = path.join(dirPath, 'cats', 'orange') + const dirWithoutContent = path.join(dirPath, 'cats', 'empty') + + await pathExists(t, dirWithContent, 'directory created') + + const filesWithContent = await fs.readdir(dirWithContent) + t.not(filesWithContent.length, 0, 'directory has files') + + await pathExists(t, dirWithoutContent, 'empty directory created') + + const filesWithoutContent = await fs.readdir(dirWithoutContent) + t.is(filesWithoutContent.length, 0, 'empty directory has no files') +}) + +test('verify github zip extraction worked', async t => { + const dirPath = await tempExtract(t, 'verify-extraction', githubZip) + await pathExists(t, path.join(dirPath, 'extract-zip-master', 'test'), 'folder created') + if (process.platform !== 'win32') { + await assertPermissions(t, path.join(dirPath, 'extract-zip-master', 'test'), 0o755) + } +}) + +test('opts.onEntry', async t => { + const dirPath = await mkdtemp(t, 'onEntry') + const actualEntries = [] + const expectedEntries = [ + 'symlink/', + 'symlink/foo.txt', + 'symlink/foo_symlink.txt' + ] + const onEntry = function (entry) { + actualEntries.push(entry.fileName) + } + await extract(symlinkZip, { dir: dirPath, onEntry }) + t.deepEqual(actualEntries, expectedEntries, 'entries should match') +}) + +test('relative target directory', async t => { + await fs.remove(relativeTarget) + await t.throwsAsync(extract(catsZip, { dir: relativeTarget }), { + message: 'Target directory is expected to be absolute' + }) + await pathDoesntExist(t, path.join(__dirname, relativeTarget), 'folder not created') + await fs.remove(relativeTarget) +}) + +if (process.platform !== 'win32') { + test('symlink destination disallowed', async t => { + const dirPath = await mkdtemp(t, 'symlink-destination-disallowed') + await pathDoesntExist(t, path.join(dirPath, 'file.txt'), "file doesn't exist at symlink target") + + await t.throwsAsync(extract(symlinkDestZip, { dir: dirPath }), { + message: /Out of bound path ".*?" found while processing file symlink-dest\/aaa\/file.txt/ + }) + }) + + test('no file created out of bound', async t => { + const dirPath = await mkdtemp(t, 'out-of-bounds-file') + await t.throwsAsync(extract(symlinkDestZip, { dir: dirPath })) + + const symlinkDestDir = path.join(dirPath, 'symlink-dest') + + await pathExists(t, symlinkDestDir, 'target folder created') + await pathExists(t, path.join(symlinkDestDir, 'aaa'), 'symlink created') + await pathExists(t, path.join(symlinkDestDir, 'ccc'), 'parent folder created') + await pathDoesntExist(t, path.join(symlinkDestDir, 'ccc/file.txt'), 'file not created in original folder') + await pathDoesntExist(t, path.join(dirPath, 'file.txt'), 'file not created in symlink target') + }) + + test('defaultDirMode', async t => { + const dirPath = await mkdtemp(t, 'default-dir-mode') + const defaultDirMode = 0o700 + await extract(githubZip, { dir: dirPath, defaultDirMode }) + await assertPermissions(t, path.join(dirPath, 'extract-zip-master', 'test'), defaultDirMode) + }) + + test('defaultFileMode not set', async t => { + const dirPath = await mkdtemp(t, 'default-file-mode') + await extract(noPermissionsZip, { dir: dirPath }) + await assertPermissions(t, path.join(dirPath, 'folder', 'file.txt'), 0o644) + }) + + test('defaultFileMode', async t => { + const dirPath = await mkdtemp(t, 'default-file-mode') + const defaultFileMode = 0o600 + await extract(noPermissionsZip, { dir: dirPath, defaultFileMode }) + await assertPermissions(t, path.join(dirPath, 'folder', 'file.txt'), defaultFileMode) + }) +} + +test('files in subdirs where the subdir does not have its own entry is extracted', async t => { + const dirPath = await tempExtract(t, 'subdir-file', subdirZip) + await pathExists(t, path.join(dirPath, 'foo', 'bar'), 'file created') +}) + +test('extract broken zip', async t => { + const dirPath = await mkdtemp(t, 'broken-zip') + await t.throwsAsync(extract(brokenZip, { dir: dirPath }), { + message: 'invalid central directory file header signature: 0x2014b00' + }) +}) diff --git a/test/no-permissions.zip b/test/no-permissions.zip new file mode 100644 index 0000000..756e3bf Binary files /dev/null and b/test/no-permissions.zip differ diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 4f4d105..0000000 --- a/test/test.js +++ /dev/null @@ -1,212 +0,0 @@ -const extract = require('../') -const fs = require('fs') -const os = require('os') -const path = require('path') -const rimraf = require('rimraf') -const test = require('tape') - -const catsZip = path.join(__dirname, 'cats.zip') -const githubZip = path.join(__dirname, 'github.zip') -const subdirZip = path.join(__dirname, 'file-in-subdir-without-subdir-entry.zip') -const symlinkDestZip = path.join(__dirname, 'symlink-dest.zip') -const symlinkZip = path.join(__dirname, 'symlink.zip') -const brokenZip = path.join(__dirname, 'broken.zip') - -const relativeTarget = './cats' - -function mkdtemp (t, suffix, callback) { - fs.mkdtemp(path.join(os.tmpdir(), `extract-zip-${suffix}`), function (err, dirPath) { - t.notOk(err, 'no error when creating temporary directory') - callback(dirPath) - }) -} - -function tempExtract (t, suffix, zipPath, callback) { - mkdtemp(t, suffix, function (dirPath) { - extract(zipPath, { dir: dirPath }, function (err) { - t.notOk(err, 'no error when extracting ' + zipPath) - - callback(dirPath) - }) - }) -} - -function relativeExtract (callback) { - rimraf.sync(relativeTarget) - extract(catsZip, { dir: relativeTarget }, callback) - rimraf.sync(relativeTarget) -} - -function exists (t, pathToCheck, message) { - const exists = fs.existsSync(pathToCheck) - t.true(exists, message) -} - -function doesntExist (t, pathToCheck, message) { - const exists = fs.existsSync(pathToCheck) - t.false(exists, message) -} - -test('files', function (t) { - t.plan(3) - - tempExtract(t, 'files', catsZip, function (dirPath) { - exists(t, path.join(dirPath, 'cats', 'gJqEYBs.jpg'), 'file created') - }) -}) - -test('symlinks', function (t) { - t.plan(7) - - tempExtract(t, 'symlinks', catsZip, function (dirPath) { - const symlink = path.join(dirPath, 'cats', 'orange_symlink') - - exists(t, symlink, 'symlink created') - - fs.lstat(symlink, function (err, stats) { - t.same(err, null, "symlink can be stat'd") - t.ok(stats.isSymbolicLink(), 'symlink is valid') - fs.readlink(symlink, function (err, linkString) { - t.same(err, null, 'symlink itself can be read') - t.equal(linkString, 'orange') - }) - }) - }) -}) - -test('directories', function (t) { - t.plan(8) - - tempExtract(t, 'directories', catsZip, function (dirPath) { - const dirWithContent = path.join(dirPath, 'cats', 'orange') - const dirWithoutContent = path.join(dirPath, 'cats', 'empty') - - exists(t, dirWithContent, 'directory created') - - fs.readdir(dirWithContent, function (err, files) { - t.same(err, null, 'directory can be read') - t.ok(files.length > 0, 'directory has files') - }) - - exists(t, dirWithoutContent, 'empty directory created') - - fs.readdir(dirWithoutContent, function (err, files) { - t.same(err, null, 'empty directory can be read') - t.ok(files.length === 0, 'empty directory has no files') - }) - }) -}) - -test('verify github zip extraction worked', function (t) { - t.plan(3) - - tempExtract(t, 'verify-extraction', githubZip, function (dirPath) { - exists(t, path.join(dirPath, 'extract-zip-master', 'test'), 'folder created') - }) -}) - -test('opts.onEntry', function (t) { - t.plan(3) - - mkdtemp(t, 'onEntry', function (dirPath) { - const actualEntries = [] - const expectedEntries = [ - 'symlink/', - 'symlink/foo.txt', - 'symlink/foo_symlink.txt' - ] - const onEntry = function (entry) { - actualEntries.push(entry.fileName) - } - extract(symlinkZip, { dir: dirPath, onEntry }, function (err) { - t.notOk(err) - - t.same(actualEntries, expectedEntries, 'entries should match') - }) - }) -}) - -test('callback called once', function (t) { - t.plan(4) - - tempExtract(t, 'callback', symlinkZip, function (dirPath) { - // this triggers an error due to symlink creation - extract(symlinkZip, { dir: dirPath }, function (err) { - if (err) t.ok(true, 'error passed') - - t.ok(true, 'callback called') - }) - }) -}) - -test('relative target directory', function (t) { - t.plan(2) - - relativeExtract(function (err) { - t.true(err instanceof Error, 'is native V8 error') - t.same(err.message, 'Target directory is expected to be absolute', 'has descriptive error message') - }) -}) - -test('no folder created', function (t) { - t.plan(2) - - relativeExtract(function (err) { - t.true(err instanceof Error, 'is native V8 error') - doesntExist(t, path.join(__dirname, relativeTarget), 'file not created') - }) -}) - -if (process.platform !== 'win32') { - test('symlink destination disallowed', function (t) { - t.plan(4) - - mkdtemp(t, 'symlink-destination-disallowed', function (dirPath) { - doesntExist(t, path.join(dirPath, 'file.txt'), "file doesn't exist at symlink target") - - extract(symlinkDestZip, { dir: dirPath }, function (err) { - t.true(err instanceof Error, 'is native V8 error') - - if (err) { - t.match(err.message, /Out of bound path ".*?" found while processing file symlink-dest\/aaa\/file.txt/, 'has descriptive error message') - } - }) - }) - }) - - test('no file created out of bound', function (t) { - t.plan(7) - - mkdtemp(t, 'out-of-bounds-file', function (dirPath) { - extract(symlinkDestZip, { dir: dirPath }, function (err) { - const symlinkDestDir = path.join(dirPath, 'symlink-dest') - - t.true(err instanceof Error, 'is native V8 error') - - exists(t, symlinkDestDir, 'target folder created') - exists(t, path.join(symlinkDestDir, 'aaa'), 'symlink created') - exists(t, path.join(symlinkDestDir, 'ccc'), 'parent folder created') - doesntExist(t, path.join(symlinkDestDir, 'ccc/file.txt'), 'file not created in original folder') - doesntExist(t, path.join(dirPath, 'file.txt'), 'file not created in symlink target') - }) - }) - }) -} - -test('files in subdirs where the subdir does not have its own entry is extracted', function (t) { - t.plan(3) - - tempExtract(t, 'subdir-file', subdirZip, function (dirPath) { - exists(t, path.join(dirPath, 'foo', 'bar'), 'file created') - }) -}) - -test('extract broken zip', function (t) { - t.plan(2) - - mkdtemp(t, 'broken-zip', function (dirPath) { - extract(brokenZip, { dir: dirPath }, function (err) { - t.ok(err, 'Error: invalid central directory file header signature: 0x2014b00') - }) - }) -})