diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4a3e95..634856c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,4 +38,4 @@ jobs: - name: Lint run: yarn lint - name: Test - run: yarn coverage + run: yarn ci:coverage diff --git a/cli.js b/cli.js index b594016..c82f4c8 100755 --- a/cli.js +++ b/cli.js @@ -7,12 +7,18 @@ var extract = require('./') var args = process.argv.slice(2) var source = args[0] var dest = args[1] || process.cwd() +var encodingReg = /^--encoding=*/ +var encodingIndex = args.findIndex(entry => encodingReg.test(entry)) +var encoding +if (encodingIndex !== -1) { + encoding = args.splice(encodingIndex, 1)[0].replace(encodingReg, '') +} if (!source) { - console.error('Usage: extract-zip foo.zip ') + console.error('Usage: extract-zip foo.zip [--encoding=]') process.exit(1) } -extract(source, { dir: dest }) +extract(source, { dir: dest, encoding }) .catch(function (err) { console.error('error!', err) process.exit(1) diff --git a/index.d.ts b/index.d.ts index 6dbcc80..b4b17bd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,6 +6,7 @@ import { Entry, ZipFile } from 'yauzl'; declare namespace extract { interface Options { + encoding?: string; dir: string; defaultDirMode?: number; defaultFileMode?: number; diff --git a/index.js b/index.js index 23384ea..5bb4200 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,7 @@ 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 { promisify, TextDecoder } = require('util') const stream = require('stream') const yauzl = require('yauzl') @@ -14,12 +14,13 @@ class Extractor { constructor (zipPath, opts) { this.zipPath = zipPath this.opts = opts + this.decoder = new TextDecoder(opts.encoding) } async extract () { debug('opening', this.zipPath, 'with opts', this.opts) - this.zipfile = await openZip(this.zipPath, { lazyEntries: true }) + this.zipfile = await openZip(this.zipPath, { lazyEntries: true, decodeStrings: false }) this.canceled = false return new Promise((resolve, reject) => { @@ -37,6 +38,9 @@ class Extractor { }) this.zipfile.on('entry', async entry => { + entry.fileName = this.decoder.decode(entry.fileName) + entry.fileNameLength = entry.fileName.length + /* istanbul ignore if */ if (this.canceled) { debug('skipping entry', entry.fileName, { cancelled: this.canceled }) diff --git a/index.test-d.ts b/index.test-d.ts index 62fc01d..692240a 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -10,6 +10,7 @@ let options: extract.Options = { }; options = { dir, + encoding: 'shift-jis', defaultDirMode: 0o700, defaultFileMode, onEntry: (entry: Entry, zipfile: ZipFile): void => { diff --git a/package.json b/package.json index f59a35a..7568c37 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lint:js": "eslint .", "lint:ts": "eslint --config .eslintrc.typescript.js --ext .ts .", "test": "yarn lint && ava", + "ci:coverage": "full-icu node_modules/nyc/bin/nyc.js ava", "tsd": "tsd" }, "files": [ @@ -29,7 +30,7 @@ "extract" ], "engines": { - "node": ">= 10.17.0" + "node": ">= 10.18.0" }, "dependencies": { "debug": "^4.1.1", @@ -40,6 +41,7 @@ "@types/yauzl": "^2.9.1" }, "devDependencies": { + "@leichtgewicht/full-icu": "^1.3.3", "@typescript-eslint/eslint-plugin": "^3.2.0", "@typescript-eslint/parser": "^3.2.0", "ava": "^3.5.1", diff --git a/readme.md b/readme.md index 4ee7108..20b7937 100644 --- a/readme.md +++ b/readme.md @@ -42,16 +42,19 @@ async function main () { ### Options - `dir` (required) - the path to the directory where the extracted files are written +- `encoding` - string - [encoding][] to be used for file names, defaults to `utf-8` - `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. +[encoding]: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/encoding + ## CLI Usage ``` -extract-zip foo.zip +extract-zip foo.zip [--encoding=] ``` If not specified, `targetDirectory` will default to `process.cwd()`. diff --git a/test/index.js b/test/index.js index a955a21..4d22854 100644 --- a/test/index.js +++ b/test/index.js @@ -11,6 +11,7 @@ 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 mojibakeZip = path.join(__dirname, 'mojibake.zip') const relativeTarget = './cats' @@ -161,3 +162,11 @@ test('extract broken zip', async t => { message: 'invalid central directory file header signature: 0x2014b00' }) }) + +test('extract mojibake', async t => { + const dirPath = await mkdtemp(t, 'mojibake-zip') + await extract(mojibakeZip, { dir: dirPath, encoding: 'windows-949' }) + await pathExists(t, path.join(dirPath, '새 텍스트 문서.txt'), 'file created') + await pathExists(t, path.join(dirPath, '새 폴더'), 'folder created') + await pathExists(t, path.join(dirPath, '새 폴더', '한글문서.txt'), 'subfile created') +}) diff --git a/test/mojibake.zip b/test/mojibake.zip new file mode 100644 index 0000000..473a00b Binary files /dev/null and b/test/mojibake.zip differ