From 7eac676fa740d8f6f19671a38a805e0e3f4fcacd Mon Sep 17 00:00:00 2001 From: "trop[bot]" Date: Thu, 2 May 2019 22:34:25 -0700 Subject: [PATCH] fix: fs.promises does not work with asar paths (#18115) --- lib/common/asar.js | 52 ++++++--- spec/asar-spec.js | 256 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+), 15 deletions(-) diff --git a/lib/common/asar.js b/lib/common/asar.js index ee8f9dd2a6c69..f0977753d0704 100644 --- a/lib/common/asar.js +++ b/lib/common/asar.js @@ -201,24 +201,32 @@ } if (old[util.promisify.custom]) { - module[name][util.promisify.custom] = function () { - const pathArgument = arguments[pathArgumentIndex] - const { isAsar, asarPath, filePath } = splitPath(pathArgument) - if (!isAsar) return old[util.promisify.custom].apply(this, arguments) - - const archive = getOrCreateArchive(asarPath) - if (!archive) { - return Promise.reject(createError(AsarError.INVALID_ARCHIVE, { asarPath })) - } + module[name][util.promisify.custom] = makePromiseFunction(old[util.promisify.custom], pathArgumentIndex) + } - const newPath = archive.copyFileOut(filePath) - if (!newPath) { - return Promise.reject(createError(AsarError.NOT_FOUND, { asarPath, filePath })) - } + if (module.promises && module.promises[name]) { + module.promises[name] = makePromiseFunction(module.promises[name], pathArgumentIndex) + } + } - arguments[pathArgumentIndex] = newPath - return old[util.promisify.custom].apply(this, arguments) + const makePromiseFunction = function (orig, pathArgumentIndex) { + return function (...args) { + const pathArgument = args[pathArgumentIndex] + const { isAsar, asarPath, filePath } = splitPath(pathArgument) + if (!isAsar) return orig.apply(this, args) + + const archive = getOrCreateArchive(asarPath) + if (!archive) { + return Promise.reject(createError(AsarError.INVALID_ARCHIVE, { asarPath })) } + + const newPath = archive.copyFileOut(filePath) + if (!newPath) { + return Promise.reject(createError(AsarError.NOT_FOUND, { asarPath, filePath })) + } + + args[pathArgumentIndex] = newPath + return orig.apply(this, args) } } @@ -277,6 +285,8 @@ nextTick(callback, [null, fsStats]) } + fs.promises.lstat = util.promisify(fs.lstat) + const { statSync } = fs fs.statSync = (pathArgument, options) => { const { isAsar } = splitPath(pathArgument) @@ -299,6 +309,8 @@ process.nextTick(() => fs.lstat(pathArgument, options, callback)) } + fs.promises.stat = util.promisify(fs.stat) + const { realpathSync } = fs fs.realpathSync = function (pathArgument, options) { const { isAsar, asarPath, filePath } = splitPath(pathArgument) @@ -401,6 +413,8 @@ }) } + fs.promises.realpath = util.promisify(fs.realpath.native) + const { exists } = fs fs.exists = (pathArgument, callback) => { const { isAsar, asarPath, filePath } = splitPath(pathArgument) @@ -486,6 +500,8 @@ nextTick(callback) } + fs.promises.access = util.promisify(fs.access) + const { accessSync } = fs fs.accessSync = function (pathArgument, mode) { const { isAsar, asarPath, filePath } = splitPath(pathArgument) @@ -573,6 +589,8 @@ }) } + fs.promises.readFile = util.promisify(fs.readFile) + const { readFileSync } = fs fs.readFileSync = function (pathArgument, options) { const { isAsar, asarPath, filePath } = splitPath(pathArgument) @@ -634,6 +652,8 @@ nextTick(callback, [null, files]) } + fs.promises.readdir = util.promisify(fs.readdir) + const { readdirSync } = fs fs.readdirSync = function (pathArgument, options) { const { isAsar, asarPath, filePath } = splitPath(pathArgument) @@ -713,6 +733,8 @@ mkdir(pathArgument, options, callback) } + fs.promises.mkdir = util.promisify(fs.mkdir) + const { mkdirSync } = fs fs.mkdirSync = function (pathArgument, options) { const { isAsar, filePath } = splitPath(pathArgument) diff --git a/spec/asar-spec.js b/spec/asar-spec.js index 18a9dc2d21067..ae0ec5e8934c1 100644 --- a/spec/asar-spec.js +++ b/spec/asar-spec.js @@ -14,6 +14,18 @@ const { ipcMain, BrowserWindow } = remote const features = process.atomBinding('features') +async function expectToThrowErrorWithCode (func, code) { + let error + try { + await func() + } catch (e) { + error = e + } + + expect(error).is.an('Error') + expect(error).to.have.property('code').which.equals(code) +} + describe('asar package', function () { const fixtures = path.join(__dirname, 'fixtures') @@ -139,6 +151,43 @@ describe('asar package', function () { }) }) + describe('fs.promises.readFile', function () { + it('reads a normal file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const content = await fs.promises.readFile(p) + assert.strictEqual(String(content).trim(), 'file1') + }) + + it('reads from a empty file', async function () { + const p = path.join(fixtures, 'asar', 'empty.asar', 'file1') + const content = await fs.promises.readFile(p) + assert.strictEqual(String(content), '') + }) + + it('reads from a empty file with encoding', async function () { + const p = path.join(fixtures, 'asar', 'empty.asar', 'file1') + const content = await fs.promises.readFile(p, 'utf8') + assert.strictEqual(content, '') + }) + + it('reads a linked file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'link1') + const content = await fs.promises.readFile(p) + assert.strictEqual(String(content).trim(), 'file1') + }) + + it('reads a file from linked directory', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1') + const content = await fs.promises.readFile(p) + assert.strictEqual(String(content).trim(), 'file1') + }) + + it('throws ENOENT error when can not find file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + await expectToThrowErrorWithCode(() => fs.promises.readFile(p), 'ENOENT') + }) + }) + describe('fs.copyFile', function () { it('copies a normal file', function (done) { const p = path.join(fixtures, 'asar', 'a.asar', 'file1') @@ -161,6 +210,22 @@ describe('asar package', function () { }) }) + describe('fs.promises.copyFile', function () { + it('copies a normal file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const dest = temp.path() + await fs.promises.copyFile(p, dest) + assert(fs.readFileSync(p).equals(fs.readFileSync(dest))) + }) + + it('copies a unpacked file', async function () { + const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') + const dest = temp.path() + await fs.promises.copyFile(p, dest) + assert(fs.readFileSync(p).equals(fs.readFileSync(dest))) + }) + }) + describe('fs.copyFileSync', function () { it('copies a normal file', function () { const p = path.join(fixtures, 'asar', 'a.asar', 'file1') @@ -354,6 +419,72 @@ describe('asar package', function () { }) }) + describe('fs.promises.lstat', function () { + it('handles path with trailing slash correctly', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2', 'file1') + await fs.promises.lstat(p + '/') + }) + + it('returns information of root', async function () { + const p = path.join(fixtures, 'asar', 'a.asar') + const stats = await fs.promises.lstat(p) + assert.strictEqual(stats.isFile(), false) + assert.strictEqual(stats.isDirectory(), true) + assert.strictEqual(stats.isSymbolicLink(), false) + assert.strictEqual(stats.size, 0) + }) + + it('returns information of root with stats as bigint', async function () { + const p = path.join(fixtures, 'asar', 'a.asar') + const stats = await fs.promises.lstat(p, { bigint: false }) + assert.strictEqual(stats.isFile(), false) + assert.strictEqual(stats.isDirectory(), true) + assert.strictEqual(stats.isSymbolicLink(), false) + assert.strictEqual(stats.size, 0) + }) + + it('returns information of a normal file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'file1') + const stats = await fs.promises.lstat(p) + assert.strictEqual(stats.isFile(), true) + assert.strictEqual(stats.isDirectory(), false) + assert.strictEqual(stats.isSymbolicLink(), false) + assert.strictEqual(stats.size, 6) + }) + + it('returns information of a normal directory', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'dir1') + const stats = await fs.promises.lstat(p) + assert.strictEqual(stats.isFile(), false) + assert.strictEqual(stats.isDirectory(), true) + assert.strictEqual(stats.isSymbolicLink(), false) + assert.strictEqual(stats.size, 0) + }) + + it('returns information of a linked file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link1') + const stats = await fs.promises.lstat(p) + assert.strictEqual(stats.isFile(), false) + assert.strictEqual(stats.isDirectory(), false) + assert.strictEqual(stats.isSymbolicLink(), true) + assert.strictEqual(stats.size, 0) + }) + + it('returns information of a linked directory', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2') + const stats = await fs.promises.lstat(p) + assert.strictEqual(stats.isFile(), false) + assert.strictEqual(stats.isDirectory(), false) + assert.strictEqual(stats.isSymbolicLink(), true) + assert.strictEqual(stats.size, 0) + }) + + it('throws ENOENT error when can not find file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'file4') + await expectToThrowErrorWithCode(() => fs.promises.lstat(p), 'ENOENT') + }) + }) + describe('fs.realpathSync', () => { it('returns real path root', () => { const parent = fs.realpathSync(path.join(fixtures, 'asar')) @@ -527,6 +658,56 @@ describe('asar package', function () { }) }) + describe('fs.promises.realpath', () => { + it('returns real path root', async () => { + const parent = fs.realpathSync(path.join(fixtures, 'asar')) + const p = 'a.asar' + const r = await fs.promises.realpath(path.join(parent, p)) + assert.strictEqual(r, path.join(parent, p)) + }) + + it('returns real path of a normal file', async () => { + const parent = fs.realpathSync(path.join(fixtures, 'asar')) + const p = path.join('a.asar', 'file1') + const r = await fs.promises.realpath(path.join(parent, p)) + assert.strictEqual(r, path.join(parent, p)) + }) + + it('returns real path of a normal directory', async () => { + const parent = fs.realpathSync(path.join(fixtures, 'asar')) + const p = path.join('a.asar', 'dir1') + const r = await fs.promises.realpath(path.join(parent, p)) + assert.strictEqual(r, path.join(parent, p)) + }) + + it('returns real path of a linked file', async () => { + const parent = fs.realpathSync(path.join(fixtures, 'asar')) + const p = path.join('a.asar', 'link2', 'link1') + const r = await fs.promises.realpath(path.join(parent, p)) + assert.strictEqual(r, path.join(parent, 'a.asar', 'file1')) + }) + + it('returns real path of a linked directory', async () => { + const parent = fs.realpathSync(path.join(fixtures, 'asar')) + const p = path.join('a.asar', 'link2', 'link2') + const r = await fs.promises.realpath(path.join(parent, p)) + assert.strictEqual(r, path.join(parent, 'a.asar', 'dir1')) + }) + + it('returns real path of an unpacked file', async () => { + const parent = fs.realpathSync(path.join(fixtures, 'asar')) + const p = path.join('unpack.asar', 'a.txt') + const r = await fs.promises.realpath(path.join(parent, p)) + assert.strictEqual(r, path.join(parent, p)) + }) + + it('throws ENOENT error when can not find file', async () => { + const parent = fs.realpathSync(path.join(fixtures, 'asar')) + const p = path.join('a.asar', 'not-exist') + await expectToThrowErrorWithCode(() => fs.promises.realpath(path.join(parent, p)), 'ENOENT') + }) + }) + describe('fs.realpath.native', () => { it('returns real path root', done => { const parent = fs.realpathSync.native(path.join(fixtures, 'asar')) @@ -662,6 +843,31 @@ describe('asar package', function () { }) }) + describe('fs.promises.readdir', function () { + it('reads dirs from root', async function () { + const p = path.join(fixtures, 'asar', 'a.asar') + const dirs = await fs.promises.readdir(p) + assert.deepStrictEqual(dirs, ['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']) + }) + + it('reads dirs from a normal dir', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'dir1') + const dirs = await fs.promises.readdir(p) + assert.deepStrictEqual(dirs, ['file1', 'file2', 'file3', 'link1', 'link2']) + }) + + it('reads dirs from a linked dir', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'link2', 'link2') + const dirs = await fs.promises.readdir(p) + assert.deepStrictEqual(dirs, ['file1', 'file2', 'file3', 'link1', 'link2']) + }) + + it('throws ENOENT error when can not find file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + await expectToThrowErrorWithCode(() => fs.promises.readdir(p), 'ENOENT') + }) + }) + describe('fs.openSync', function () { it('opens a normal/linked/under-linked-directory file', function () { const ref2 = ['file1', 'link1', path.join('link2', 'file1')] @@ -708,6 +914,22 @@ describe('asar package', function () { }) }) + describe('fs.promises.open', function () { + it('opens a normal file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + const fh = await fs.promises.open(p, 'r') + const buffer = Buffer.alloc(6) + await fh.read(buffer, 0, 6, 0) + assert.strictEqual(String(buffer).trim(), 'file1') + await fh.close() + }) + + it('throws ENOENT error when can not find file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + await expectToThrowErrorWithCode(() => fs.promises.open(p, 'r'), 'ENOENT') + }) + }) + describe('fs.mkdir', function () { it('throws error when calling inside asar archive', function (done) { const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') @@ -718,6 +940,13 @@ describe('asar package', function () { }) }) + describe('fs.promises.mkdir', function () { + it('throws error when calling inside asar archive', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + await expectToThrowErrorWithCode(() => fs.promises.mkdir(p), 'ENOTDIR') + }) + }) + describe('fs.mkdirSync', function () { it('throws error when calling inside asar archive', function () { const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') @@ -815,6 +1044,28 @@ describe('asar package', function () { }) }) + describe('fs.promises.access', function () { + it('accesses a normal file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + await fs.promises.access(p) + }) + + it('throws an error when called with write mode', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'file1') + await expectToThrowErrorWithCode(() => fs.promises.access(p, fs.constants.R_OK | fs.constants.W_OK), 'EACCES') + }) + + it('throws an error when called on non-existent file', async function () { + const p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + await expectToThrowErrorWithCode(() => fs.promises.access(p), 'ENOENT') + }) + + it('allows write mode for unpacked files', async function () { + const p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') + await fs.promises.access(p, fs.constants.R_OK | fs.constants.W_OK) + }) + }) + describe('fs.accessSync', function () { it('accesses a normal file', function () { const p = path.join(fixtures, 'asar', 'a.asar', 'file1') @@ -1235,6 +1486,11 @@ describe('asar package', function () { it('can be used with streams', () => { originalFs.createReadStream(path.join(fixtures, 'asar', 'a.asar')) }) + + it('has the same APIs as fs', function () { + expect(Object.keys(require('fs'))).to.deep.equal(Object.keys(require('original-fs'))) + expect(Object.keys(require('fs').promises)).to.deep.equal(Object.keys(require('original-fs').promises)) + }) }) describe('graceful-fs module', function () {