From 0b8b0387a0ceb0f8b3547cd9ef48973957bd894d Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Fri, 7 Mar 2025 08:34:25 -0800 Subject: [PATCH 1/3] feat: allow optional destination field for `asar extract-file` / `asar ef` --- .mocharc.js | 1 + bin/asar.js | 21 ++++++++++++++------- package.json | 2 +- test/cli-spec.js | 30 ++++++++++++++---------------- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/.mocharc.js b/.mocharc.js index 9a9cc704..efda0e4a 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -3,4 +3,5 @@ module.exports = { recursive: true, file: './mocha.setup.js', // setup file before everything else loads 'forbid-only': process.env.CI ?? false, // make sure no `test.only` is merged into `main` + reporter: 'spec', }; diff --git a/bin/asar.js b/bin/asar.js index c3d094e1..9d3f0e80 100755 --- a/bin/asar.js +++ b/bin/asar.js @@ -55,13 +55,20 @@ program.command('list ') } }) -program.command('extract-file ') - .alias('ef') - .description('extract one file from archive') - .action(function (archive, filename) { - require('fs').writeFileSync(require('path').basename(filename), - asar.extractFile(archive, filename)) - }) + program + .command('extract-file [destination]') + .alias('ef') + .description( + 'extract one file from archive. Optionally output to (already-existing) destination folder, otherwise cwd will be used', + ) + .action(function (archive, filename, destination) { + const path = require('path'); + const file = path.basename(filename); + destination = destination?.trim() + const out = destination ? path.join(destination, file) : file; + require('fs').writeFileSync(out, asar.extractFile(archive, filename)); + }); + program.command('extract ') .alias('e') diff --git a/package.json b/package.json index 8cceed31..e05d793c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ }, "scripts": { "build": "tsc", - "mocha": "xvfb-maybe electron-mocha --reporter spec && mocha --reporter spec", + "mocha": "xvfb-maybe electron-mocha && mocha", "mocha:update": "UPDATE_SNAPSHOT=true yarn mocha", "mocha:watch": "mocha --watch", "test": "yarn lint && yarn mocha", diff --git a/test/cli-spec.js b/test/cli-spec.js index deac4b65..1bc6b3f8 100644 --- a/test/cli-spec.js +++ b/test/cli-spec.js @@ -32,6 +32,7 @@ async function assertAsarOutputMatches(args, expectedFilename) { describe('command line interface', function () { beforeEach(() => { rimraf.sync(TEST_APPS_DIR, fs); + fs.mkdirSync(TEST_APPS_DIR); }); it('should create archive from directory', async () => { @@ -76,27 +77,24 @@ describe('command line interface', function () { 'test/expected/packthis-unicode-path-filelist.txt', ); }); - // we need a way to set a path to extract to first, otherwise we pollute our project dir - // or we fake it by setting our cwd, but I don't like that - /* it('should extract a text file from archive', async () => { - await execAsar('ef test/input/extractthis.asar dir1/file1.txt') - const actual = await fs.readFile('tmp/file1.txt', 'utf8') - let expected = await fs.readFile('test/expected/extractthis/dir1/file1.txt', 'utf8') + await execAsar(`ef test/input/extractthis.asar dir1/file1.txt ${TEST_APPS_DIR}`); + const actual = await fs.readFile(path.join(TEST_APPS_DIR, 'file1.txt'), 'utf8'); + let expected = await fs.readFile('test/expected/extractthis/dir1/file1.txt', 'utf8'); // on windows replace crlf with lf if (os.platform() === 'win32') { - expected = expected.replace(/\r\n/g, '\n') + expected = expected.replace(/\r\n/g, '\n'); } - assert.strictEqual(actual, expected) - }) + assert.strictEqual(actual, expected); + }); + it('should extract a binary file from archive', async () => { + await execAsar(`ef test/input/extractthis.asar dir2/file2.png ${TEST_APPS_DIR}`); + await compFiles( + path.join(TEST_APPS_DIR, 'file2.png'), + 'test/expected/extractthis/dir2/file2.png', + ); + }); - it('should extract a binary file from archive', async () => { - await execAsar('ef test/input/extractthis.asar dir2/file2.png') - const actual = await fs.readFile('tmp/file2.png', 'utf8') - const expected = await fs.readFile('test/expected/extractthis/dir2/file2.png', 'utf8') - assert.strictEqual(actual, expected) - }) - */ it('should extract an archive', async () => { await execAsar('e test/input/extractthis.asar tmp/extractthis-cli/'); return compDirs('tmp/extractthis-cli/', 'test/expected/extractthis'); From 0103c3e754ac047dc2a351cfcaf0ff919e034ade Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sun, 9 Mar 2025 11:02:21 -0700 Subject: [PATCH 2/3] add filesystem checks for destination dir and output file already existing --- bin/asar.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bin/asar.js b/bin/asar.js index 9d3f0e80..f0f0a281 100755 --- a/bin/asar.js +++ b/bin/asar.js @@ -63,10 +63,22 @@ program.command('list ') ) .action(function (archive, filename, destination) { const path = require('path'); + const fs = require('fs'); + + // extract first to memory so that error is thrown if file does not exist + const fileData = asar.extractFile(archive, filename) const file = path.basename(filename); destination = destination?.trim() const out = destination ? path.join(destination, file) : file; - require('fs').writeFileSync(out, asar.extractFile(archive, filename)); + + // check if destination exists. when destination is not provided, we will write to cwd + if (destination && !fs.existsSync(destination)) { + throw new Error("destination directory does not exist, please create before attempting extraction") + } + if (fs.existsSync(out)) { + throw new Error("destination file already exists") + } + fs.writeFileSync(out, fileData); }); From cc55a060c8c0201d00d424573bf1d17a238904ab Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sun, 9 Mar 2025 11:08:13 -0700 Subject: [PATCH 3/3] add additional unit test for `throw Error` logic --- test/cli-spec.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/cli-spec.js b/test/cli-spec.js index 9fadf7d0..75c19483 100644 --- a/test/cli-spec.js +++ b/test/cli-spec.js @@ -32,7 +32,6 @@ async function assertAsarOutputMatches(args, expectedFilename) { describe('command line interface', function () { beforeEach(() => { rimraf.sync(TEST_APPS_DIR, fs); - fs.mkdirSync(TEST_APPS_DIR); }); it('should create archive from directory', async () => { @@ -78,6 +77,7 @@ describe('command line interface', function () { ); }); it('should extract a text file from archive', async () => { + fs.mkdirSync(TEST_APPS_DIR); await execAsar(`ef test/input/extractthis.asar dir1/file1.txt ${TEST_APPS_DIR}`); const actual = await fs.readFile(path.join(TEST_APPS_DIR, 'file1.txt'), 'utf8'); let expected = await fs.readFile('test/expected/extractthis/dir1/file1.txt', 'utf8'); @@ -88,12 +88,19 @@ describe('command line interface', function () { assert.strictEqual(actual, expected); }); it('should extract a binary file from archive', async () => { + fs.mkdirSync(TEST_APPS_DIR); await execAsar(`ef test/input/extractthis.asar dir2/file2.png ${TEST_APPS_DIR}`); await compFiles( path.join(TEST_APPS_DIR, 'file2.png'), 'test/expected/extractthis/dir2/file2.png', ); }); + it('should throw error when extracting file to non-existant destination directory', async () => { + await assert.rejects( + execAsar(`ef test/input/extractthis.asar dir2/file2.png ${TEST_APPS_DIR}`), + /destination directory does not exist, please create before attempting extraction/, + ); + }); it('should extract an archive', async () => { await execAsar('e test/input/extractthis.asar tmp/extractthis-cli/');