import { Stats } from 'fs' import { getLatestGitRelease } from '../releases' export interface NexeBinary { blobPath: string resources: { [key: string]: number[] } layout: { stat: Stats resourceStart: number contentSize?: number contentStart?: number resourceSize?: number } } let originalFsMethods: any = null let lazyRestoreFs = () => {} // optional Win32 file namespace prefix followed by drive letter and colon const windowsFullPathRegex = /^(\\{2}\?\\)?([a-zA-Z]):/ const upcaseDriveLetter = (s: string): string => s.replace(windowsFullPathRegex, (_match, ns, drive) => `${ns || ''}${drive.toUpperCase()}:`) function shimFs(binary: NexeBinary, fs: any = require('fs')) { if (originalFsMethods !== null) { return } originalFsMethods = Object.assign({}, fs) const { blobPath, resources: manifest } = binary, { resourceStart, stat } = binary.layout, directories: { [key: string]: { [key: string]: boolean } } = {}, notAFile = '!@#$%^&*', isWin = process.platform.startsWith('win'), isString = (x: any): x is string => typeof x === 'string' || x instanceof String, noop = () => {}, path = require('path'), winPath: (key: string) => string = isWin ? upcaseDriveLetter : (s) => s, baseDir = winPath(path.dirname(process.execPath)) let log = (_: string) => true let loggedManifest = false if ((process.env.DEBUG || '').toLowerCase().includes('nexe:require')) { log = (text: string) => { setupManifest() if (!loggedManifest) { process.stderr.write('[nexe] - MANIFEST' + JSON.stringify(manifest, null, 4) + '\n') process.stderr.write('[nexe] - DIRECTORIES' + JSON.stringify(directories, null, 4) + '\n') loggedManifest = true } return process.stderr.write('[nexe] - ' + text + '\n') } } const getKey = function getKey(filepath: string | Buffer | null): string { if (Buffer.isBuffer(filepath)) { filepath = filepath.toString() } if (!isString(filepath)) { return notAFile } let key = path.resolve(baseDir, filepath) return winPath(key) } const statTime = function () { return { atime: new Date(stat.atime), mtime: new Date(stat.mtime), ctime: new Date(stat.ctime), birthtime: new Date(stat.birthtime) } } let BigInt: Function try { BigInt = eval('BigInt') } catch (ignored) {} const minBlocks = Math.max(Math.ceil(stat.blksize / 512), 1) const createStat = function (extensions: any, options: any) { const stat = Object.assign(new fs.Stats(), binary.layout.stat, statTime(), extensions) if ('size' in extensions) { //Assume non adjustable allocation size for file system stat.blocks = Math.ceil(stat.size / stat.blksize) * minBlocks } if (options && options.bigint && typeof BigInt !== 'undefined') { for (const k in stat) { if (Object.prototype.hasOwnProperty.call(stat, k) && typeof stat[k] === 'number') { stat[k] = BigInt(stat[k]) } } } return stat } const ownStat = function (filepath: any, options: any) { setupManifest() const key = getKey(filepath) if (directories[key]) { let mode = binary.layout.stat.mode mode |= fs.constants.S_IFDIR mode &= ~fs.constants.S_IFREG return createStat({ mode, size: 0 }, options) } if (manifest[key]) { return createStat({ size: manifest[key][1] }, options) } } const getStat = function (fn: string) { return function stat(filepath: string | Buffer, options: any, callback: any) { let stat: any if (typeof options === 'function') { callback = options stat = ownStat(filepath, null) } else { stat = ownStat(filepath, options) } if (stat) { process.nextTick(() => { callback(null, stat) }) } else { return originalFsMethods[fn].apply(fs, arguments) } } } function makeLong(filepath: string) { return (path as any)._makeLong && (path as any)._makeLong(filepath) } function fileOpts(options: any) { return !options ? {} : isString(options) ? { encoding: options } : options } let setupManifest = () => { Object.keys(manifest).forEach((filepath) => { const entry = manifest[filepath] const absolutePath = getKey(filepath) const longPath = makeLong(absolutePath) const normalizedPath = winPath(path.normalize(filepath)) if (!manifest[absolutePath]) { manifest[absolutePath] = entry } if (longPath && !manifest[longPath]) { manifest[longPath] = entry } if (!manifest[normalizedPath]) { manifest[normalizedPath] = manifest[filepath] } let currentDir = path.dirname(absolutePath) let prevDir = absolutePath while (currentDir !== prevDir) { directories[currentDir] = directories[currentDir] || {} directories[currentDir][path.basename(prevDir)] = true const longDir = makeLong(currentDir) if (longDir && !directories[longDir]) { directories[longDir] = directories[currentDir] } prevDir = currentDir currentDir = path.dirname(currentDir) } }) ;(manifest[notAFile] as any) = false ;(directories[notAFile] as any) = false setupManifest = noop } //naive patches intended to work for most use cases const nfs: any = { existsSync: function existsSync(filepath: string) { setupManifest() const key = getKey(filepath) if (manifest[key] || directories[key]) { return true } return originalFsMethods.existsSync.apply(fs, arguments) }, realpath: function realpath(filepath: any, options: any, cb: any): void { setupManifest() const key = getKey(filepath) if (isString(filepath) && (manifest[filepath] || manifest[key])) { return process.nextTick(() => cb(null, filepath)) } return originalFsMethods.realpath.call(fs, filepath, options, cb) }, realpathSync: function realpathSync(filepath: any, options: any) { setupManifest() const key = getKey(filepath) if (manifest[key]) { return filepath } return originalFsMethods.realpathSync.call(fs, filepath, options) }, readdir: function readdir(filepath: string | Buffer, options: any, callback: any) { setupManifest() const dir = directories[getKey(filepath)] if (dir) { if ('function' === typeof options) { callback = options options = { encoding: 'utf8' } } process.nextTick(() => callback(null, Object.keys(dir))) } else { return originalFsMethods.readdir.apply(fs, arguments) } }, readdirSync: function readdirSync(filepath: string | Buffer, options: any) { setupManifest() const dir = directories[getKey(filepath)] if (dir) { return Object.keys(dir) } return originalFsMethods.readdirSync.apply(fs, arguments) }, readFile: function readFile(filepath: any, options: any, callback: any) { setupManifest() const entry = manifest[getKey(filepath)] if (!entry) { return originalFsMethods.readFile.apply(fs, arguments) } const [offset, length] = entry const resourceOffset = resourceStart + offset const encoding = fileOpts(options).encoding callback = typeof options === 'function' ? options : callback originalFsMethods.open(blobPath, 'r', function (err: Error, fd: number) { if (err) return callback(err, null) originalFsMethods.read( fd, Buffer.alloc(length), 0, length, resourceOffset, function (error: Error, bytesRead: number, result: Buffer) { if (error) { return originalFsMethods.close(fd, function () { callback(error, null) }) } originalFsMethods.close(fd, function (err: Error) { if (err) { return callback(err, result) } callback(err, encoding ? result.toString(encoding) : result) }) } ) }) }, createReadStream: function createReadStream(filepath: any, options: any) { setupManifest() const entry = manifest[getKey(filepath)] if (!entry) { return originalFsMethods.createReadStream.apply(fs, arguments) } const [offset, length] = entry const resourceOffset = resourceStart + offset const opts = fileOpts(options) return originalFsMethods.createReadStream( blobPath, Object.assign({}, opts, { start: resourceOffset, end: resourceOffset + length - 1, }) ) }, readFileSync: function readFileSync(filepath: any, options: any) { setupManifest() const entry = manifest[getKey(filepath)] if (!entry) { return originalFsMethods.readFileSync.apply(fs, arguments) } const [offset, length] = entry const resourceOffset = resourceStart + offset const encoding = fileOpts(options).encoding const fd = originalFsMethods.openSync(process.execPath, 'r') const result = Buffer.alloc(length) originalFsMethods.readSync(fd, result, 0, length, resourceOffset) originalFsMethods.closeSync(fd) return encoding ? result.toString(encoding) : result }, statSync: function statSync(filepath: string | Buffer, options: any) { const stat = ownStat(filepath, options) if (stat) { return stat } return originalFsMethods.statSync.apply(fs, arguments) }, stat: getStat('stat'), lstat: getStat('lstat'), lstatSync: function statSync(filepath: string | Buffer, options: any) { const stat = ownStat(filepath, options) if (stat) { return stat } return originalFsMethods.lstatSync.apply(fs, arguments) }, } if (typeof fs.exists === 'function') { nfs.exists = function (filepath: string, cb: Function) { cb = cb || noop const exists = nfs.existsSync(filepath) process.nextTick(() => cb(exists)) } } const patches = (process as any).nexe.patches || {} delete (process as any).nexe patches.internalModuleReadFile = function (this: any, original: any, ...args: any[]) { setupManifest() const filepath = getKey(args[0]) if (manifest[filepath]) { log('read (hit) ' + filepath) return nfs.readFileSync(filepath, 'utf-8') } log('read (miss) ' + filepath) return original.call(this, ...args) } let returningArray: boolean patches.internalModuleReadJSON = function (this: any, original: any, ...args: any[]) { if (returningArray == null) returningArray = Array.isArray(original.call(this, '')) const res = patches.internalModuleReadFile.call(this, original, ...args) return returningArray && !Array.isArray(res) ? [res, /"(main|name|type|exports|imports)"/.test(res)] : res } patches.internalModuleStat = function (this: any, original: any, ...args: any[]) { setupManifest() const filepath = getKey(args[0]) if (manifest[filepath]) { log('stat (hit) ' + filepath + ' ' + 0) return 0 } if (directories[filepath]) { log('stat dir (hit) ' + filepath + ' ' + 1) return 1 } const res = original.call(this, ...args) if (res === 0) { log('stat (miss) ' + filepath + ' ' + res) } else if (res === 1) { log('stat dir (miss) ' + filepath + ' ' + res) } else { log('stat (fail) ' + filepath + ' ' + res) } return res } if (typeof fs.exists === 'function') { nfs.exists = function (filepath: string, cb: Function) { cb = cb || noop const exists = nfs.existsSync(filepath) if (!exists) { return originalFsMethods.exists(filepath, cb) } process.nextTick(() => cb(exists)) } } if (typeof fs.copyFile === 'function') { nfs.copyFile = function (filepath: string, dest: string, flags: number, callback: Function) { setupManifest() const entry = manifest[getKey(filepath)] if (!entry) { return originalFsMethods.copyFile.apply(fs, arguments) } if (typeof flags === 'function') { callback = flags flags = 0 } nfs.readFile(filepath, (err: any, buffer: any) => { if (err) { return callback(err) } originalFsMethods.writeFile(dest, buffer, (err: any) => { if (err) { return callback(err) } callback(null) }) }) } nfs.copyFileSync = function (filepath: string, dest: string) { setupManifest() const entry = manifest[getKey(filepath)] if (!entry) { return originalFsMethods.copyFileSync.apply(fs, arguments) } return originalFsMethods.writeFileSync(dest, nfs.readFileSync(filepath)) } } if (typeof fs.realpath.native === 'function') { nfs.realpath.native = function realpathNative(filepath: any, options: any, cb: any): void { setupManifest() const key = getKey(filepath) if (isString(filepath) && (manifest[filepath] || manifest[key])) { return process.nextTick(() => cb(null, filepath)) } return originalFsMethods.realpath.native.call(fs, filepath, options, cb) } nfs.realpathSync.native = function realpathSyncNative(filepath: any, options: any) { setupManifest() const key = getKey(filepath) if (manifest[key]) { return filepath } return originalFsMethods.realpathSync.native.call(fs, filepath, options) } } Object.assign(fs, nfs) lazyRestoreFs = () => { Object.keys(nfs).forEach((key) => { fs[key] = originalFsMethods[key] }) lazyRestoreFs = () => {} } return true } function restoreFs() { lazyRestoreFs() } export { shimFs, restoreFs }