asar-fs-wrapper.ts 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271
  1. import { Buffer } from 'buffer';
  2. import { Dirent, constants } from 'fs';
  3. import * as path from 'path';
  4. import * as util from 'util';
  5. import type * as Crypto from 'crypto';
  6. import type * as os from 'os';
  7. const asar = process._linkedBinding('electron_common_asar');
  8. const Module = require('module') as NodeJS.ModuleInternal;
  9. const Promise: PromiseConstructor = global.Promise;
  10. const envNoAsar = process.env.ELECTRON_NO_ASAR &&
  11. process.type !== 'browser' &&
  12. process.type !== 'renderer';
  13. const isAsarDisabled = () => process.noAsar || envNoAsar;
  14. const internalBinding = process.internalBinding!;
  15. delete process.internalBinding;
  16. const nextTick = (functionToCall: Function, args: any[] = []) => {
  17. process.nextTick(() => functionToCall(...args));
  18. };
  19. const binding = internalBinding('fs');
  20. // Cache asar archive objects.
  21. const cachedArchives = new Map<string, NodeJS.AsarArchive>();
  22. const getOrCreateArchive = (archivePath: string) => {
  23. const isCached = cachedArchives.has(archivePath);
  24. if (isCached) {
  25. return cachedArchives.get(archivePath)!;
  26. }
  27. try {
  28. const newArchive = new asar.Archive(archivePath);
  29. cachedArchives.set(archivePath, newArchive);
  30. return newArchive;
  31. } catch {
  32. return null;
  33. }
  34. };
  35. process._getOrCreateArchive = getOrCreateArchive;
  36. const asarRe = /\.asar/i;
  37. const {
  38. getValidatedPath,
  39. getOptions,
  40. getDirent
  41. } = __non_webpack_require__('internal/fs/utils');
  42. const {
  43. validateBoolean,
  44. validateFunction
  45. } = __non_webpack_require__('internal/validators');
  46. // In the renderer node internals use the node global URL but we do not set that to be
  47. // the global URL instance. We need to do instanceof checks against the internal URL impl
  48. const { URL: NodeURL } = __non_webpack_require__('internal/url');
  49. // Separate asar package's path from full path.
  50. const splitPath = (archivePathOrBuffer: string | Buffer | URL) => {
  51. // Shortcut for disabled asar.
  52. if (isAsarDisabled()) return { isAsar: <const>false };
  53. // Check for a bad argument type.
  54. let archivePath = archivePathOrBuffer;
  55. if (Buffer.isBuffer(archivePathOrBuffer)) {
  56. archivePath = archivePathOrBuffer.toString();
  57. }
  58. if (archivePath instanceof NodeURL) {
  59. archivePath = getValidatedPath(archivePath);
  60. }
  61. if (typeof archivePath !== 'string') return { isAsar: <const>false };
  62. if (!asarRe.test(archivePath)) return { isAsar: <const>false };
  63. return asar.splitPath(path.normalize(archivePath));
  64. };
  65. // Convert asar archive's Stats object to fs's Stats object.
  66. let nextInode = 0;
  67. const uid = process.getuid?.() ?? 0;
  68. const gid = process.getgid?.() ?? 0;
  69. const fakeTime = new Date();
  70. function getDirents (p: string, { 0: names, 1: types }: any[][]): Dirent[] {
  71. for (let i = 0; i < names.length; i++) {
  72. let type = types[i];
  73. const info = splitPath(path.join(p, names[i]));
  74. if (info.isAsar) {
  75. const archive = getOrCreateArchive(info.asarPath);
  76. if (!archive) continue;
  77. const stats = archive.stat(info.filePath);
  78. if (!stats) continue;
  79. type = stats.type;
  80. }
  81. names[i] = getDirent(p, names[i], type);
  82. }
  83. return names;
  84. }
  85. enum AsarFileType {
  86. kFile = (constants as any).UV_DIRENT_FILE,
  87. kDirectory = (constants as any).UV_DIRENT_DIR,
  88. kLink = (constants as any).UV_DIRENT_LINK,
  89. }
  90. const fileTypeToMode = new Map<AsarFileType, number>([
  91. [AsarFileType.kFile, constants.S_IFREG],
  92. [AsarFileType.kDirectory, constants.S_IFDIR],
  93. [AsarFileType.kLink, constants.S_IFLNK]
  94. ]);
  95. const asarStatsToFsStats = function (stats: NodeJS.AsarFileStat) {
  96. const { Stats } = require('fs');
  97. const mode = constants.S_IROTH | constants.S_IRGRP | constants.S_IRUSR | constants.S_IWUSR | fileTypeToMode.get(stats.type)!;
  98. return new Stats(
  99. 1, // dev
  100. mode, // mode
  101. 1, // nlink
  102. uid,
  103. gid,
  104. 0, // rdev
  105. undefined, // blksize
  106. ++nextInode, // ino
  107. stats.size,
  108. undefined, // blocks,
  109. fakeTime.getTime(), // atim_msec
  110. fakeTime.getTime(), // mtim_msec
  111. fakeTime.getTime(), // ctim_msec
  112. fakeTime.getTime() // birthtim_msec
  113. );
  114. };
  115. const enum AsarError {
  116. NOT_FOUND = 'NOT_FOUND',
  117. NOT_DIR = 'NOT_DIR',
  118. NO_ACCESS = 'NO_ACCESS',
  119. INVALID_ARCHIVE = 'INVALID_ARCHIVE'
  120. }
  121. type AsarErrorObject = Error & { code?: string, errno?: number };
  122. const createError = (errorType: AsarError, { asarPath, filePath }: { asarPath?: string, filePath?: string } = {}) => {
  123. let error: AsarErrorObject;
  124. switch (errorType) {
  125. case AsarError.NOT_FOUND:
  126. error = new Error(`ENOENT, ${filePath} not found in ${asarPath}`);
  127. error.code = 'ENOENT';
  128. error.errno = -2;
  129. break;
  130. case AsarError.NOT_DIR:
  131. error = new Error('ENOTDIR, not a directory');
  132. error.code = 'ENOTDIR';
  133. error.errno = -20;
  134. break;
  135. case AsarError.NO_ACCESS:
  136. error = new Error(`EACCES: permission denied, access '${filePath}'`);
  137. error.code = 'EACCES';
  138. error.errno = -13;
  139. break;
  140. case AsarError.INVALID_ARCHIVE:
  141. error = new Error(`Invalid package ${asarPath}`);
  142. break;
  143. default:
  144. throw new Error(`Invalid error type "${errorType}" passed to createError.`);
  145. }
  146. return error;
  147. };
  148. const overrideAPISync = function (module: Record<string, any>, name: string, pathArgumentIndex?: number | null, fromAsync: boolean = false) {
  149. if (pathArgumentIndex == null) pathArgumentIndex = 0;
  150. const old = module[name];
  151. const func = function (this: any, ...args: any[]) {
  152. const pathArgument = args[pathArgumentIndex!];
  153. const pathInfo = splitPath(pathArgument);
  154. if (!pathInfo.isAsar) return old.apply(this, args);
  155. const { asarPath, filePath } = pathInfo;
  156. const archive = getOrCreateArchive(asarPath);
  157. if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
  158. const newPath = archive.copyFileOut(filePath);
  159. if (!newPath) throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
  160. args[pathArgumentIndex!] = newPath;
  161. return old.apply(this, args);
  162. };
  163. if (fromAsync) {
  164. return func;
  165. }
  166. module[name] = func;
  167. };
  168. const overrideAPI = function (module: Record<string, any>, name: string, pathArgumentIndex?: number | null) {
  169. if (pathArgumentIndex == null) pathArgumentIndex = 0;
  170. const old = module[name];
  171. module[name] = function (this: any, ...args: any[]) {
  172. const pathArgument = args[pathArgumentIndex!];
  173. const pathInfo = splitPath(pathArgument);
  174. if (!pathInfo.isAsar) return old.apply(this, args);
  175. const { asarPath, filePath } = pathInfo;
  176. const callback = args[args.length - 1];
  177. if (typeof callback !== 'function') {
  178. return overrideAPISync(module, name, pathArgumentIndex!, true)!.apply(this, args);
  179. }
  180. const archive = getOrCreateArchive(asarPath);
  181. if (!archive) {
  182. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  183. nextTick(callback, [error]);
  184. return;
  185. }
  186. const newPath = archive.copyFileOut(filePath);
  187. if (!newPath) {
  188. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  189. nextTick(callback, [error]);
  190. return;
  191. }
  192. args[pathArgumentIndex!] = newPath;
  193. return old.apply(this, args);
  194. };
  195. if (old[util.promisify.custom]) {
  196. module[name][util.promisify.custom] = makePromiseFunction(old[util.promisify.custom], pathArgumentIndex);
  197. }
  198. if (module.promises && module.promises[name]) {
  199. module.promises[name] = makePromiseFunction(module.promises[name], pathArgumentIndex);
  200. }
  201. };
  202. let crypto: typeof Crypto;
  203. function validateBufferIntegrity (buffer: Buffer, integrity: NodeJS.AsarFileInfo['integrity']) {
  204. if (!integrity) return;
  205. // Delay load crypto to improve app boot performance
  206. // when integrity protection is not enabled
  207. crypto = crypto || require('crypto');
  208. const actual = crypto.createHash(integrity.algorithm).update(buffer).digest('hex');
  209. if (actual !== integrity.hash) {
  210. console.error(`ASAR Integrity Violation: got a hash mismatch (${actual} vs ${integrity.hash})`);
  211. process.exit(1);
  212. }
  213. }
  214. const makePromiseFunction = function (orig: Function, pathArgumentIndex: number) {
  215. return function (this: any, ...args: any[]) {
  216. const pathArgument = args[pathArgumentIndex];
  217. const pathInfo = splitPath(pathArgument);
  218. if (!pathInfo.isAsar) return orig.apply(this, args);
  219. const { asarPath, filePath } = pathInfo;
  220. const archive = getOrCreateArchive(asarPath);
  221. if (!archive) {
  222. return Promise.reject(createError(AsarError.INVALID_ARCHIVE, { asarPath }));
  223. }
  224. const newPath = archive.copyFileOut(filePath);
  225. if (!newPath) {
  226. return Promise.reject(createError(AsarError.NOT_FOUND, { asarPath, filePath }));
  227. }
  228. args[pathArgumentIndex] = newPath;
  229. return orig.apply(this, args);
  230. };
  231. };
  232. // Override fs APIs.
  233. export const wrapFsWithAsar = (fs: Record<string, any>) => {
  234. const logFDs = new Map<string, number>();
  235. const logASARAccess = (asarPath: string, filePath: string, offset: number) => {
  236. if (!process.env.ELECTRON_LOG_ASAR_READS) return;
  237. if (!logFDs.has(asarPath)) {
  238. const logFilename = `${path.basename(asarPath, '.asar')}-access-log.txt`;
  239. const logPath = path.join((require('os') as typeof os).tmpdir(), logFilename);
  240. logFDs.set(asarPath, fs.openSync(logPath, 'a'));
  241. }
  242. fs.writeSync(logFDs.get(asarPath), `${offset}: ${filePath}\n`);
  243. };
  244. const shouldThrowStatError = (options: any) => {
  245. if (options && typeof options === 'object' && options.throwIfNoEntry === false) {
  246. return false;
  247. }
  248. return true;
  249. };
  250. const { lstatSync } = fs;
  251. fs.lstatSync = (pathArgument: string, options: any) => {
  252. const pathInfo = splitPath(pathArgument);
  253. if (!pathInfo.isAsar) return lstatSync(pathArgument, options);
  254. const { asarPath, filePath } = pathInfo;
  255. const archive = getOrCreateArchive(asarPath);
  256. if (!archive) {
  257. if (shouldThrowStatError(options)) {
  258. throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
  259. };
  260. return null;
  261. }
  262. const stats = archive.stat(filePath);
  263. if (!stats) {
  264. if (shouldThrowStatError(options)) {
  265. throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
  266. };
  267. return null;
  268. }
  269. return asarStatsToFsStats(stats);
  270. };
  271. const { lstat } = fs;
  272. fs.lstat = (pathArgument: string, options: any, callback: any) => {
  273. const pathInfo = splitPath(pathArgument);
  274. if (typeof options === 'function') {
  275. callback = options;
  276. options = {};
  277. }
  278. if (!pathInfo.isAsar) return lstat(pathArgument, options, callback);
  279. const { asarPath, filePath } = pathInfo;
  280. const archive = getOrCreateArchive(asarPath);
  281. if (!archive) {
  282. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  283. nextTick(callback, [error]);
  284. return;
  285. }
  286. const stats = archive.stat(filePath);
  287. if (!stats) {
  288. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  289. nextTick(callback, [error]);
  290. return;
  291. }
  292. const fsStats = asarStatsToFsStats(stats);
  293. nextTick(callback, [null, fsStats]);
  294. };
  295. fs.promises.lstat = util.promisify(fs.lstat);
  296. const { statSync } = fs;
  297. fs.statSync = (pathArgument: string, options: any) => {
  298. const { isAsar } = splitPath(pathArgument);
  299. if (!isAsar) return statSync(pathArgument, options);
  300. // Do not distinguish links for now.
  301. return fs.lstatSync(pathArgument, options);
  302. };
  303. const { stat } = fs;
  304. fs.stat = (pathArgument: string, options: any, callback: any) => {
  305. const { isAsar } = splitPath(pathArgument);
  306. if (typeof options === 'function') {
  307. callback = options;
  308. options = {};
  309. }
  310. if (!isAsar) return stat(pathArgument, options, callback);
  311. // Do not distinguish links for now.
  312. process.nextTick(() => fs.lstat(pathArgument, options, callback));
  313. };
  314. fs.promises.stat = util.promisify(fs.stat);
  315. const wrapRealpathSync = function (realpathSync: Function) {
  316. return function (this: any, pathArgument: string, options: any) {
  317. const pathInfo = splitPath(pathArgument);
  318. if (!pathInfo.isAsar) return realpathSync.apply(this, arguments);
  319. const { asarPath, filePath } = pathInfo;
  320. const archive = getOrCreateArchive(asarPath);
  321. if (!archive) {
  322. throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
  323. }
  324. const fileRealPath = archive.realpath(filePath);
  325. if (fileRealPath === false) {
  326. throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
  327. }
  328. return path.join(realpathSync(asarPath, options), fileRealPath);
  329. };
  330. };
  331. const { realpathSync } = fs;
  332. fs.realpathSync = wrapRealpathSync(realpathSync);
  333. fs.realpathSync.native = wrapRealpathSync(realpathSync.native);
  334. const wrapRealpath = function (realpath: Function) {
  335. return function (this: any, pathArgument: string, options: any, callback: any) {
  336. const pathInfo = splitPath(pathArgument);
  337. if (!pathInfo.isAsar) return realpath.apply(this, arguments);
  338. const { asarPath, filePath } = pathInfo;
  339. if (arguments.length < 3) {
  340. callback = options;
  341. options = {};
  342. }
  343. const archive = getOrCreateArchive(asarPath);
  344. if (!archive) {
  345. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  346. nextTick(callback, [error]);
  347. return;
  348. }
  349. const fileRealPath = archive.realpath(filePath);
  350. if (fileRealPath === false) {
  351. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  352. nextTick(callback, [error]);
  353. return;
  354. }
  355. realpath(asarPath, options, (error: Error | null, archiveRealPath: string) => {
  356. if (error === null) {
  357. const fullPath = path.join(archiveRealPath, fileRealPath);
  358. callback(null, fullPath);
  359. } else {
  360. callback(error);
  361. }
  362. });
  363. };
  364. };
  365. const { realpath } = fs;
  366. fs.realpath = wrapRealpath(realpath);
  367. fs.realpath.native = wrapRealpath(realpath.native);
  368. fs.promises.realpath = util.promisify(fs.realpath.native);
  369. const { exists: nativeExists } = fs;
  370. fs.exists = function exists (pathArgument: string, callback: any) {
  371. let pathInfo: ReturnType<typeof splitPath>;
  372. try {
  373. pathInfo = splitPath(pathArgument);
  374. } catch {
  375. nextTick(callback, [false]);
  376. return;
  377. }
  378. if (!pathInfo.isAsar) return nativeExists(pathArgument, callback);
  379. const { asarPath, filePath } = pathInfo;
  380. const archive = getOrCreateArchive(asarPath);
  381. if (!archive) {
  382. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  383. nextTick(callback, [error]);
  384. return;
  385. }
  386. const pathExists = (archive.stat(filePath) !== false);
  387. nextTick(callback, [pathExists]);
  388. };
  389. fs.exists[util.promisify.custom] = function exists (pathArgument: string) {
  390. const pathInfo = splitPath(pathArgument);
  391. if (!pathInfo.isAsar) return nativeExists[util.promisify.custom](pathArgument);
  392. const { asarPath, filePath } = pathInfo;
  393. const archive = getOrCreateArchive(asarPath);
  394. if (!archive) {
  395. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  396. return Promise.reject(error);
  397. }
  398. return Promise.resolve(archive.stat(filePath) !== false);
  399. };
  400. const { existsSync } = fs;
  401. fs.existsSync = (pathArgument: string) => {
  402. let pathInfo: ReturnType<typeof splitPath>;
  403. try {
  404. pathInfo = splitPath(pathArgument);
  405. } catch {
  406. return false;
  407. }
  408. if (!pathInfo.isAsar) return existsSync(pathArgument);
  409. const { asarPath, filePath } = pathInfo;
  410. const archive = getOrCreateArchive(asarPath);
  411. if (!archive) return false;
  412. return archive.stat(filePath) !== false;
  413. };
  414. const { access } = fs;
  415. fs.access = function (pathArgument: string, mode: number, callback: any) {
  416. const pathInfo = splitPath(pathArgument);
  417. if (!pathInfo.isAsar) return access.apply(this, arguments);
  418. const { asarPath, filePath } = pathInfo;
  419. if (typeof mode === 'function') {
  420. callback = mode;
  421. mode = fs.constants.F_OK;
  422. }
  423. const archive = getOrCreateArchive(asarPath);
  424. if (!archive) {
  425. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  426. nextTick(callback, [error]);
  427. return;
  428. }
  429. const info = archive.getFileInfo(filePath);
  430. if (!info) {
  431. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  432. nextTick(callback, [error]);
  433. return;
  434. }
  435. if (info.unpacked) {
  436. const realPath = archive.copyFileOut(filePath);
  437. return fs.access(realPath, mode, callback);
  438. }
  439. const stats = archive.stat(filePath);
  440. if (!stats) {
  441. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  442. nextTick(callback, [error]);
  443. return;
  444. }
  445. if (mode & fs.constants.W_OK) {
  446. const error = createError(AsarError.NO_ACCESS, { asarPath, filePath });
  447. nextTick(callback, [error]);
  448. return;
  449. }
  450. nextTick(callback);
  451. };
  452. const { access: accessPromise } = fs.promises;
  453. fs.promises.access = function (pathArgument: string, mode: number) {
  454. const pathInfo = splitPath(pathArgument);
  455. if (!pathInfo.isAsar) {
  456. return accessPromise.apply(this, arguments);
  457. }
  458. const p = util.promisify(fs.access);
  459. return p(pathArgument, mode);
  460. };
  461. const { accessSync } = fs;
  462. fs.accessSync = function (pathArgument: string, mode: any) {
  463. const pathInfo = splitPath(pathArgument);
  464. if (!pathInfo.isAsar) return accessSync.apply(this, arguments);
  465. const { asarPath, filePath } = pathInfo;
  466. if (mode == null) mode = fs.constants.F_OK;
  467. const archive = getOrCreateArchive(asarPath);
  468. if (!archive) {
  469. throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
  470. }
  471. const info = archive.getFileInfo(filePath);
  472. if (!info) {
  473. throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
  474. }
  475. if (info.unpacked) {
  476. const realPath = archive.copyFileOut(filePath);
  477. return fs.accessSync(realPath, mode);
  478. }
  479. const stats = archive.stat(filePath);
  480. if (!stats) {
  481. throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
  482. }
  483. if (mode & fs.constants.W_OK) {
  484. throw createError(AsarError.NO_ACCESS, { asarPath, filePath });
  485. }
  486. };
  487. function fsReadFileAsar (pathArgument: string, options: any, callback: any) {
  488. const pathInfo = splitPath(pathArgument);
  489. if (pathInfo.isAsar) {
  490. const { asarPath, filePath } = pathInfo;
  491. if (typeof options === 'function') {
  492. callback = options;
  493. options = { encoding: null };
  494. } else if (typeof options === 'string') {
  495. options = { encoding: options };
  496. } else if (options === null || options === undefined) {
  497. options = { encoding: null };
  498. } else if (typeof options !== 'object') {
  499. throw new TypeError('Bad arguments');
  500. }
  501. const { encoding } = options;
  502. const archive = getOrCreateArchive(asarPath);
  503. if (!archive) {
  504. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  505. nextTick(callback, [error]);
  506. return;
  507. }
  508. const info = archive.getFileInfo(filePath);
  509. if (!info) {
  510. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  511. nextTick(callback, [error]);
  512. return;
  513. }
  514. if (info.size === 0) {
  515. nextTick(callback, [null, encoding ? '' : Buffer.alloc(0)]);
  516. return;
  517. }
  518. if (info.unpacked) {
  519. const realPath = archive.copyFileOut(filePath);
  520. return fs.readFile(realPath, options, callback);
  521. }
  522. const buffer = Buffer.alloc(info.size);
  523. const fd = archive.getFdAndValidateIntegrityLater();
  524. if (!(fd >= 0)) {
  525. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  526. nextTick(callback, [error]);
  527. return;
  528. }
  529. logASARAccess(asarPath, filePath, info.offset);
  530. fs.read(fd, buffer, 0, info.size, info.offset, (error: Error) => {
  531. validateBufferIntegrity(buffer, info.integrity);
  532. callback(error, encoding ? buffer.toString(encoding) : buffer);
  533. });
  534. }
  535. }
  536. const { readFile } = fs;
  537. fs.readFile = function (pathArgument: string, options: any, callback: any) {
  538. const pathInfo = splitPath(pathArgument);
  539. if (!pathInfo.isAsar) {
  540. return readFile.apply(this, arguments);
  541. }
  542. return fsReadFileAsar(pathArgument, options, callback);
  543. };
  544. const { readFile: readFilePromise } = fs.promises;
  545. fs.promises.readFile = function (pathArgument: string, options: any) {
  546. const pathInfo = splitPath(pathArgument);
  547. if (!pathInfo.isAsar) {
  548. return readFilePromise.apply(this, arguments);
  549. }
  550. const p = util.promisify(fsReadFileAsar);
  551. return p(pathArgument, options);
  552. };
  553. const { readFileSync } = fs;
  554. fs.readFileSync = function (pathArgument: string, options: any) {
  555. const pathInfo = splitPath(pathArgument);
  556. if (!pathInfo.isAsar) return readFileSync.apply(this, arguments);
  557. const { asarPath, filePath } = pathInfo;
  558. const archive = getOrCreateArchive(asarPath);
  559. if (!archive) throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
  560. const info = archive.getFileInfo(filePath);
  561. if (!info) throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
  562. if (info.size === 0) return (options) ? '' : Buffer.alloc(0);
  563. if (info.unpacked) {
  564. const realPath = archive.copyFileOut(filePath);
  565. return fs.readFileSync(realPath, options);
  566. }
  567. if (!options) {
  568. options = { encoding: null };
  569. } else if (typeof options === 'string') {
  570. options = { encoding: options };
  571. } else if (typeof options !== 'object') {
  572. throw new TypeError('Bad arguments');
  573. }
  574. const { encoding } = options;
  575. const buffer = Buffer.alloc(info.size);
  576. const fd = archive.getFdAndValidateIntegrityLater();
  577. if (!(fd >= 0)) {
  578. throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
  579. }
  580. logASARAccess(asarPath, filePath, info.offset);
  581. fs.readSync(fd, buffer, 0, info.size, info.offset);
  582. validateBufferIntegrity(buffer, info.integrity);
  583. return (encoding) ? buffer.toString(encoding) : buffer;
  584. };
  585. type ReaddirOptions = { encoding: BufferEncoding | null; withFileTypes?: false, recursive?: false } | undefined | null;
  586. type ReaddirCallback = (err: NodeJS.ErrnoException | null, files?: string[]) => void;
  587. const processReaddirResult = (args: any) => (args.context.withFileTypes ? handleDirents(args) : handleFilePaths(args));
  588. function handleDirents ({ result, currentPath, context }: { result: any[], currentPath: string, context: any }) {
  589. const length = result[0].length;
  590. for (let i = 0; i < length; i++) {
  591. const resultPath = path.join(currentPath, result[0][i]);
  592. const info = splitPath(resultPath);
  593. let type = result[1][i];
  594. if (info.isAsar) {
  595. const archive = getOrCreateArchive(info.asarPath);
  596. if (!archive) return;
  597. const stats = archive.stat(info.filePath);
  598. if (!stats) continue;
  599. type = stats.type;
  600. }
  601. const dirent = getDirent(currentPath, result[0][i], type);
  602. const stat = internalBinding('fs').internalModuleStat(binding, resultPath);
  603. context.readdirResults.push(dirent);
  604. if (dirent.isDirectory() || stat === 1) {
  605. context.pathsQueue.push(path.join(dirent.path, dirent.name));
  606. }
  607. }
  608. }
  609. function handleFilePaths ({ result, currentPath, context }: { result: string[], currentPath: string, context: any }) {
  610. for (let i = 0; i < result.length; i++) {
  611. const resultPath = path.join(currentPath, result[i]);
  612. const relativeResultPath = path.relative(context.basePath, resultPath);
  613. const stat = internalBinding('fs').internalModuleStat(binding, resultPath);
  614. context.readdirResults.push(relativeResultPath);
  615. if (stat === 1) {
  616. context.pathsQueue.push(resultPath);
  617. }
  618. }
  619. }
  620. function readdirRecursive (basePath: string, options: ReaddirOptions, callback: ReaddirCallback) {
  621. const context = {
  622. withFileTypes: Boolean(options!.withFileTypes),
  623. encoding: options!.encoding,
  624. basePath,
  625. readdirResults: [],
  626. pathsQueue: [basePath]
  627. };
  628. let i = 0;
  629. function read (pathArg: string) {
  630. const req = new binding.FSReqCallback();
  631. req.oncomplete = (err: any, result: string) => {
  632. if (err) {
  633. callback(err);
  634. return;
  635. }
  636. if (result === undefined) {
  637. callback(null, context.readdirResults);
  638. return;
  639. }
  640. processReaddirResult({
  641. result,
  642. currentPath: pathArg,
  643. context
  644. });
  645. if (i < context.pathsQueue.length) {
  646. read(context.pathsQueue[i++]);
  647. } else {
  648. callback(null, context.readdirResults);
  649. }
  650. };
  651. const pathInfo = splitPath(pathArg);
  652. if (pathInfo.isAsar) {
  653. let readdirResult;
  654. const { asarPath, filePath } = pathInfo;
  655. const archive = getOrCreateArchive(asarPath);
  656. if (!archive) {
  657. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  658. nextTick(callback, [error]);
  659. return;
  660. }
  661. readdirResult = archive.readdir(filePath);
  662. if (!readdirResult) {
  663. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  664. nextTick(callback, [error]);
  665. return;
  666. }
  667. // If we're in an asar dir, we need to ensure the result is in the same format as the
  668. // native call to readdir withFileTypes i.e. an array of arrays.
  669. if (context.withFileTypes) {
  670. readdirResult = [
  671. [...readdirResult], readdirResult.map((p: string) => {
  672. return internalBinding('fs').internalModuleStat(binding, path.join(pathArg, p));
  673. })
  674. ];
  675. }
  676. processReaddirResult({
  677. result: readdirResult,
  678. currentPath: pathArg,
  679. context
  680. });
  681. if (i < context.pathsQueue.length) {
  682. read(context.pathsQueue[i++]);
  683. } else {
  684. callback(null, context.readdirResults);
  685. }
  686. } else {
  687. binding.readdir(
  688. pathArg,
  689. context.encoding,
  690. context.withFileTypes,
  691. req
  692. );
  693. }
  694. }
  695. read(context.pathsQueue[i++]);
  696. }
  697. const { readdir } = fs;
  698. fs.readdir = function (pathArgument: string, options: ReaddirOptions, callback: ReaddirCallback) {
  699. callback = typeof options === 'function' ? options : callback;
  700. validateFunction(callback, 'callback');
  701. options = getOptions(options);
  702. pathArgument = getValidatedPath(pathArgument);
  703. if (options?.recursive != null) {
  704. validateBoolean(options?.recursive, 'options.recursive');
  705. }
  706. if (options?.recursive) {
  707. readdirRecursive(pathArgument, options, callback);
  708. return;
  709. }
  710. const pathInfo = splitPath(pathArgument);
  711. if (!pathInfo.isAsar) return readdir.apply(this, arguments);
  712. const { asarPath, filePath } = pathInfo;
  713. const archive = getOrCreateArchive(asarPath);
  714. if (!archive) {
  715. const error = createError(AsarError.INVALID_ARCHIVE, { asarPath });
  716. nextTick(callback!, [error]);
  717. return;
  718. }
  719. const files = archive.readdir(filePath);
  720. if (!files) {
  721. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
  722. nextTick(callback!, [error]);
  723. return;
  724. }
  725. if (options?.withFileTypes) {
  726. const dirents = [];
  727. for (const file of files) {
  728. const childPath = path.join(filePath, file);
  729. const stats = archive.stat(childPath);
  730. if (!stats) {
  731. const error = createError(AsarError.NOT_FOUND, { asarPath, filePath: childPath });
  732. nextTick(callback!, [error]);
  733. return;
  734. }
  735. dirents.push(new fs.Dirent(file, stats.type));
  736. }
  737. nextTick(callback!, [null, dirents]);
  738. return;
  739. }
  740. nextTick(callback!, [null, files]);
  741. };
  742. const { readdir: readdirPromise } = fs.promises;
  743. fs.promises.readdir = async function (pathArgument: string, options: ReaddirOptions) {
  744. options = getOptions(options);
  745. pathArgument = getValidatedPath(pathArgument);
  746. if (options?.recursive != null) {
  747. validateBoolean(options?.recursive, 'options.recursive');
  748. }
  749. if (options?.recursive) {
  750. return readdirRecursivePromises(pathArgument, options);
  751. }
  752. const pathInfo = splitPath(pathArgument);
  753. if (!pathInfo.isAsar) return readdirPromise(pathArgument, options);
  754. const { asarPath, filePath } = pathInfo;
  755. const archive = getOrCreateArchive(asarPath);
  756. if (!archive) {
  757. return Promise.reject(createError(AsarError.INVALID_ARCHIVE, { asarPath }));
  758. }
  759. const files = archive.readdir(filePath);
  760. if (!files) {
  761. return Promise.reject(createError(AsarError.NOT_FOUND, { asarPath, filePath }));
  762. }
  763. if (options?.withFileTypes) {
  764. const dirents = [];
  765. for (const file of files) {
  766. const childPath = path.join(filePath, file);
  767. const stats = archive.stat(childPath);
  768. if (!stats) {
  769. throw createError(AsarError.NOT_FOUND, { asarPath, filePath: childPath });
  770. }
  771. dirents.push(new fs.Dirent(file, stats.type));
  772. }
  773. return Promise.resolve(dirents);
  774. }
  775. return Promise.resolve(files);
  776. };
  777. const { readdirSync } = fs;
  778. fs.readdirSync = function (pathArgument: string, options: ReaddirOptions) {
  779. options = getOptions(options);
  780. pathArgument = getValidatedPath(pathArgument);
  781. if (options?.recursive != null) {
  782. validateBoolean(options?.recursive, 'options.recursive');
  783. }
  784. if (options?.recursive) {
  785. return readdirSyncRecursive(pathArgument, options);
  786. }
  787. const pathInfo = splitPath(pathArgument);
  788. if (!pathInfo.isAsar) return readdirSync.apply(this, arguments);
  789. const { asarPath, filePath } = pathInfo;
  790. const archive = getOrCreateArchive(asarPath);
  791. if (!archive) {
  792. throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
  793. }
  794. const files = archive.readdir(filePath);
  795. if (!files) {
  796. throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
  797. }
  798. if (options?.withFileTypes) {
  799. const dirents = [];
  800. for (const file of files) {
  801. const childPath = path.join(filePath, file);
  802. const stats = archive.stat(childPath);
  803. if (!stats) {
  804. throw createError(AsarError.NOT_FOUND, { asarPath, filePath: childPath });
  805. }
  806. dirents.push(new fs.Dirent(file, stats.type));
  807. }
  808. return dirents;
  809. }
  810. return files;
  811. };
  812. const modBinding = internalBinding('modules');
  813. const { readPackageJSON } = modBinding;
  814. internalBinding('modules').readPackageJSON = (
  815. jsonPath: string,
  816. isESM: boolean,
  817. base: undefined | string,
  818. specifier: undefined | string
  819. ) => {
  820. const pathInfo = splitPath(jsonPath);
  821. if (!pathInfo.isAsar) return readPackageJSON(jsonPath, isESM, base, specifier);
  822. const { asarPath, filePath } = pathInfo;
  823. const archive = getOrCreateArchive(asarPath);
  824. if (!archive) return undefined;
  825. const realPath = archive.copyFileOut(filePath);
  826. if (!realPath) return undefined;
  827. return readPackageJSON(realPath, isESM, base, specifier);
  828. };
  829. const { internalModuleStat } = binding;
  830. internalBinding('fs').internalModuleStat = (receiver: unknown, pathArgument: string) => {
  831. const pathInfo = splitPath(pathArgument);
  832. if (!pathInfo.isAsar) return internalModuleStat(receiver, pathArgument);
  833. const { asarPath, filePath } = pathInfo;
  834. // -ENOENT
  835. const archive = getOrCreateArchive(asarPath);
  836. if (!archive) return -34;
  837. // -ENOENT
  838. const stats = archive.stat(filePath);
  839. if (!stats) return -34;
  840. return (stats.type === AsarFileType.kDirectory) ? 1 : 0;
  841. };
  842. const { kUsePromises } = binding;
  843. async function readdirRecursivePromises (originalPath: string, options: ReaddirOptions) {
  844. const result: any[] = [];
  845. const pathInfo = splitPath(originalPath);
  846. let queue: [string, string[]][] = [];
  847. const withFileTypes = Boolean(options?.withFileTypes);
  848. let initialItem = [];
  849. if (pathInfo.isAsar) {
  850. const archive = getOrCreateArchive(pathInfo.asarPath);
  851. if (!archive) return result;
  852. const files = archive.readdir(pathInfo.filePath);
  853. if (!files) return result;
  854. // If we're in an asar dir, we need to ensure the result is in the same format as the
  855. // native call to readdir withFileTypes i.e. an array of arrays.
  856. initialItem = files;
  857. if (withFileTypes) {
  858. initialItem = [
  859. [...initialItem], initialItem.map((p: string) => {
  860. return internalBinding('fs').internalModuleStat(binding, path.join(originalPath, p));
  861. })
  862. ];
  863. }
  864. } else {
  865. initialItem = await binding.readdir(
  866. path.toNamespacedPath(originalPath),
  867. options!.encoding,
  868. withFileTypes,
  869. kUsePromises
  870. );
  871. }
  872. queue = [[originalPath, initialItem]];
  873. if (withFileTypes) {
  874. while (queue.length > 0) {
  875. // @ts-expect-error this is a valid array destructure assignment.
  876. const { 0: pathArg, 1: readDir } = queue.pop();
  877. for (const dirent of getDirents(pathArg, readDir)) {
  878. result.push(dirent);
  879. if (dirent.isDirectory()) {
  880. const direntPath = path.join(pathArg, dirent.name);
  881. const info = splitPath(direntPath);
  882. let readdirResult;
  883. if (info.isAsar) {
  884. const archive = getOrCreateArchive(info.asarPath);
  885. if (!archive) continue;
  886. const files = archive.readdir(info.filePath);
  887. if (!files) continue;
  888. readdirResult = [
  889. [...files], files.map((p: string) => {
  890. return internalBinding('fs').internalModuleStat(binding, path.join(direntPath, p));
  891. })
  892. ];
  893. } else {
  894. readdirResult = await binding.readdir(
  895. direntPath,
  896. options!.encoding,
  897. true,
  898. kUsePromises
  899. );
  900. }
  901. queue.push([direntPath, readdirResult]);
  902. }
  903. }
  904. }
  905. } else {
  906. while (queue.length > 0) {
  907. // @ts-expect-error this is a valid array destructure assignment.
  908. const { 0: pathArg, 1: readDir } = queue.pop();
  909. for (const ent of readDir) {
  910. const direntPath = path.join(pathArg, ent);
  911. const stat = internalBinding('fs').internalModuleStat(binding, direntPath);
  912. result.push(path.relative(originalPath, direntPath));
  913. if (stat === 1) {
  914. const subPathInfo = splitPath(direntPath);
  915. let item = [];
  916. if (subPathInfo.isAsar) {
  917. const archive = getOrCreateArchive(subPathInfo.asarPath);
  918. if (!archive) return;
  919. const files = archive.readdir(subPathInfo.filePath);
  920. if (!files) return result;
  921. item = files;
  922. } else {
  923. item = await binding.readdir(
  924. path.toNamespacedPath(direntPath),
  925. options!.encoding,
  926. false,
  927. kUsePromises
  928. );
  929. }
  930. queue.push([direntPath, item]);
  931. }
  932. }
  933. }
  934. }
  935. return result;
  936. }
  937. function readdirSyncRecursive (basePath: string, options: ReaddirOptions) {
  938. const context = {
  939. withFileTypes: Boolean(options!.withFileTypes),
  940. encoding: options!.encoding,
  941. basePath,
  942. readdirResults: [] as any,
  943. pathsQueue: [basePath]
  944. };
  945. function read (pathArg: string) {
  946. let readdirResult;
  947. const pathInfo = splitPath(pathArg);
  948. if (pathInfo.isAsar) {
  949. const { asarPath, filePath } = pathInfo;
  950. const archive = getOrCreateArchive(asarPath);
  951. if (!archive) return;
  952. readdirResult = archive.readdir(filePath);
  953. if (!readdirResult) return;
  954. // If we're in an asar dir, we need to ensure the result is in the same format as the
  955. // native call to readdir withFileTypes i.e. an array of arrays.
  956. if (context.withFileTypes) {
  957. readdirResult = [
  958. [...readdirResult], readdirResult.map((p: string) => {
  959. return internalBinding('fs').internalModuleStat(binding, path.join(pathArg, p));
  960. })
  961. ];
  962. }
  963. } else {
  964. readdirResult = binding.readdir(
  965. path.toNamespacedPath(pathArg),
  966. context.encoding,
  967. context.withFileTypes
  968. );
  969. }
  970. if (readdirResult === undefined) {
  971. return;
  972. }
  973. processReaddirResult({
  974. result: readdirResult,
  975. currentPath: pathArg,
  976. context
  977. });
  978. }
  979. for (let i = 0; i < context.pathsQueue.length; i++) {
  980. read(context.pathsQueue[i]);
  981. }
  982. return context.readdirResults;
  983. }
  984. // Calling mkdir for directory inside asar archive should throw ENOTDIR
  985. // error, but on Windows it throws ENOENT.
  986. if (process.platform === 'win32') {
  987. const { mkdir } = fs;
  988. fs.mkdir = (pathArgument: string, options: any, callback: any) => {
  989. if (typeof options === 'function') {
  990. callback = options;
  991. options = {};
  992. }
  993. const pathInfo = splitPath(pathArgument);
  994. if (pathInfo.isAsar && pathInfo.filePath.length > 0) {
  995. const error = createError(AsarError.NOT_DIR);
  996. nextTick(callback, [error]);
  997. return;
  998. }
  999. mkdir(pathArgument, options, callback);
  1000. };
  1001. fs.promises.mkdir = util.promisify(fs.mkdir);
  1002. const { mkdirSync } = fs;
  1003. fs.mkdirSync = function (pathArgument: string, options: any) {
  1004. const pathInfo = splitPath(pathArgument);
  1005. if (pathInfo.isAsar && pathInfo.filePath.length) throw createError(AsarError.NOT_DIR);
  1006. return mkdirSync(pathArgument, options);
  1007. };
  1008. }
  1009. function invokeWithNoAsar (func: Function) {
  1010. return function (this: any) {
  1011. const processNoAsarOriginalValue = process.noAsar;
  1012. process.noAsar = true;
  1013. try {
  1014. return func.apply(this, arguments);
  1015. } finally {
  1016. process.noAsar = processNoAsarOriginalValue;
  1017. }
  1018. };
  1019. }
  1020. // Strictly implementing the flags of fs.copyFile is hard, just do a simple
  1021. // implementation for now. Doing 2 copies won't spend much time more as OS
  1022. // has filesystem caching.
  1023. overrideAPI(fs, 'copyFile');
  1024. overrideAPISync(fs, 'copyFileSync');
  1025. overrideAPI(fs, 'open');
  1026. overrideAPISync(process, 'dlopen', 1);
  1027. overrideAPISync(Module._extensions, '.node', 1);
  1028. overrideAPISync(fs, 'openSync');
  1029. const overrideChildProcess = (childProcess: Record<string, any>) => {
  1030. // Executing a command string containing a path to an asar archive
  1031. // confuses `childProcess.execFile`, which is internally called by
  1032. // `childProcess.{exec,execSync}`, causing Electron to consider the full
  1033. // command as a single path to an archive.
  1034. const { exec, execSync } = childProcess;
  1035. childProcess.exec = invokeWithNoAsar(exec);
  1036. childProcess.exec[util.promisify.custom] = invokeWithNoAsar(exec[util.promisify.custom]);
  1037. childProcess.execSync = invokeWithNoAsar(execSync);
  1038. overrideAPI(childProcess, 'execFile');
  1039. overrideAPISync(childProcess, 'execFileSync');
  1040. };
  1041. const asarReady = new WeakSet();
  1042. // Lazily override the child_process APIs only when child_process is
  1043. // fetched the first time. We will eagerly override the child_process APIs
  1044. // when this env var is set so that stack traces generated inside node unit
  1045. // tests will match. This env var will only slow things down in users apps
  1046. // and should not be used.
  1047. if (process.env.ELECTRON_EAGER_ASAR_HOOK_FOR_TESTING) {
  1048. overrideChildProcess(require('child_process'));
  1049. } else {
  1050. const originalModuleLoad = Module._load;
  1051. Module._load = (request: string, ...args: any[]) => {
  1052. const loadResult = originalModuleLoad(request, ...args);
  1053. if (request === 'child_process' || request === 'node:child_process') {
  1054. if (!asarReady.has(loadResult)) {
  1055. asarReady.add(loadResult);
  1056. // Just to make it obvious what we are dealing with here
  1057. const childProcess = loadResult;
  1058. overrideChildProcess(childProcess);
  1059. }
  1060. }
  1061. return loadResult;
  1062. };
  1063. }
  1064. };