diff --git a/lib/common/asar.js b/lib/common/asar.js index 6adc24e8ded0..fcb26ba97b71 100644 --- a/lib/common/asar.js +++ b/lib/common/asar.js @@ -4,14 +4,13 @@ const path = require('path') const util = require('util') - var hasProp = {}.hasOwnProperty + const hasProp = {}.hasOwnProperty // Cache asar archive objects. - var cachedArchives = {} + const cachedArchives = {} - var getOrCreateArchive = function (p) { - var archive - archive = cachedArchives[p] + const getOrCreateArchive = function (p) { + let archive = cachedArchives[p] if (archive != null) { return archive } @@ -25,31 +24,33 @@ // Clean cache on quit. process.on('exit', function () { - var archive, p - for (p in cachedArchives) { + for (let p in cachedArchives) { if (!hasProp.call(cachedArchives, p)) continue - archive = cachedArchives[p] - archive.destroy() + cachedArchives[p].destroy() } }) // Separate asar package's path from full path. - var splitPath = function (p) { - var index - + const splitPath = function (p) { // shortcut to disable asar. if (process.noAsar) { return [false] } + if (Buffer.isBuffer(p)) { + p = p.toString() + } + if (typeof p !== 'string') { return [false] } + if (p.substr(-5) === '.asar') { return [true, p, ''] } + p = path.normalize(p) - index = p.lastIndexOf('.asar' + path.sep) + const index = p.lastIndexOf('.asar' + path.sep) if (index === -1) { return [false] } @@ -57,15 +58,15 @@ } // Convert asar archive's Stats object to fs's Stats object. - var nextInode = 0 + let nextInode = 0 - var uid = process.getuid != null ? process.getuid() : 0 + const uid = process.getuid != null ? process.getuid() : 0 - var gid = process.getgid != null ? process.getgid() : 0 + const gid = process.getgid != null ? process.getgid() : 0 - var fakeTime = new Date() + const fakeTime = new Date() - var asarStatsToFsStats = function (stats) { + const asarStatsToFsStats = function (stats) { return { dev: 1, ino: ++nextInode, @@ -104,97 +105,110 @@ } // Create a ENOENT error. - var notFoundError = function (asarPath, filePath, callback) { - var error - error = new Error(`ENOENT, ${filePath} not found in ${asarPath}`) + const notFoundError = function (asarPath, filePath, callback) { + const error = new Error(`ENOENT, ${filePath} not found in ${asarPath}`) error.code = 'ENOENT' error.errno = -2 if (typeof callback !== 'function') { throw error } - return process.nextTick(function () { - return callback(error) + process.nextTick(function () { + callback(error) }) } // Create a ENOTDIR error. - var notDirError = function (callback) { - var error - error = new Error('ENOTDIR, not a directory') + const notDirError = function (callback) { + const error = new Error('ENOTDIR, not a directory') error.code = 'ENOTDIR' error.errno = -20 if (typeof callback !== 'function') { throw error } - return process.nextTick(function () { - return callback(error) + process.nextTick(function () { + callback(error) + }) + } + + // Create a EACCES error. + const accessError = function (asarPath, filePath, callback) { + const error = new Error(`EACCES: permission denied, access '${filePath}'`) + error.code = 'EACCES' + error.errno = -13 + if (typeof callback !== 'function') { + throw error + } + process.nextTick(function () { + callback(error) }) } // Create invalid archive error. - var invalidArchiveError = function (asarPath, callback) { - var error - error = new Error(`Invalid package ${asarPath}`) + const invalidArchiveError = function (asarPath, callback) { + const error = new Error(`Invalid package ${asarPath}`) if (typeof callback !== 'function') { throw error } - return process.nextTick(function () { - return callback(error) + process.nextTick(function () { + callback(error) }) } // Override APIs that rely on passing file path instead of content to C++. - var overrideAPISync = function (module, name, arg) { - var old + const overrideAPISync = function (module, name, arg) { if (arg == null) { arg = 0 } - old = module[name] + const old = module[name] module[name] = function () { - var archive, newPath, p - p = arguments[arg] + const p = arguments[arg] const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return old.apply(this, arguments) } - archive = getOrCreateArchive(asarPath) + + const archive = getOrCreateArchive(asarPath) if (!archive) { invalidArchiveError(asarPath) } - newPath = archive.copyFileOut(filePath) + + const newPath = archive.copyFileOut(filePath) if (!newPath) { notFoundError(asarPath, filePath) } + arguments[arg] = newPath return old.apply(this, arguments) } } - var overrideAPI = function (module, name, arg) { - var old + const overrideAPI = function (module, name, arg) { if (arg == null) { arg = 0 } - old = module[name] + const old = module[name] module[name] = function () { - var archive, callback, newPath, p - p = arguments[arg] + const p = arguments[arg] const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return old.apply(this, arguments) } - callback = arguments[arguments.length - 1] + + const callback = arguments[arguments.length - 1] if (typeof callback !== 'function') { return overrideAPISync(module, name, arg) } - archive = getOrCreateArchive(asarPath) + + const archive = getOrCreateArchive(asarPath) if (!archive) { return invalidArchiveError(asarPath, callback) } - newPath = archive.copyFileOut(filePath) + + const newPath = archive.copyFileOut(filePath) if (!newPath) { return notFoundError(asarPath, filePath, callback) } + arguments[arg] = newPath return old.apply(this, arguments) } @@ -202,61 +216,58 @@ // Override fs APIs. exports.wrapFsWithAsar = function (fs) { - var exists, existsSync, internalModuleReadFile, internalModuleStat, lstat, lstatSync, mkdir, mkdirSync, readFile, readFileSync, readdir, readdirSync, realpath, realpathSync, stat, statSync, statSyncNoException, logFDs, logASARAccess - - logFDs = {} - logASARAccess = function (asarPath, filePath, offset) { + const logFDs = {} + const logASARAccess = function (asarPath, filePath, offset) { if (!process.env.ELECTRON_LOG_ASAR_READS) { return } if (!logFDs[asarPath]) { - var logFilename, logPath const path = require('path') - logFilename = path.basename(asarPath, '.asar') + '-access-log.txt' - logPath = path.join(require('os').tmpdir(), logFilename) + const logFilename = path.basename(asarPath, '.asar') + '-access-log.txt' + const logPath = path.join(require('os').tmpdir(), logFilename) logFDs[asarPath] = fs.openSync(logPath, 'a') console.log('Logging ' + asarPath + ' access to ' + logPath) } fs.writeSync(logFDs[asarPath], offset + ': ' + filePath + '\n') } - lstatSync = fs.lstatSync + const {lstatSync} = fs fs.lstatSync = function (p) { - var archive, stats const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return lstatSync(p) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { invalidArchiveError(asarPath) } - stats = archive.stat(filePath) + const stats = archive.stat(filePath) if (!stats) { notFoundError(asarPath, filePath) } return asarStatsToFsStats(stats) } - lstat = fs.lstat + + const {lstat} = fs fs.lstat = function (p, callback) { - var archive, stats const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return lstat(p, callback) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { return invalidArchiveError(asarPath, callback) } - stats = getOrCreateArchive(asarPath).stat(filePath) + const stats = getOrCreateArchive(asarPath).stat(filePath) if (!stats) { return notFoundError(asarPath, filePath, callback) } - return process.nextTick(function () { - return callback(null, asarStatsToFsStats(stats)) + process.nextTick(function () { + callback(null, asarStatsToFsStats(stats)) }) } - statSync = fs.statSync + + const {statSync} = fs fs.statSync = function (p) { const [isAsar] = splitPath(p) if (!isAsar) { @@ -266,7 +277,8 @@ // Do not distinguish links for now. return fs.lstatSync(p) } - stat = fs.stat + + const {stat} = fs fs.stat = function (p, callback) { const [isAsar] = splitPath(p) if (!isAsar) { @@ -274,47 +286,47 @@ } // Do not distinguish links for now. - return process.nextTick(function () { - return fs.lstat(p, callback) + process.nextTick(function () { + fs.lstat(p, callback) }) } - statSyncNoException = fs.statSyncNoException + + const {statSyncNoException} = fs fs.statSyncNoException = function (p) { - var archive, stats const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return statSyncNoException(p) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { return false } - stats = archive.stat(filePath) + const stats = archive.stat(filePath) if (!stats) { return false } return asarStatsToFsStats(stats) } - realpathSync = fs.realpathSync + + const {realpathSync} = fs fs.realpathSync = function (p) { - var archive, real const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return realpathSync.apply(this, arguments) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { invalidArchiveError(asarPath) } - real = archive.realpath(filePath) + const real = archive.realpath(filePath) if (real === false) { notFoundError(asarPath, filePath) } return path.join(realpathSync(asarPath), real) } - realpath = fs.realpath + + const {realpath} = fs fs.realpath = function (p, cache, callback) { - var archive, real const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return realpath.apply(this, arguments) @@ -323,11 +335,11 @@ callback = cache cache = void 0 } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { return invalidArchiveError(asarPath, callback) } - real = archive.realpath(filePath) + const real = archive.realpath(filePath) if (real === false) { return notFoundError(asarPath, filePath, callback) } @@ -338,37 +350,101 @@ return callback(null, path.join(p, real)) }) } - exists = fs.exists + + const {exists} = fs fs.exists = function (p, callback) { - var archive const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return exists(p, callback) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { return invalidArchiveError(asarPath, callback) } - return process.nextTick(function () { - return callback(archive.stat(filePath) !== false) + process.nextTick(function () { + callback(archive.stat(filePath) !== false) }) } - existsSync = fs.existsSync + + const {existsSync} = fs fs.existsSync = function (p) { - var archive const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return existsSync(p) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { return false } return archive.stat(filePath) !== false } - readFile = fs.readFile + + const {access} = fs + fs.access = function (p, mode, callback) { + const [isAsar, asarPath, filePath] = splitPath(p) + if (!isAsar) { + return access.apply(this, arguments) + } + if (typeof mode === 'function') { + callback = mode + mode = fs.constants.F_OK + } + const archive = getOrCreateArchive(asarPath) + if (!archive) { + return invalidArchiveError(asarPath, callback) + } + const info = archive.getFileInfo(filePath) + if (!info) { + return notFoundError(asarPath, filePath, callback) + } + if (info.unpacked) { + const realPath = archive.copyFileOut(filePath) + return fs.access(realPath, mode, callback) + } + const stats = getOrCreateArchive(asarPath).stat(filePath) + if (!stats) { + return notFoundError(asarPath, filePath, callback) + } + if (mode & fs.constants.W_OK) { + return accessError(asarPath, filePath, callback) + } + process.nextTick(function () { + callback() + }) + } + + const {accessSync} = fs + fs.accessSync = function (p, mode) { + const [isAsar, asarPath, filePath] = splitPath(p) + if (!isAsar) { + return accessSync.apply(this, arguments) + } + if (mode == null) { + mode = fs.constants.F_OK + } + const archive = getOrCreateArchive(asarPath) + if (!archive) { + invalidArchiveError(asarPath) + } + const info = archive.getFileInfo(filePath) + if (!info) { + notFoundError(asarPath, filePath) + } + if (info.unpacked) { + const realPath = archive.copyFileOut(filePath) + return fs.accessSync(realPath, mode) + } + const stats = getOrCreateArchive(asarPath).stat(filePath) + if (!stats) { + notFoundError(asarPath, filePath) + } + if (mode & fs.constants.W_OK) { + accessError(asarPath, filePath) + } + } + + const {readFile} = fs fs.readFile = function (p, options, callback) { - var archive, buffer, encoding, fd, info, realPath const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return readFile.apply(this, arguments) @@ -377,21 +453,21 @@ callback = options options = void 0 } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { return invalidArchiveError(asarPath, callback) } - info = archive.getFileInfo(filePath) + const info = archive.getFileInfo(filePath) if (!info) { return notFoundError(asarPath, filePath, callback) } if (info.size === 0) { return process.nextTick(function () { - return callback(null, new Buffer(0)) + callback(null, new Buffer(0)) }) } if (info.unpacked) { - realPath = archive.copyFileOut(filePath) + const realPath = archive.copyFileOut(filePath) return fs.readFile(realPath, options, callback) } if (!options) { @@ -405,31 +481,29 @@ } else if (!util.isObject(options)) { throw new TypeError('Bad arguments') } - encoding = options.encoding - buffer = new Buffer(info.size) - fd = archive.getFd() + const {encoding} = options + const buffer = new Buffer(info.size) + const fd = archive.getFd() if (!(fd >= 0)) { return notFoundError(asarPath, filePath, callback) } logASARAccess(asarPath, filePath, info.offset) - return fs.read(fd, buffer, 0, info.size, info.offset, function (error) { - return callback(error, encoding ? buffer.toString(encoding) : buffer) + fs.read(fd, buffer, 0, info.size, info.offset, function (error) { + callback(error, encoding ? buffer.toString(encoding) : buffer) }) } - readFileSync = fs.readFileSync - fs.readFileSync = function (p, opts) { - // this allows v8 to optimize this function - var archive, buffer, encoding, fd, info, options, realPath - options = opts + + const {readFileSync} = fs + fs.readFileSync = function (p, options) { const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return readFileSync.apply(this, arguments) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { invalidArchiveError(asarPath) } - info = archive.getFileInfo(filePath) + const info = archive.getFileInfo(filePath) if (!info) { notFoundError(asarPath, filePath) } @@ -441,7 +515,7 @@ } } if (info.unpacked) { - realPath = archive.copyFileOut(filePath) + const realPath = archive.copyFileOut(filePath) return fs.readFileSync(realPath, options) } if (!options) { @@ -455,9 +529,9 @@ } else if (!util.isObject(options)) { throw new TypeError('Bad arguments') } - encoding = options.encoding - buffer = new Buffer(info.size) - fd = archive.getFd() + const {encoding} = options + const buffer = new Buffer(info.size) + const fd = archive.getFd() if (!(fd >= 0)) { notFoundError(asarPath, filePath) } @@ -469,89 +543,89 @@ return buffer } } - readdir = fs.readdir + + const {readdir} = fs fs.readdir = function (p, callback) { - var archive, files const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return readdir.apply(this, arguments) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { return invalidArchiveError(asarPath, callback) } - files = archive.readdir(filePath) + const files = archive.readdir(filePath) if (!files) { return notFoundError(asarPath, filePath, callback) } - return process.nextTick(function () { - return callback(null, files) + process.nextTick(function () { + callback(null, files) }) } - readdirSync = fs.readdirSync + + const {readdirSync} = fs fs.readdirSync = function (p) { - var archive, files const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return readdirSync.apply(this, arguments) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { invalidArchiveError(asarPath) } - files = archive.readdir(filePath) + const files = archive.readdir(filePath) if (!files) { notFoundError(asarPath, filePath) } return files } - internalModuleReadFile = process.binding('fs').internalModuleReadFile + + const {internalModuleReadFile} = process.binding('fs') process.binding('fs').internalModuleReadFile = function (p) { - var archive, buffer, fd, info, realPath const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return internalModuleReadFile(p) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) if (!archive) { - return void 0 + return } - info = archive.getFileInfo(filePath) + const info = archive.getFileInfo(filePath) if (!info) { - return void 0 + return } if (info.size === 0) { return '' } if (info.unpacked) { - realPath = archive.copyFileOut(filePath) + const realPath = archive.copyFileOut(filePath) return fs.readFileSync(realPath, { encoding: 'utf8' }) } - buffer = new Buffer(info.size) - fd = archive.getFd() + const buffer = new Buffer(info.size) + const fd = archive.getFd() if (!(fd >= 0)) { - return void 0 + return } logASARAccess(asarPath, filePath, info.offset) fs.readSync(fd, buffer, 0, info.size, info.offset) return buffer.toString('utf8') } - internalModuleStat = process.binding('fs').internalModuleStat + + const {internalModuleStat} = process.binding('fs') process.binding('fs').internalModuleStat = function (p) { - var archive, stats const [isAsar, asarPath, filePath] = splitPath(p) if (!isAsar) { return internalModuleStat(p) } - archive = getOrCreateArchive(asarPath) + const archive = getOrCreateArchive(asarPath) // -ENOENT if (!archive) { return -34 } - stats = archive.stat(filePath) + const stats = archive.stat(filePath) // -ENOENT if (!stats) { @@ -569,7 +643,7 @@ // This is to work around the recursive looping bug of mkdirp since it is // widely used. if (process.platform === 'win32') { - mkdir = fs.mkdir + const {mkdir} = fs fs.mkdir = function (p, mode, callback) { if (typeof mode === 'function') { callback = mode @@ -578,9 +652,10 @@ if (isAsar && filePath.length) { return notDirError(callback) } - return mkdir(p, mode, callback) + mkdir(p, mode, callback) } - mkdirSync = fs.mkdirSync + + const {mkdirSync} = fs fs.mkdirSync = function (p, mode) { const [isAsar, , filePath] = splitPath(p) if (isAsar && filePath.length) { @@ -595,12 +670,12 @@ // called by `childProcess.{exec,execSync}`, causing // Electron to consider the full command as a single path // to an archive. - [ 'exec', 'execSync' ].forEach(function (functionName) { - var old = childProcess[functionName] + ['exec', 'execSync'].forEach(function (functionName) { + const old = childProcess[functionName] childProcess[functionName] = function () { - var processNoAsarOriginalValue = process.noAsar + const processNoAsarOriginalValue = process.noAsar process.noAsar = true - var result = old.apply(this, arguments) + const result = old.apply(this, arguments) process.noAsar = processNoAsarOriginalValue return result } @@ -611,6 +686,6 @@ overrideAPISync(process, 'dlopen', 1) overrideAPISync(require('module')._extensions, '.node', 1) overrideAPISync(fs, 'openSync') - return overrideAPISync(childProcess, 'execFileSync') + overrideAPISync(childProcess, 'execFileSync') } })() diff --git a/spec/asar-spec.js b/spec/asar-spec.js index 376e5ef0805f..81f6ebe2161d 100644 --- a/spec/asar-spec.js +++ b/spec/asar-spec.js @@ -13,6 +13,11 @@ describe('asar package', function () { var fixtures = path.join(__dirname, 'fixtures') describe('node api', function () { + it('supports paths specified as a Buffer', function () { + var file = new Buffer(path.join(fixtures, 'asar', 'a.asar', 'file1')) + assert.equal(fs.existsSync(file), true) + }) + describe('fs.readFileSync', function () { it('does not leak fd', function () { var readCalls = 1 @@ -534,6 +539,70 @@ describe('asar package', function () { }) }) + describe('fs.access', function () { + it('accesses a normal file', function (done) { + var p = path.join(fixtures, 'asar', 'a.asar', 'file1') + fs.access(p, function (err) { + assert(err == null) + done() + }) + }) + + it('throws an error when called with write mode', function (done) { + var p = path.join(fixtures, 'asar', 'a.asar', 'file1') + fs.access(p, fs.constants.R_OK | fs.constants.W_OK, function (err) { + assert.equal(err.code, 'EACCES') + done() + }) + }) + + it('throws an error when called on non-existent file', function (done) { + var p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + fs.access(p, function (err) { + assert.equal(err.code, 'ENOENT') + done() + }) + }) + + it('allows write mode for unpacked files', function (done) { + var p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') + fs.access(p, fs.constants.R_OK | fs.constants.W_OK, function (err) { + assert(err == null) + done() + }) + }) + }) + + describe('fs.accessSync', function () { + it('accesses a normal file', function () { + var p = path.join(fixtures, 'asar', 'a.asar', 'file1') + assert.doesNotThrow(function () { + fs.accessSync(p) + }) + }) + + it('throws an error when called with write mode', function () { + var p = path.join(fixtures, 'asar', 'a.asar', 'file1') + assert.throws(function () { + fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK) + }, /EACCES/) + }) + + it('throws an error when called on non-existent file', function () { + var p = path.join(fixtures, 'asar', 'a.asar', 'not-exist') + assert.throws(function () { + fs.accessSync(p) + }, /ENOENT/) + }) + + it('allows write mode for unpacked files', function () { + var p = path.join(fixtures, 'asar', 'unpack.asar', 'a.txt') + assert.doesNotThrow(function () { + fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK) + }) + }) + }) + describe('child_process.fork', function () { it('opens a normal js file', function (done) { var child = ChildProcess.fork(path.join(fixtures, 'asar', 'a.asar', 'ping.js'))