From f649e604bed7ae9106d9ab9cc95e1bc1515a12e4 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Thu, 16 Jul 2020 11:38:31 -0700 Subject: [PATCH] build: tsify asar and move to webpack js2c pipeline (#24495) * build: tsify asar and move to webpack js2c pipeline * build: use the webpack provider for fs-wrapper --- .eslintrc.json | 1 + BUILD.gn | 27 +- build/webpack/webpack.config.asar.js | 5 + build/webpack/webpack.config.base.js | 6 +- filenames.auto.gni | 15 +- lib/asar/fs-wrapper.ts | 804 +++++++++++++++++++ lib/asar/init.ts | 3 + lib/common/asar.js | 783 ------------------ lib/common/asar_init.js | 4 - lib/{renderer => common}/webpack-provider.ts | 2 +- script/gen-filenames.js | 4 + shell/common/api/electron_api_asar.cc | 12 +- typings/internal-ambient.d.ts | 37 + 13 files changed, 889 insertions(+), 814 deletions(-) create mode 100644 build/webpack/webpack.config.asar.js create mode 100644 lib/asar/fs-wrapper.ts create mode 100644 lib/asar/init.ts delete mode 100644 lib/common/asar.js delete mode 100644 lib/common/asar_init.js rename lib/{renderer => common}/webpack-provider.ts (82%) diff --git a/.eslintrc.json b/.eslintrc.json index 0db6a039d9cf..38e1c21d5bf1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,6 +28,7 @@ }, "globals": { "standardScheme": "readonly", + "globalThis": "readonly", "BUILDFLAG": "readonly", "ENABLE_DESKTOP_CAPTURER": "readonly", "ENABLE_REMOTE_MODULE": "readonly", diff --git a/BUILD.gn b/BUILD.gn index 27660ec4b15a..498419c58e34 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -94,6 +94,15 @@ npm_action("build_electron_definitions") { outputs = [ "$target_gen_dir/tsc/typings/electron.d.ts" ] } +webpack_build("electron_asar_bundle") { + deps = [ ":build_electron_definitions" ] + + inputs = auto_filenames.asar_bundle_deps + + config_file = "//electron/build/webpack/webpack.config.asar.js" + out_file = "$target_gen_dir/js2c/asar_bundle.js" +} + webpack_build("electron_browser_bundle") { deps = [ ":build_electron_definitions" ] @@ -139,25 +148,18 @@ webpack_build("electron_isolated_renderer_bundle") { out_file = "$target_gen_dir/js2c/isolated_bundle.js" } -copy("electron_js2c_copy") { - sources = [ - "lib/common/asar.js", - "lib/common/asar_init.js", - ] - outputs = [ "$target_gen_dir/js2c/{{source_file_part}}" ] -} - action("electron_js2c") { deps = [ + ":electron_asar_bundle", ":electron_browser_bundle", ":electron_isolated_renderer_bundle", - ":electron_js2c_copy", ":electron_renderer_bundle", ":electron_sandboxed_renderer_bundle", ":electron_worker_bundle", ] - webpack_sources = [ + sources = [ + "$target_gen_dir/js2c/asar_bundle.js", "$target_gen_dir/js2c/browser_init.js", "$target_gen_dir/js2c/isolated_bundle.js", "$target_gen_dir/js2c/renderer_init.js", @@ -165,11 +167,6 @@ action("electron_js2c") { "$target_gen_dir/js2c/worker_init.js", ] - sources = webpack_sources + [ - "$target_gen_dir/js2c/asar.js", - "$target_gen_dir/js2c/asar_init.js", - ] - inputs = sources + [ "//third_party/electron_node/tools/js2c.py" ] outputs = [ "$root_gen_dir/electron_natives.cc" ] diff --git a/build/webpack/webpack.config.asar.js b/build/webpack/webpack.config.asar.js new file mode 100644 index 000000000000..83443f467cad --- /dev/null +++ b/build/webpack/webpack.config.asar.js @@ -0,0 +1,5 @@ +module.exports = require('./webpack.config.base')({ + target: 'asar', + alwaysHasNode: true, + targetDeletesNodeGlobals: true +}); diff --git a/build/webpack/webpack.config.base.js b/build/webpack/webpack.config.base.js index c9e51db6c3ff..037159408deb 100644 --- a/build/webpack/webpack.config.base.js +++ b/build/webpack/webpack.config.base.js @@ -138,9 +138,9 @@ module.exports = ({ new AccessDependenciesPlugin(), ...(targetDeletesNodeGlobals ? [ new webpack.ProvidePlugin({ - process: ['@electron/internal/renderer/webpack-provider', 'process'], - global: ['@electron/internal/renderer/webpack-provider', '_global'], - Buffer: ['@electron/internal/renderer/webpack-provider', 'Buffer'] + process: ['@electron/internal/common/webpack-provider', 'process'], + global: ['@electron/internal/common/webpack-provider', '_global'], + Buffer: ['@electron/internal/common/webpack-provider', 'Buffer'] }) ] : []), new webpack.ProvidePlugin({ diff --git a/filenames.auto.gni b/filenames.auto.gni index 4fbe40cb0084..e5030b2019ea 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -273,6 +273,7 @@ auto_filenames = { "lib/common/type-utils.ts", "lib/common/web-view-methods.ts", "lib/common/webpack-globals-provider.ts", + "lib/common/webpack-provider.ts", "lib/renderer/api/context-bridge.ts", "lib/renderer/api/crash-reporter.ts", "lib/renderer/api/desktop-capturer.ts", @@ -294,7 +295,6 @@ auto_filenames = { "lib/renderer/web-view/web-view-element.ts", "lib/renderer/web-view/web-view-impl.ts", "lib/renderer/web-view/web-view-init.ts", - "lib/renderer/webpack-provider.ts", "lib/renderer/window-setup.ts", "package.json", "tsconfig.electron.json", @@ -315,6 +315,7 @@ auto_filenames = { "lib/common/reset-search-paths.ts", "lib/common/type-utils.ts", "lib/common/webpack-globals-provider.ts", + "lib/common/webpack-provider.ts", "lib/renderer/api/context-bridge.ts", "lib/renderer/api/crash-reporter.ts", "lib/renderer/api/desktop-capturer.ts", @@ -326,7 +327,6 @@ auto_filenames = { "lib/renderer/ipc-renderer-internal-utils.ts", "lib/renderer/ipc-renderer-internal.ts", "lib/renderer/remote/callbacks-registry.ts", - "lib/renderer/webpack-provider.ts", "lib/worker/init.ts", "package.json", "tsconfig.electron.json", @@ -334,4 +334,15 @@ auto_filenames = { "typings/internal-ambient.d.ts", "typings/internal-electron.d.ts", ] + + asar_bundle_deps = [ + "lib/asar/fs-wrapper.ts", + "lib/asar/init.ts", + "lib/common/webpack-provider.ts", + "package.json", + "tsconfig.electron.json", + "tsconfig.json", + "typings/internal-ambient.d.ts", + "typings/internal-electron.d.ts", + ] } diff --git a/lib/asar/fs-wrapper.ts b/lib/asar/fs-wrapper.ts new file mode 100644 index 000000000000..b1cfbd4d3e73 --- /dev/null +++ b/lib/asar/fs-wrapper.ts @@ -0,0 +1,804 @@ +import { Buffer } from 'buffer'; +import * as path from 'path'; +import * as util from 'util'; + +const asar = process._linkedBinding('electron_common_asar'); +const v8Util = process._linkedBinding('electron_common_v8_util'); + +const Module = require('module'); + +const Promise: PromiseConstructor = global.Promise as any; + +const envNoAsar = process.env.ELECTRON_NO_ASAR && + process.type !== 'browser' && + process.type !== 'renderer'; +const isAsarDisabled = () => process.noAsar || envNoAsar; + +const internalBinding = (process as any).internalBinding; +delete (process as any).internalBinding; + +const nextTick = (functionToCall: Function, args: any[] = []) => { + process.nextTick(() => functionToCall(...args)); +}; + +// Cache asar archive objects. +const cachedArchives = new Map(); + +const getOrCreateArchive = (archivePath: string) => { + const isCached = cachedArchives.has(archivePath); + if (isCached) { + return cachedArchives.get(archivePath); + } + + const newArchive = asar.createArchive(archivePath); + if (!newArchive) return null; + + cachedArchives.set(archivePath, newArchive); + return newArchive; +}; + +// Separate asar package's path from full path. +const splitPath = (archivePathOrBuffer: string | Buffer) => { + // Shortcut for disabled asar. + if (isAsarDisabled()) return { isAsar: false }; + + // Check for a bad argument type. + let archivePath = archivePathOrBuffer; + if (Buffer.isBuffer(archivePathOrBuffer)) { + archivePath = archivePathOrBuffer.toString(); + } + if (typeof archivePath !== 'string') return { isAsar: false }; + + return asar.splitPath(path.normalize(archivePath)); +}; + +// Convert asar archive's Stats object to fs's Stats object. +let nextInode = 0; + +const uid = process.getuid != null ? process.getuid() : 0; +const gid = process.getgid != null ? process.getgid() : 0; + +const fakeTime = new Date(); + +const asarStatsToFsStats = function (stats: NodeJS.AsarFileStat) { + const { Stats, constants } = require('fs'); + + let mode = constants.S_IROTH ^ constants.S_IRGRP ^ constants.S_IRUSR ^ constants.S_IWUSR; + + if (stats.isFile) { + mode ^= constants.S_IFREG; + } else if (stats.isDirectory) { + mode ^= constants.S_IFDIR; + } else if (stats.isLink) { + mode ^= constants.S_IFLNK; + } + + return new Stats( + 1, // dev + mode, // mode + 1, // nlink + uid, + gid, + 0, // rdev + undefined, // blksize + ++nextInode, // ino + stats.size, + undefined, // blocks, + fakeTime.getTime(), // atim_msec + fakeTime.getTime(), // mtim_msec + fakeTime.getTime(), // ctim_msec + fakeTime.getTime() // birthtim_msec + ); +}; + +const enum AsarError { + NOT_FOUND = 'NOT_FOUND', + NOT_DIR = 'NOT_DIR', + NO_ACCESS = 'NO_ACCESS', + INVALID_ARCHIVE = 'INVALID_ARCHIVE' +} + +type AsarErrorObject = Error & { code?: string, errno?: number }; + +const createError = (errorType: AsarError, { asarPath, filePath }: { asarPath?: string, filePath?: string } = {}) => { + let error: AsarErrorObject; + switch (errorType) { + case AsarError.NOT_FOUND: + error = new Error(`ENOENT, ${filePath} not found in ${asarPath}`); + error.code = 'ENOENT'; + error.errno = -2; + break; + case AsarError.NOT_DIR: + error = new Error('ENOTDIR, not a directory'); + error.code = 'ENOTDIR'; + error.errno = -20; + break; + case AsarError.NO_ACCESS: + error = new Error(`EACCES: permission denied, access '${filePath}'`); + error.code = 'EACCES'; + error.errno = -13; + break; + case AsarError.INVALID_ARCHIVE: + error = new Error(`Invalid package ${asarPath}`); + break; + default: + throw new Error(`Invalid error type "${errorType}" passed to createError.`); + } + return error; +}; + +const overrideAPISync = function (module: Record, name: string, pathArgumentIndex?: number | null, fromAsync: boolean = false) { + if (pathArgumentIndex == null) pathArgumentIndex = 0; + const old = module[name]; + const func = function (this: any, ...args: any[]) { + const pathArgument = args[pathArgumentIndex!]; + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return old.apply(this, args); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); + + const newPath = archive.copyFileOut(filePath); + if (!newPath) throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); + + args[pathArgumentIndex!] = newPath; + return old.apply(this, args); + }; + if (fromAsync) { + return func; + } + module[name] = func; +}; + +const overrideAPI = function (module: Record, name: string, pathArgumentIndex?: number | null) { + if (pathArgumentIndex == null) pathArgumentIndex = 0; + const old = module[name]; + module[name] = function (this: any, ...args: any[]) { + const pathArgument = args[pathArgumentIndex!]; + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return old.apply(this, args); + const { asarPath, filePath } = pathInfo; + + const callback = args[args.length - 1]; + if (typeof callback !== 'function') { + return overrideAPISync(module, name, pathArgumentIndex!, true)!.apply(this, args); + } + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); + nextTick(callback, [error]); + return; + } + + const newPath = archive.copyFileOut(filePath); + if (!newPath) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); + nextTick(callback, [error]); + return; + } + + args[pathArgumentIndex!] = newPath; + return old.apply(this, args); + }; + + if (old[util.promisify.custom]) { + module[name][util.promisify.custom] = makePromiseFunction(old[util.promisify.custom], pathArgumentIndex); + } + + if (module.promises && module.promises[name]) { + module.promises[name] = makePromiseFunction(module.promises[name], pathArgumentIndex); + } +}; + +const makePromiseFunction = function (orig: Function, pathArgumentIndex: number) { + return function (this: any, ...args: any[]) { + const pathArgument = args[pathArgumentIndex]; + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return orig.apply(this, args); + const { asarPath, filePath } = pathInfo; + + 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); + }; +}; + +// Override fs APIs. +export const wrapFsWithAsar = (fs: Record) => { + const logFDs: Record = {}; + const logASARAccess = (asarPath: string, filePath: string, offset: number) => { + if (!process.env.ELECTRON_LOG_ASAR_READS) return; + if (!logFDs[asarPath]) { + const path = require('path'); + const logFilename = `${path.basename(asarPath, '.asar')}-access-log.txt`; + const logPath = path.join(require('os').tmpdir(), logFilename); + logFDs[asarPath] = fs.openSync(logPath, 'a'); + } + fs.writeSync(logFDs[asarPath], `${offset}: ${filePath}\n`); + }; + + const { lstatSync } = fs; + fs.lstatSync = (pathArgument: string, options: any) => { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return lstatSync(pathArgument, options); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); + + const stats = archive.stat(filePath); + if (!stats) throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); + + return asarStatsToFsStats(stats); + }; + + const { lstat } = fs; + fs.lstat = function (pathArgument: string, options: any, callback: any) { + const pathInfo = splitPath(pathArgument); + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (!pathInfo.isAsar) return lstat(pathArgument, options, callback); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); + nextTick(callback, [error]); + return; + } + + const stats = archive.stat(filePath); + if (!stats) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); + nextTick(callback, [error]); + return; + } + + const fsStats = asarStatsToFsStats(stats); + nextTick(callback, [null, fsStats]); + }; + + fs.promises.lstat = util.promisify(fs.lstat); + + const { statSync } = fs; + fs.statSync = (pathArgument: string, options: any) => { + const { isAsar } = splitPath(pathArgument); + if (!isAsar) return statSync(pathArgument, options); + + // Do not distinguish links for now. + return fs.lstatSync(pathArgument, options); + }; + + const { stat } = fs; + fs.stat = (pathArgument: string, options: any, callback: any) => { + const { isAsar } = splitPath(pathArgument); + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (!isAsar) return stat(pathArgument, options, callback); + + // Do not distinguish links for now. + process.nextTick(() => fs.lstat(pathArgument, options, callback)); + }; + + fs.promises.stat = util.promisify(fs.stat); + + const wrapRealpathSync = function (realpathSync: Function) { + return function (this: any, pathArgument: string, options: any) { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return realpathSync.apply(this, arguments); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); + } + + const fileRealPath = archive.realpath(filePath); + if (fileRealPath === false) { + throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); + } + + return path.join(realpathSync(asarPath, options), fileRealPath); + }; + }; + + const { realpathSync } = fs; + fs.realpathSync = wrapRealpathSync(realpathSync); + fs.realpathSync.native = wrapRealpathSync(realpathSync.native); + + const wrapRealpath = function (realpath: Function) { + return function (this: any, pathArgument: string, options: any, callback: any) { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return realpath.apply(this, arguments); + const { asarPath, filePath } = pathInfo; + + if (arguments.length < 3) { + callback = options; + options = {}; + } + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); + nextTick(callback, [error]); + return; + } + + const fileRealPath = archive.realpath(filePath); + if (fileRealPath === false) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); + nextTick(callback, [error]); + return; + } + + realpath(asarPath, options, (error: Error | null, archiveRealPath: string) => { + if (error === null) { + const fullPath = path.join(archiveRealPath, fileRealPath); + callback(null, fullPath); + } else { + callback(error); + } + }); + }; + }; + + const { realpath } = fs; + fs.realpath = wrapRealpath(realpath); + fs.realpath.native = wrapRealpath(realpath.native); + + fs.promises.realpath = util.promisify(fs.realpath.native); + + const { exists } = fs; + fs.exists = (pathArgument: string, callback: any) => { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return exists(pathArgument, callback); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); + nextTick(callback, [error]); + return; + } + + const pathExists = (archive.stat(filePath) !== false); + nextTick(callback, [pathExists]); + }; + + fs.exists[util.promisify.custom] = (pathArgument: string) => { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return exists[util.promisify.custom](pathArgument); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); + return Promise.reject(error); + } + + return Promise.resolve(archive.stat(filePath) !== false); + }; + + const { existsSync } = fs; + fs.existsSync = (pathArgument: string) => { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return existsSync(pathArgument); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) return false; + + return archive.stat(filePath) !== false; + }; + + const { access } = fs; + fs.access = function (pathArgument: string, mode: any, callback: any) { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return access.apply(this, arguments); + const { asarPath, filePath } = pathInfo; + + if (typeof mode === 'function') { + callback = mode; + mode = fs.constants.F_OK; + } + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); + nextTick(callback, [error]); + return; + } + + const info = archive.getFileInfo(filePath); + if (!info) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); + nextTick(callback, [error]); + return; + } + + if (info.unpacked) { + const realPath = archive.copyFileOut(filePath); + return fs.access(realPath, mode, callback); + } + + const stats = archive.stat(filePath); + if (!stats) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); + nextTick(callback, [error]); + return; + } + + if (mode & fs.constants.W_OK) { + const error = createError(AsarError.NO_ACCESS, { asarPath, filePath }); + nextTick(callback, [error]); + return; + } + + nextTick(callback); + }; + + fs.promises.access = util.promisify(fs.access); + + const { accessSync } = fs; + fs.accessSync = function (pathArgument: string, mode: any) { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return accessSync.apply(this, arguments); + const { asarPath, filePath } = pathInfo; + + if (mode == null) mode = fs.constants.F_OK; + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); + } + + const info = archive.getFileInfo(filePath); + if (!info) { + throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); + } + + if (info.unpacked) { + const realPath = archive.copyFileOut(filePath); + return fs.accessSync(realPath, mode); + } + + const stats = archive.stat(filePath); + if (!stats) { + throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); + } + + if (mode & fs.constants.W_OK) { + throw createError(AsarError.NO_ACCESS, { asarPath, filePath }); + } + }; + + const { readFile } = fs; + fs.readFile = function (pathArgument: string, options: any, callback: any) { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return readFile.apply(this, arguments); + const { asarPath, filePath } = pathInfo; + + if (typeof options === 'function') { + callback = options; + options = { encoding: null }; + } else if (typeof options === 'string') { + options = { encoding: options }; + } else if (options === null || options === undefined) { + options = { encoding: null }; + } else if (typeof options !== 'object') { + throw new TypeError('Bad arguments'); + } + + const { encoding } = options; + const archive = getOrCreateArchive(asarPath); + if (!archive) { + const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); + nextTick(callback, [error]); + return; + } + + const info = archive.getFileInfo(filePath); + if (!info) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); + nextTick(callback, [error]); + return; + } + + if (info.size === 0) { + nextTick(callback, [null, encoding ? '' : Buffer.alloc(0)]); + return; + } + + if (info.unpacked) { + const realPath = archive.copyFileOut(filePath); + return fs.readFile(realPath, options, callback); + } + + const buffer = Buffer.alloc(info.size); + const fd = archive.getFd(); + if (!(fd >= 0)) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); + nextTick(callback, [error]); + return; + } + + logASARAccess(asarPath, filePath, info.offset); + fs.read(fd, buffer, 0, info.size, info.offset, (error: Error) => { + callback(error, encoding ? buffer.toString(encoding) : buffer); + }); + }; + + fs.promises.readFile = util.promisify(fs.readFile); + + const { readFileSync } = fs; + fs.readFileSync = function (pathArgument: string, options: any) { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return readFileSync.apply(this, arguments); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); + + const info = archive.getFileInfo(filePath); + if (!info) throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); + + if (info.size === 0) return (options) ? '' : Buffer.alloc(0); + if (info.unpacked) { + const realPath = archive.copyFileOut(filePath); + return fs.readFileSync(realPath, options); + } + + if (!options) { + options = { encoding: null }; + } else if (typeof options === 'string') { + options = { encoding: options }; + } else if (typeof options !== 'object') { + throw new TypeError('Bad arguments'); + } + + const { encoding } = options; + const buffer = Buffer.alloc(info.size); + const fd = archive.getFd(); + if (!(fd >= 0)) throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); + + logASARAccess(asarPath, filePath, info.offset); + fs.readSync(fd, buffer, 0, info.size, info.offset); + return (encoding) ? buffer.toString(encoding) : buffer; + }; + + const { readdir } = fs; + fs.readdir = function (pathArgument: string, options: { encoding?: string | null; withFileTypes?: boolean } = {}, callback?: Function) { + const pathInfo = splitPath(pathArgument); + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (!pathInfo.isAsar) return readdir.apply(this, arguments); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); + nextTick(callback!, [error]); + return; + } + + const files = archive.readdir(filePath); + if (!files) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); + nextTick(callback!, [error]); + return; + } + + if (options.withFileTypes) { + const dirents = []; + for (const file of files) { + const stats = archive.stat(file); + if (!stats) { + const error = createError(AsarError.NOT_FOUND, { asarPath, filePath: file }); + nextTick(callback!, [error]); + return; + } + if (stats.isFile) { + dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_FILE)); + } else if (stats.isDirectory) { + dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_DIR)); + } else if (stats.isLink) { + dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_LINK)); + } + } + nextTick(callback!, [null, dirents]); + return; + } + + nextTick(callback!, [null, files]); + }; + + fs.promises.readdir = util.promisify(fs.readdir); + + const { readdirSync } = fs; + fs.readdirSync = function (pathArgument: string, options: { encoding: BufferEncoding | null; withFileTypes?: false } | BufferEncoding | null) { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return readdirSync.apply(this, arguments); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) { + throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); + } + + const files = archive.readdir(filePath); + if (!files) { + throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); + } + + if (options && (options as any).withFileTypes) { + const dirents = []; + for (const file of files) { + const stats = archive.stat(file); + if (!stats) { + throw createError(AsarError.NOT_FOUND, { asarPath, filePath: file }); + } + if (stats.isFile) { + dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_FILE)); + } else if (stats.isDirectory) { + dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_DIR)); + } else if (stats.isLink) { + dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_LINK)); + } + } + return dirents; + } + + return files; + }; + + const { internalModuleReadJSON } = internalBinding('fs'); + internalBinding('fs').internalModuleReadJSON = (pathArgument: string) => { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return internalModuleReadJSON(pathArgument); + const { asarPath, filePath } = pathInfo; + + const archive = getOrCreateArchive(asarPath); + if (!archive) return; + + const info = archive.getFileInfo(filePath); + if (!info) return; + if (info.size === 0) return ''; + if (info.unpacked) { + const realPath = archive.copyFileOut(filePath); + return fs.readFileSync(realPath, { encoding: 'utf8' }); + } + + const buffer = Buffer.alloc(info.size); + const fd = archive.getFd(); + if (!(fd >= 0)) return; + + logASARAccess(asarPath, filePath, info.offset); + fs.readSync(fd, buffer, 0, info.size, info.offset); + return buffer.toString('utf8'); + }; + + const { internalModuleStat } = internalBinding('fs'); + internalBinding('fs').internalModuleStat = (pathArgument: string) => { + const pathInfo = splitPath(pathArgument); + if (!pathInfo.isAsar) return internalModuleStat(pathArgument); + const { asarPath, filePath } = pathInfo; + + // -ENOENT + const archive = getOrCreateArchive(asarPath); + if (!archive) return -34; + + // -ENOENT + const stats = archive.stat(filePath); + if (!stats) return -34; + + return (stats.isDirectory) ? 1 : 0; + }; + + // Calling mkdir for directory inside asar archive should throw ENOTDIR + // error, but on Windows it throws ENOENT. + if (process.platform === 'win32') { + const { mkdir } = fs; + fs.mkdir = (pathArgument: string, options: any, callback: any) => { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + const pathInfo = splitPath(pathArgument); + if (pathInfo.isAsar && pathInfo.filePath.length > 0) { + const error = createError(AsarError.NOT_DIR); + nextTick(callback, [error]); + return; + } + + mkdir(pathArgument, options, callback); + }; + + fs.promises.mkdir = util.promisify(fs.mkdir); + + const { mkdirSync } = fs; + fs.mkdirSync = function (pathArgument: string, options: any) { + const pathInfo = splitPath(pathArgument); + if (pathInfo.isAsar && pathInfo.filePath.length) throw createError(AsarError.NOT_DIR); + return mkdirSync(pathArgument, options); + }; + } + + function invokeWithNoAsar (func: Function) { + return function (this: any) { + const processNoAsarOriginalValue = process.noAsar; + process.noAsar = true; + try { + return func.apply(this, arguments); + } finally { + process.noAsar = processNoAsarOriginalValue; + } + }; + } + + // Strictly implementing the flags of fs.copyFile is hard, just do a simple + // implementation for now. Doing 2 copies won't spend much time more as OS + // has filesystem caching. + overrideAPI(fs, 'copyFile'); + overrideAPISync(fs, 'copyFileSync'); + + overrideAPI(fs, 'open'); + overrideAPISync(process, 'dlopen', 1); + overrideAPISync(Module._extensions, '.node', 1); + overrideAPISync(fs, 'openSync'); + + const overrideChildProcess = (childProcess: Record) => { + // Executing a command string containing a path to an asar archive + // confuses `childProcess.execFile`, which is internally called by + // `childProcess.{exec,execSync}`, causing Electron to consider the full + // command as a single path to an archive. + const { exec, execSync } = childProcess; + childProcess.exec = invokeWithNoAsar(exec); + childProcess.exec[util.promisify.custom] = invokeWithNoAsar(exec[util.promisify.custom]); + childProcess.execSync = invokeWithNoAsar(execSync); + + overrideAPI(childProcess, 'execFile'); + overrideAPISync(childProcess, 'execFileSync'); + }; + + // Lazily override the child_process APIs only when child_process is + // fetched the first time. We will eagerly override the child_process APIs + // when this env var is set so that stack traces generated inside node unit + // tests will match. This env var will only slow things down in users apps + // and should not be used. + if (process.env.ELECTRON_EAGER_ASAR_HOOK_FOR_TESTING) { + overrideChildProcess(require('child_process')); + } else { + const originalModuleLoad = Module._load; + Module._load = (request: string, ...args: any[]) => { + const loadResult = originalModuleLoad(request, ...args); + if (request === 'child_process') { + if (!v8Util.getHiddenValue(loadResult, 'asar-ready')) { + v8Util.setHiddenValue(loadResult, 'asar-ready', true); + // Just to make it obvious what we are dealing with here + const childProcess = loadResult; + + overrideChildProcess(childProcess); + } + } + return loadResult; + }; + } +}; diff --git a/lib/asar/init.ts b/lib/asar/init.ts new file mode 100644 index 000000000000..71ad738e9f9e --- /dev/null +++ b/lib/asar/init.ts @@ -0,0 +1,3 @@ +import { wrapFsWithAsar } from './fs-wrapper'; + +wrapFsWithAsar(require('fs')); diff --git a/lib/common/asar.js b/lib/common/asar.js deleted file mode 100644 index e7dd3d26dced..000000000000 --- a/lib/common/asar.js +++ /dev/null @@ -1,783 +0,0 @@ -'use strict'; - -(function () { - const asar = process._linkedBinding('electron_common_asar'); - const v8Util = process._linkedBinding('electron_common_v8_util'); - const { Buffer } = require('buffer'); - const Module = require('module'); - const path = require('path'); - const util = require('util'); - - const Promise = global.Promise; - - const envNoAsar = process.env.ELECTRON_NO_ASAR && - process.type !== 'browser' && - process.type !== 'renderer'; - const isAsarDisabled = () => process.noAsar || envNoAsar; - - const internalBinding = process.internalBinding; - delete process.internalBinding; - - /** - * @param {!Function} functionToCall - * @param {!Array|undefined} args - */ - const nextTick = (functionToCall, args = []) => { - process.nextTick(() => functionToCall(...args)); - }; - - // Cache asar archive objects. - const cachedArchives = new Map(); - - const getOrCreateArchive = archivePath => { - const isCached = cachedArchives.has(archivePath); - if (isCached) { - return cachedArchives.get(archivePath); - } - - const newArchive = asar.createArchive(archivePath); - if (!newArchive) return null; - - cachedArchives.set(archivePath, newArchive); - return newArchive; - }; - - // Separate asar package's path from full path. - const splitPath = archivePathOrBuffer => { - // Shortcut for disabled asar. - if (isAsarDisabled()) return { isAsar: false }; - - // Check for a bad argument type. - let archivePath = archivePathOrBuffer; - if (Buffer.isBuffer(archivePathOrBuffer)) { - archivePath = archivePathOrBuffer.toString(); - } - if (typeof archivePath !== 'string') return { isAsar: false }; - - return asar.splitPath(path.normalize(archivePath)); - }; - - // Convert asar archive's Stats object to fs's Stats object. - let nextInode = 0; - - const uid = process.getuid != null ? process.getuid() : 0; - const gid = process.getgid != null ? process.getgid() : 0; - - const fakeTime = new Date(); - const msec = (date) => (date || fakeTime).getTime(); - - const asarStatsToFsStats = function (stats) { - const { Stats, constants } = require('fs'); - - let mode = constants.S_IROTH ^ constants.S_IRGRP ^ constants.S_IRUSR ^ constants.S_IWUSR; - - if (stats.isFile) { - mode ^= constants.S_IFREG; - } else if (stats.isDirectory) { - mode ^= constants.S_IFDIR; - } else if (stats.isLink) { - mode ^= constants.S_IFLNK; - } - - return new Stats( - 1, // dev - mode, // mode - 1, // nlink - uid, - gid, - 0, // rdev - undefined, // blksize - ++nextInode, // ino - stats.size, - undefined, // blocks, - msec(stats.atime), // atim_msec - msec(stats.mtime), // mtim_msec - msec(stats.ctime), // ctim_msec - msec(stats.birthtime) // birthtim_msec - ); - }; - - const AsarError = { - NOT_FOUND: 'NOT_FOUND', - NOT_DIR: 'NOT_DIR', - NO_ACCESS: 'NO_ACCESS', - INVALID_ARCHIVE: 'INVALID_ARCHIVE' - }; - - const createError = (errorType, { asarPath, filePath } = {}) => { - let error; - switch (errorType) { - case AsarError.NOT_FOUND: - error = new Error(`ENOENT, ${filePath} not found in ${asarPath}`); - error.code = 'ENOENT'; - error.errno = -2; - break; - case AsarError.NOT_DIR: - error = new Error('ENOTDIR, not a directory'); - error.code = 'ENOTDIR'; - error.errno = -20; - break; - case AsarError.NO_ACCESS: - error = new Error(`EACCES: permission denied, access '${filePath}'`); - error.code = 'EACCES'; - error.errno = -13; - break; - case AsarError.INVALID_ARCHIVE: - error = new Error(`Invalid package ${asarPath}`); - break; - default: - throw new Error(`Invalid error type "${errorType}" passed to createError.`); - } - return error; - }; - - const overrideAPISync = function (module, name, pathArgumentIndex, fromAsync) { - if (pathArgumentIndex == null) pathArgumentIndex = 0; - const old = module[name]; - const func = function () { - const pathArgument = arguments[pathArgumentIndex]; - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return old.apply(this, arguments); - - const archive = getOrCreateArchive(asarPath); - if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); - - const newPath = archive.copyFileOut(filePath); - if (!newPath) throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); - - arguments[pathArgumentIndex] = newPath; - return old.apply(this, arguments); - }; - if (fromAsync) { - return func; - } - module[name] = func; - }; - - const overrideAPI = function (module, name, pathArgumentIndex) { - if (pathArgumentIndex == null) pathArgumentIndex = 0; - const old = module[name]; - module[name] = function () { - const pathArgument = arguments[pathArgumentIndex]; - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return old.apply(this, arguments); - - const callback = arguments[arguments.length - 1]; - if (typeof callback !== 'function') { - return overrideAPISync(module, name, pathArgumentIndex, true).apply(this, arguments); - } - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); - nextTick(callback, [error]); - return; - } - - const newPath = archive.copyFileOut(filePath); - if (!newPath) { - const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - arguments[pathArgumentIndex] = newPath; - return old.apply(this, arguments); - }; - - if (old[util.promisify.custom]) { - module[name][util.promisify.custom] = makePromiseFunction(old[util.promisify.custom], pathArgumentIndex); - } - - if (module.promises && module.promises[name]) { - module.promises[name] = makePromiseFunction(module.promises[name], pathArgumentIndex); - } - }; - - 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); - }; - }; - - // Override fs APIs. - exports.wrapFsWithAsar = fs => { - const logFDs = {}; - const logASARAccess = (asarPath, filePath, offset) => { - if (!process.env.ELECTRON_LOG_ASAR_READS) return; - if (!logFDs[asarPath]) { - const path = require('path'); - const logFilename = `${path.basename(asarPath, '.asar')}-access-log.txt`; - const logPath = path.join(require('os').tmpdir(), logFilename); - logFDs[asarPath] = fs.openSync(logPath, 'a'); - } - fs.writeSync(logFDs[asarPath], `${offset}: ${filePath}\n`); - }; - - const { lstatSync } = fs; - fs.lstatSync = (pathArgument, options) => { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return lstatSync(pathArgument, options); - - const archive = getOrCreateArchive(asarPath); - if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); - - const stats = archive.stat(filePath); - if (!stats) throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); - - return asarStatsToFsStats(stats); - }; - - const { lstat } = fs; - fs.lstat = function (pathArgument, options, callback) { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (typeof options === 'function') { - callback = options; - options = {}; - } - if (!isAsar) return lstat(pathArgument, options, callback); - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); - nextTick(callback, [error]); - return; - } - - const stats = archive.stat(filePath); - if (!stats) { - const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - const fsStats = asarStatsToFsStats(stats); - nextTick(callback, [null, fsStats]); - }; - - fs.promises.lstat = util.promisify(fs.lstat); - - const { statSync } = fs; - fs.statSync = (pathArgument, options) => { - const { isAsar } = splitPath(pathArgument); - if (!isAsar) return statSync(pathArgument, options); - - // Do not distinguish links for now. - return fs.lstatSync(pathArgument, options); - }; - - const { stat } = fs; - fs.stat = (pathArgument, options, callback) => { - const { isAsar } = splitPath(pathArgument); - if (typeof options === 'function') { - callback = options; - options = {}; - } - if (!isAsar) return stat(pathArgument, options, callback); - - // Do not distinguish links for now. - process.nextTick(() => fs.lstat(pathArgument, options, callback)); - }; - - fs.promises.stat = util.promisify(fs.stat); - - const wrapRealpathSync = function (realpathSync) { - return function (pathArgument, options) { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return realpathSync.apply(this, arguments); - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); - } - - const fileRealPath = archive.realpath(filePath); - if (fileRealPath === false) { - throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); - } - - return path.join(realpathSync(asarPath, options), fileRealPath); - }; - }; - - const { realpathSync } = fs; - fs.realpathSync = wrapRealpathSync(realpathSync); - fs.realpathSync.native = wrapRealpathSync(realpathSync.native); - - const wrapRealpath = function (realpath) { - return function (pathArgument, options, callback) { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return realpath.apply(this, arguments); - - if (arguments.length < 3) { - callback = options; - options = {}; - } - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); - nextTick(callback, [error]); - return; - } - - const fileRealPath = archive.realpath(filePath); - if (fileRealPath === false) { - const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - realpath(asarPath, options, (error, archiveRealPath) => { - if (error === null) { - const fullPath = path.join(archiveRealPath, fileRealPath); - callback(null, fullPath); - } else { - callback(error); - } - }); - }; - }; - - const { realpath } = fs; - fs.realpath = wrapRealpath(realpath); - fs.realpath.native = wrapRealpath(realpath.native); - - fs.promises.realpath = util.promisify(fs.realpath.native); - - const { exists } = fs; - fs.exists = (pathArgument, callback) => { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return exists(pathArgument, callback); - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); - nextTick(callback, [error]); - return; - } - - const pathExists = (archive.stat(filePath) !== false); - nextTick(callback, [pathExists]); - }; - - fs.exists[util.promisify.custom] = pathArgument => { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return exists[util.promisify.custom](pathArgument); - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); - return Promise.reject(error); - } - - return Promise.resolve(archive.stat(filePath) !== false); - }; - - const { existsSync } = fs; - fs.existsSync = pathArgument => { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return existsSync(pathArgument); - - const archive = getOrCreateArchive(asarPath); - if (!archive) return false; - - return archive.stat(filePath) !== false; - }; - - const { access } = fs; - fs.access = function (pathArgument, mode, callback) { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return access.apply(this, arguments); - - if (typeof mode === 'function') { - callback = mode; - mode = fs.constants.F_OK; - } - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); - nextTick(callback, [error]); - return; - } - - const info = archive.getFileInfo(filePath); - if (!info) { - const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - if (info.unpacked) { - const realPath = archive.copyFileOut(filePath); - return fs.access(realPath, mode, callback); - } - - const stats = archive.stat(filePath); - if (!stats) { - const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - if (mode & fs.constants.W_OK) { - const error = createError(AsarError.NO_ACCESS, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - nextTick(callback); - }; - - fs.promises.access = util.promisify(fs.access); - - const { accessSync } = fs; - fs.accessSync = function (pathArgument, mode) { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return accessSync.apply(this, arguments); - - if (mode == null) mode = fs.constants.F_OK; - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); - } - - const info = archive.getFileInfo(filePath); - if (!info) { - throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); - } - - if (info.unpacked) { - const realPath = archive.copyFileOut(filePath); - return fs.accessSync(realPath, mode); - } - - const stats = archive.stat(filePath); - if (!stats) { - throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); - } - - if (mode & fs.constants.W_OK) { - throw createError(AsarError.NO_ACCESS, { asarPath, filePath }); - } - }; - - const { readFile } = fs; - fs.readFile = function (pathArgument, options, callback) { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return readFile.apply(this, arguments); - - if (typeof options === 'function') { - callback = options; - options = { encoding: null }; - } else if (typeof options === 'string') { - options = { encoding: options }; - } else if (options === null || options === undefined) { - options = { encoding: null }; - } else if (typeof options !== 'object') { - throw new TypeError('Bad arguments'); - } - - const { encoding } = options; - const archive = getOrCreateArchive(asarPath); - if (!archive) { - const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); - nextTick(callback, [error]); - return; - } - - const info = archive.getFileInfo(filePath); - if (!info) { - const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - if (info.size === 0) { - nextTick(callback, [null, encoding ? '' : Buffer.alloc(0)]); - return; - } - - if (info.unpacked) { - const realPath = archive.copyFileOut(filePath); - return fs.readFile(realPath, options, callback); - } - - const buffer = Buffer.alloc(info.size); - const fd = archive.getFd(); - if (!(fd >= 0)) { - const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - logASARAccess(asarPath, filePath, info.offset); - fs.read(fd, buffer, 0, info.size, info.offset, error => { - callback(error, encoding ? buffer.toString(encoding) : buffer); - }); - }; - - fs.promises.readFile = util.promisify(fs.readFile); - - const { readFileSync } = fs; - fs.readFileSync = function (pathArgument, options) { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return readFileSync.apply(this, arguments); - - const archive = getOrCreateArchive(asarPath); - if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); - - const info = archive.getFileInfo(filePath); - if (!info) throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); - - if (info.size === 0) return (options) ? '' : Buffer.alloc(0); - if (info.unpacked) { - const realPath = archive.copyFileOut(filePath); - return fs.readFileSync(realPath, options); - } - - if (!options) { - options = { encoding: null }; - } else if (typeof options === 'string') { - options = { encoding: options }; - } else if (typeof options !== 'object') { - throw new TypeError('Bad arguments'); - } - - const { encoding } = options; - const buffer = Buffer.alloc(info.size); - const fd = archive.getFd(); - if (!(fd >= 0)) throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); - - logASARAccess(asarPath, filePath, info.offset); - fs.readSync(fd, buffer, 0, info.size, info.offset); - return (encoding) ? buffer.toString(encoding) : buffer; - }; - - const { readdir } = fs; - fs.readdir = function (pathArgument, options = {}, callback) { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (typeof options === 'function') { - callback = options; - options = {}; - } - if (!isAsar) return readdir.apply(this, arguments); - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - const error = createError(AsarError.INVALID_ARCHIVE, { asarPath }); - nextTick(callback, [error]); - return; - } - - const files = archive.readdir(filePath); - if (!files) { - const error = createError(AsarError.NOT_FOUND, { asarPath, filePath }); - nextTick(callback, [error]); - return; - } - - if (options.withFileTypes) { - const dirents = []; - for (const file of files) { - const stats = archive.stat(file); - if (stats.isFile) { - dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_FILE)); - } else if (stats.isDirectory) { - dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_DIR)); - } else if (stats.isLink) { - dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_LINK)); - } - } - nextTick(callback, [null, dirents]); - return; - } - - 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); - if (!isAsar) return readdirSync.apply(this, arguments); - - const archive = getOrCreateArchive(asarPath); - if (!archive) { - throw createError(AsarError.INVALID_ARCHIVE, { asarPath }); - } - - const files = archive.readdir(filePath); - if (!files) { - throw createError(AsarError.NOT_FOUND, { asarPath, filePath }); - } - - if (options.withFileTypes) { - const dirents = []; - for (const file of files) { - const stats = archive.stat(file); - if (stats.isFile) { - dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_FILE)); - } else if (stats.isDirectory) { - dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_DIR)); - } else if (stats.isLink) { - dirents.push(new fs.Dirent(file, fs.constants.UV_DIRENT_LINK)); - } - } - return dirents; - } - - return files; - }; - - const { internalModuleReadJSON } = internalBinding('fs'); - internalBinding('fs').internalModuleReadJSON = pathArgument => { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return internalModuleReadJSON(pathArgument); - - const archive = getOrCreateArchive(asarPath); - if (!archive) return; - - const info = archive.getFileInfo(filePath); - if (!info) return; - if (info.size === 0) return ''; - if (info.unpacked) { - const realPath = archive.copyFileOut(filePath); - return fs.readFileSync(realPath, { encoding: 'utf8' }); - } - - const buffer = Buffer.alloc(info.size); - const fd = archive.getFd(); - if (!(fd >= 0)) return; - - logASARAccess(asarPath, filePath, info.offset); - fs.readSync(fd, buffer, 0, info.size, info.offset); - return buffer.toString('utf8'); - }; - - const { internalModuleStat } = internalBinding('fs'); - internalBinding('fs').internalModuleStat = pathArgument => { - const { isAsar, asarPath, filePath } = splitPath(pathArgument); - if (!isAsar) return internalModuleStat(pathArgument); - - // -ENOENT - const archive = getOrCreateArchive(asarPath); - if (!archive) return -34; - - // -ENOENT - const stats = archive.stat(filePath); - if (!stats) return -34; - - return (stats.isDirectory) ? 1 : 0; - }; - - // Calling mkdir for directory inside asar archive should throw ENOTDIR - // error, but on Windows it throws ENOENT. - if (process.platform === 'win32') { - const { mkdir } = fs; - fs.mkdir = (pathArgument, options, callback) => { - if (typeof options === 'function') { - callback = options; - options = {}; - } - - const { isAsar, filePath } = splitPath(pathArgument); - if (isAsar && filePath.length > 0) { - const error = createError(AsarError.NOT_DIR); - nextTick(callback, [error]); - return; - } - - mkdir(pathArgument, options, callback); - }; - - fs.promises.mkdir = util.promisify(fs.mkdir); - - const { mkdirSync } = fs; - fs.mkdirSync = function (pathArgument, options) { - const { isAsar, filePath } = splitPath(pathArgument); - if (isAsar && filePath.length) throw createError(AsarError.NOT_DIR); - return mkdirSync(pathArgument, options); - }; - } - - function invokeWithNoAsar (func) { - return function () { - const processNoAsarOriginalValue = process.noAsar; - process.noAsar = true; - try { - return func.apply(this, arguments); - } finally { - process.noAsar = processNoAsarOriginalValue; - } - }; - } - - // Strictly implementing the flags of fs.copyFile is hard, just do a simple - // implementation for now. Doing 2 copies won't spend much time more as OS - // has filesystem caching. - overrideAPI(fs, 'copyFile'); - overrideAPISync(fs, 'copyFileSync'); - - overrideAPI(fs, 'open'); - overrideAPISync(process, 'dlopen', 1); - overrideAPISync(Module._extensions, '.node', 1); - overrideAPISync(fs, 'openSync'); - - const overrideChildProcess = (childProcess) => { - // Executing a command string containing a path to an asar archive - // confuses `childProcess.execFile`, which is internally called by - // `childProcess.{exec,execSync}`, causing Electron to consider the full - // command as a single path to an archive. - const { exec, execSync } = childProcess; - childProcess.exec = invokeWithNoAsar(exec); - childProcess.exec[util.promisify.custom] = invokeWithNoAsar(exec[util.promisify.custom]); - childProcess.execSync = invokeWithNoAsar(execSync); - - overrideAPI(childProcess, 'execFile'); - overrideAPISync(childProcess, 'execFileSync'); - }; - - // Lazily override the child_process APIs only when child_process is - // fetched the first time. We will eagerly override the child_process APIs - // when this env var is set so that stack traces generated inside node unit - // tests will match. This env var will only slow things down in users apps - // and should not be used. - if (process.env.ELECTRON_EAGER_ASAR_HOOK_FOR_TESTING) { - overrideChildProcess(require('child_process')); - } else { - const originalModuleLoad = Module._load; - Module._load = (request, ...args) => { - const loadResult = originalModuleLoad(request, ...args); - if (request === 'child_process') { - if (!v8Util.getHiddenValue(loadResult, 'asar-ready')) { - v8Util.setHiddenValue(loadResult, 'asar-ready', true); - // Just to make it obvious what we are dealing with here - const childProcess = loadResult; - - overrideChildProcess(childProcess); - } - } - return loadResult; - }; - } - }; -})(); diff --git a/lib/common/asar_init.js b/lib/common/asar_init.js deleted file mode 100644 index c253fe549c6f..000000000000 --- a/lib/common/asar_init.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -// Monkey-patch the fs module. -require('electron/js2c/asar').wrapFsWithAsar(require('fs')); diff --git a/lib/renderer/webpack-provider.ts b/lib/common/webpack-provider.ts similarity index 82% rename from lib/renderer/webpack-provider.ts rename to lib/common/webpack-provider.ts index 829320cd756e..a54551d32184 100644 --- a/lib/renderer/webpack-provider.ts +++ b/lib/common/webpack-provider.ts @@ -7,7 +7,7 @@ // Rip global off of window (which is also global) so that webpack doesn't // auto replace it with a looped reference to this file -const _global = (self as any || window as any).global as NodeJS.Global; +const _global = typeof globalThis !== 'undefined' ? globalThis.global : (self as any || window as any).global as NodeJS.Global; const process = _global.process; const Buffer = _global.Buffer; diff --git a/script/gen-filenames.js b/script/gen-filenames.js index 4023d67b5d8c..180189954819 100644 --- a/script/gen-filenames.js +++ b/script/gen-filenames.js @@ -36,6 +36,10 @@ const main = async () => { { name: 'worker_bundle_deps', config: 'webpack.config.worker.js' + }, + { + name: 'asar_bundle_deps', + config: 'webpack.config.asar.js' } ]; diff --git a/shell/common/api/electron_api_asar.cc b/shell/common/api/electron_api_asar.cc index c1d5c3bd1e93..7423dcddc7f4 100644 --- a/shell/common/api/electron_api_asar.cc +++ b/shell/common/api/electron_api_asar.cc @@ -117,13 +117,13 @@ class Archive : public gin_helper::Wrappable { }; void InitAsarSupport(v8::Isolate* isolate, v8::Local require) { - // Evaluate asar_init.js. - std::vector> asar_init_params = { + // Evaluate asar_bundle.js. + std::vector> asar_bundle_params = { node::FIXED_ONE_BYTE_STRING(isolate, "require")}; - std::vector> asar_init_args = {require}; - electron::util::CompileAndCall(isolate->GetCurrentContext(), - "electron/js2c/asar_init", &asar_init_params, - &asar_init_args, nullptr); + std::vector> asar_bundle_args = {require}; + electron::util::CompileAndCall( + isolate->GetCurrentContext(), "electron/js2c/asar_bundle", + &asar_bundle_params, &asar_bundle_args, nullptr); } v8::Local SplitPath(v8::Isolate* isolate, diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 654cdb5271cf..ed10f93bb216 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -46,6 +46,42 @@ declare namespace NodeJS { addRemoteObjectRef(contextId: string, id: number): void; } + type AsarFileInfo = { + size: number; + unpacked: boolean; + offset: number; + }; + + type AsarFileStat = { + size: number; + offset: number; + isFile: boolean; + isDirectory: boolean; + isLink: boolean; + } + + interface AsarArchive { + readonly path: string; + getFileInfo(path: string): AsarFileInfo | false; + stat(path: string): AsarFileStat | false; + readdir(path: string): string[] | false; + realpath(path: string): string | false; + copyFileOut(path: string): string | false; + getFd(): number | -1; + } + + interface AsarBinding { + createArchive(path: string): AsarArchive; + splitPath(path: string): { + isAsar: false; + } | { + isAsar: true; + asarPath: string; + filePath: string; + }; + initAsarSupport(require: NodeJS.Require): void; + } + type DataPipe = { write: (buf: Uint8Array) => Promise; done: () => void; @@ -108,6 +144,7 @@ declare namespace NodeJS { net: any; createURLLoader(options: CreateURLLoaderOptions): URLLoader; }; + _linkedBinding(name: 'electron_common_asar'): AsarBinding; log: NodeJS.WriteStream['write']; activateUvLoop(): void;