diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index 1b9f9fa..70e8529 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -2,9 +2,9 @@ import { getCurrentStack } from '@revenge-mod/utils/errors' import { proxify } from '@revenge-mod/utils/proxy' import { cache } from '../caches' import { + mDeps, mImportedPaths, mInitialized, - mMetadata, mUninitialized, } from '../metro/_internal' import { @@ -184,7 +184,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { // Full lookup if (options?.all) { - for (const id of mMetadata.keys()) { + for (let id = 0; id < mDeps.length; id++) { if (cached?.has(id)) continue const exports = getInitializedModuleExports(id) @@ -294,7 +294,7 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) { // Full lookup if (options?.all) { - for (const id of mMetadata.keys()) { + for (let id = 0; id < mDeps.length; id++) { const exports = getInitializedModuleExports(id) const flag = runFilter(filter, id, exports, options) if (flag) diff --git a/lib/modules/src/metro/_internal.ts b/lib/modules/src/metro/_internal.ts index 4760d60..37a42c9 100644 --- a/lib/modules/src/metro/_internal.ts +++ b/lib/modules/src/metro/_internal.ts @@ -2,7 +2,6 @@ import { executeInitializeSubscriptions, executeRequireSubscriptions, } from './subscriptions/_internal' -import { isModuleExportsBad } from './utils' import type { Metro } from '../types' /** Initializing ID */ @@ -13,55 +12,100 @@ export const mUninitialized = new Set() export const mInitialized = new Set() export const mImportedPaths = new Map() -export const mMetadata = new Map() +export const mDeps: Metro.DependencyMap[] = [] -export type Metadata = [ - deps: Metro.DependencyMap, - initialized: boolean, - module?: Metro.Module, -] +export let mList: Metro.ModuleList -export function patchMetroDefine(metroDefine: Metro.DefineFn) { - return ((origFactory, id, deps) => { - // deps won't be undefined last time I checked - const metadata = [deps!, false] satisfies Metadata - mMetadata.set(id, metadata) +/** + * Patching Metro's `__d` function to handle module definitions. + * + * Here's how Metro sets itself up: + * 1. `__METRO_GLOBAL_PREFIX__ = ""` + * 2. `${__METRO_GLOBAL_PREFIX__}__d = function define(...) {}` + * - Why don't we patch it here? Because we need to know the value of __METRO_GLOBAL_PREFIX__ first + * - And since Revenge runs before everything else, we need to patch in the next steps: + * - We chose to patch it in 4. because we can access all the global functions at that point. + * 3. `__r = function metroRequire(...) {}` + * 4. `__c = function clear() {}` + * + * 5. `clear()` + * - Metro clears the module list via clear(), and not __c() which makes it unpatchable. + * 6. `__d(..., 0, [...])` + * - The first module is defined with ID 0, which is the index module. + * - Patch: + * - We clear the module list again, so our module list is in sync with Metro's + * #. `__d` is called with subsequent module definitions + */ +export function patchMetroDefine() { + const defineKey = `${__METRO_GLOBAL_PREFIX__}__d` as const + const metroDefine = globalThis[defineKey] + + // Actual patched define function + const defineRest: Metro.DefineFn = function define(factory, id, deps) { + mDeps[id] = deps! mUninitialized.add(id) metroDefine( - ((global, req, importDefault, importAll, module, exports, deps) => { - handleFactoryCall( - global, - req, - importDefault, - importAll, - module, - exports, - deps!, - id, - metadata, - origFactory, - ) - }) satisfies Metro.FactoryFn, + (_, __, ___, ____, moduleObject) => { + handleFactoryCall(factory, moduleObject) + }, id, deps, ) - }) satisfies Metro.DefineFn + } + + // Patched function for first __d call + globalThis[defineKey] = function define(origFactory, id, deps) { + // Clear the module list so we can keep in sync with Metro's + mList = __c() + + metroImportDefault = metroRequire.importDefault + metroImportAll = metroRequire.importAll + + // Call the actual patched function + defineRest(origFactory, id, deps) + // Set to the actual patched function so we can use it later + globalThis[defineKey] = defineRest + } } +/** + * Returns whether the module has bad exports. If it does, it will be "blacklisted" to avoid filtering issues. + * + * **Which module exports are considered bad?** Anything not an object or function, or an empty object. + * + * @param exports The exports of the module. + */ +export function isModuleExportsBad(exports: Metro.ModuleExports): boolean { + return ( + // Nullish? + exports == null || + // Isn't an object or function? + // - Number exports are not useful, usually just an asset ID + // - String, Boolean, Symbol, BigInt exports are not useful (who would do `module.exports = ...`?) + !( + (exports.__proto__ === Object.prototype && + Reflect.ownKeys(exports).length) || + exports.__proto__ === Function.prototype + ) || + // Can't run isProxy() on this because this isn't your typical proxy: + // https://github.com/facebook/react-native/blob/master/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp + exports === nativeModuleProxy + ) +} + +const global = globalThis +const metroRequire = __r +// We can't destructure here because metroRequire.* is set a little later +let metroImportDefault: Metro.RequireFn +let metroImportAll: Metro.RequireFn + function handleFactoryCall( - global: object, - req: Metro.RequireFn, - importDefault: Metro.RequireFn, - importAll: Metro.RequireFn, - module: Metro.Module, - exports: unknown, - deps: Metro.ModuleID[], - id: Metro.ModuleID, - metadata: Metadata, factory: Metro.FactoryFn, + moduleObject: Metro.Module, ) { - metadata[2] = module + const exports = moduleObject.exports + const id = moduleObject.id! const prevIId = mInitializingId mInitializingId = id @@ -69,15 +113,22 @@ function handleFactoryCall( executeRequireSubscriptions(id) try { - factory(global, req, importDefault, importAll, module, exports, deps) - metadata[1] = true + factory( + global, + metroRequire, + metroImportDefault, + metroImportAll, + moduleObject, + exports, + mDeps[id], + ) // Add the module to the initialized set only if the factory doesn't error or the exports aren't bad // Don't use exports here, as modules can set module.exports to a different object - if (!isModuleExportsBad(module.exports)) mInitialized.add(id) - executeInitializeSubscriptions(id, module.exports) - } catch {} - - mInitializingId = prevIId - mUninitialized.delete(id) + if (!isModuleExportsBad(moduleObject.exports)) mInitialized.add(id) + executeInitializeSubscriptions(id, moduleObject.exports) + } finally { + mUninitialized.delete(mInitializingId) + mInitializingId = prevIId + } } diff --git a/lib/modules/src/metro/utils.ts b/lib/modules/src/metro/utils.ts index 02c37ba..dc48f6d 100644 --- a/lib/modules/src/metro/utils.ts +++ b/lib/modules/src/metro/utils.ts @@ -1,5 +1,5 @@ import { isProxy } from '@revenge-mod/utils/proxy' -import { mMetadata } from './_internal' +import { mDeps, mList } from './_internal' import type { Metro } from '../types' /** @@ -10,7 +10,7 @@ import type { Metro } from '../types' export function getModuleDependencies( id: Metro.ModuleID, ): Metro.DependencyMap | undefined { - return mMetadata.get(id)?.[0] + return mDeps[id] } /** @@ -19,7 +19,7 @@ export function getModuleDependencies( * @returns `true` if the module is initialized, `false` if it is not initialized, or `undefined` if the module does not exist. */ export function isModuleInitialized(id: Metro.ModuleID): boolean | undefined { - return mMetadata.get(id)?.[1] + return mList.get(id)?.isInitialized } /** @@ -33,7 +33,7 @@ export function isModuleInitialized(id: Metro.ModuleID): boolean | undefined { export function getInitializedModuleExports( id: Metro.ModuleID, ): Metro.ModuleExports | undefined { - return mMetadata.get(id)?.[2]?.exports + return mList.get(id)?.publicModule?.exports } /** @@ -53,27 +53,4 @@ export function isModuleExportBad( ) } -/** - * Returns whether the module has bad exports. If it does, it will be "blacklisted" to avoid filtering issues. - * - * **Which module exports are considered bad?** Anything not an object or function, or an empty object. - * - * @param exports The exports of the module. - */ -export function isModuleExportsBad(exports: Metro.ModuleExports): boolean { - return ( - // Nullish? - exports == null || - // Isn't an object or function? - // - Number exports are not useful, usually just an asset ID - // - String, Boolean, Symbol, BigInt exports are not useful (who would do `module.exports = ...`?) - !( - (exports.__proto__ === Object.prototype && - Reflect.ownKeys(exports).length) || - exports.__proto__ === Function.prototype - ) || - // Can't run isProxy() on this because this isn't your typical proxy: - // https://github.com/facebook/react-native/blob/master/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp - exports === nativeModuleProxy - ) -} +export { isModuleExportsBad } from './_internal' diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 042c089..25e92b4 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -69,9 +69,14 @@ declare global { /// METRO declare global { - var __r: Metro.RequireFn - var __c: Metro.ClearFn + var __METRO_GLOBAL_PREFIX__: '' + var __d: Metro.DefineFn + var __r: Metro.RequireFn & { + importDefault: Metro.ImportDefaultFn + importAll: Metro.ImportAllFn + } + var __c: Metro.ClearFn } /// REACT NATIVE diff --git a/lib/utils/package.json b/lib/utils/package.json index 7557311..d6a8f6e 100644 --- a/lib/utils/package.json +++ b/lib/utils/package.json @@ -3,6 +3,12 @@ "private": true, "type": "module", "exports": { + "./patches/objects": { + "default": "./src/patches/objects.ts" + }, + "./patches/proxy": { + "default": "./src/patches/proxy.ts" + }, "./callbacks": { "default": "./src/callbacks.ts" }, @@ -34,4 +40,4 @@ "types": "./src/types.ts" } } -} +} \ No newline at end of file diff --git a/lib/utils/src/objects.ts b/lib/utils/src/objects.ts index 08c5376..860c564 100644 --- a/lib/utils/src/objects.ts +++ b/lib/utils/src/objects.ts @@ -1,12 +1,6 @@ +import { objectDefineProperty } from './patches/objects' import type { AnyObject } from './types' -// Retain reference to original functions -export const objectFreeze = Object.freeze -export const objectDefineProperty = Object.defineProperty - -// Patch to prevent the use of Object.freeze -Object.freeze = (o: any) => o - // Heavily inspired by uwu/shelter // https://github.com/uwu/shelter/blob/main/packages/shelter/src/exfiltrate.ts diff --git a/lib/utils/src/patches/objects.ts b/lib/utils/src/patches/objects.ts new file mode 100644 index 0000000..5e48a35 --- /dev/null +++ b/lib/utils/src/patches/objects.ts @@ -0,0 +1,6 @@ +// Retain reference to original functions +export const objectFreeze = Object.freeze +export const objectDefineProperty = Object.defineProperty + +// Patch to prevent the use of Object.freeze +Object.freeze = (o: any) => o diff --git a/lib/utils/src/patches/proxy.ts b/lib/utils/src/patches/proxy.ts new file mode 100644 index 0000000..636b3c3 --- /dev/null +++ b/lib/utils/src/patches/proxy.ts @@ -0,0 +1,10 @@ +export const _targets = new WeakMap() + +const OriginalProxy = globalThis.Proxy +globalThis.Proxy = new Proxy(OriginalProxy, { + construct(_target, args) { + const prox = new OriginalProxy(args[0], args[1]) + _targets.set(prox, args[0]) + return prox + }, +}) diff --git a/lib/utils/src/proxy.ts b/lib/utils/src/proxy.ts index 2745a57..31e806c 100644 --- a/lib/utils/src/proxy.ts +++ b/lib/utils/src/proxy.ts @@ -4,17 +4,7 @@ */ import { getCurrentStack } from './errors' - -const _targets = new WeakMap() - -const OriginalProxy = globalThis.Proxy -globalThis.Proxy = new Proxy(OriginalProxy, { - construct(_target, args) { - const prox = new OriginalProxy(args[0], args[1]) - _targets.set(prox, args[0]) - return prox - }, -}) +import { _targets } from './patches/proxy' /** * Returns whether the object is a proxy. diff --git a/src/index.ts b/src/index.ts index d97e0e8..75fdf5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,71 +1,18 @@ -// Note that both Object.freeze and Proxy patches are implicitly run by requiring the two libraries below. -import { patchMetroDefine } from '@revenge-mod/modules/_/metro' -import { interceptProperty } from '@revenge-mod/utils/objects' +import type { Metro } from '@revenge-mod/modules/types' -// By doing this, we are assuming __METRO_GLOBAL_PREFIX__ is an empty string and globalThis.__d isn't already set. -// But both should be true either way. -const unintercept = interceptProperty('__d', (_, mDefine) => { - unintercept() - return patchMetroDefine(mDefine) +Object.defineProperty(globalThis, '__c', { + configurable: true, + set(clear: Metro.ClearFn) { + // @ts-expect-error + // biome-ignore lint/performance/noDelete: We need to reset the property descriptor + delete globalThis.__c + globalThis.__c = clear + + const { patchMetroDefine } = + require('@revenge-mod/modules/_/metro') as typeof import('@revenge-mod/modules/_/metro') + + patchMetroDefine() + + require('./preinit') + }, }) - -import { - onModuleFirstRequired, - onModuleInitialized, -} from '@revenge-mod/modules/metro/subscriptions' -import { getErrorStack } from '@revenge-mod/utils/errors' - -const IndexModuleId = 0 - -onModuleFirstRequired(IndexModuleId, function onIndexRequired() { - try { - if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__) - require('./patches/log-promise-rejections') - - // Initialize preinit libraries - require('@revenge-mod/modules/preinit') - require('@revenge-mod/react/preinit') - require('@revenge-mod/assets/preinit') - require('@revenge-mod/storage/preinit') - require('@revenge-mod/discord/preinit') - - // Run all preinit plugins - require('~/plugins/preinit') - require('@revenge-mod/plugins/preinit') - - onModuleInitialized(IndexModuleId, function onIndexInitialized() { - try { - // Initialize init libraries - require('@revenge-mod/storage/init') - require('@revenge-mod/externals/init') - require('@revenge-mod/discord/init') - require('@revenge-mod/components/init') - - // Run all init plugins - require('~/plugins/init') - require('@revenge-mod/plugins/init') - } catch (e) { - onError(e) - } - }) - } catch (e) { - onError(e) - } -}) - -function onError(e: unknown) { - // TODO(init): Move to use native provided alert function, which will accept a string stack trace - // Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further - const { ClientInfoModule, DeviceModule } = - require('@revenge-mod/discord/native') as typeof import('@revenge-mod/discord/native') - - const Client = ClientInfoModule.getConstants() - const Device = DeviceModule.getConstants() - - alert( - `Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` + - `Discord: ${Client.Version} (${Client.Build})\n` + - `Device: ${Device.deviceManufacturer} ${Device.deviceModel}\n\n` + - getErrorStack(e), - ) -} diff --git a/src/preinit.ts b/src/preinit.ts new file mode 100644 index 0000000..136ea65 --- /dev/null +++ b/src/preinit.ts @@ -0,0 +1,63 @@ +import '@revenge-mod/utils/patches/proxy' +import '@revenge-mod/utils/patches/objects' + +import { + onModuleFirstRequired, + onModuleInitialized, +} from '@revenge-mod/modules/metro/subscriptions' +import { getErrorStack } from '@revenge-mod/utils/errors' + +const IndexModuleId = 0 + +onModuleFirstRequired(IndexModuleId, function onIndexRequired() { + try { + if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__) + require('./patches/log-promise-rejections') + + // Initialize preinit libraries + require('@revenge-mod/modules/preinit') + require('@revenge-mod/react/preinit') + require('@revenge-mod/assets/preinit') + require('@revenge-mod/storage/preinit') + require('@revenge-mod/discord/preinit') + + // Run all preinit plugins + require('~/plugins/preinit') + require('@revenge-mod/plugins/preinit') + + onModuleInitialized(IndexModuleId, function onIndexInitialized() { + try { + // Initialize init libraries + require('@revenge-mod/storage/init') + require('@revenge-mod/externals/init') + require('@revenge-mod/discord/init') + require('@revenge-mod/components/init') + + // Run all init plugins + require('~/plugins/init') + require('@revenge-mod/plugins/init') + } catch (e) { + onError(e) + } + }) + } catch (e) { + onError(e) + } +}) + +function onError(e: unknown) { + // TODO(init): Move to use native provided alert function, which will accept a string stack trace + // Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further + const { ClientInfoModule, DeviceModule } = + require('@revenge-mod/discord/native') as typeof import('@revenge-mod/discord/native') + + const Client = ClientInfoModule.getConstants() + const Device = DeviceModule.getConstants() + + alert( + `Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` + + `Discord: ${Client.Version} (${Client.Build})\n` + + `Device: ${Device.deviceManufacturer} ${Device.deviceModel}\n\n` + + getErrorStack(e), + ) +}