From e4df998bb395c53bda51cf3269379a168c42473e Mon Sep 17 00:00:00 2001 From: amsyarasyiq <82711525+amsyarasyiq@users.noreply.github.com> Date: Fri, 10 May 2024 00:20:37 +0800 Subject: [PATCH] feat(caching): cache metro finds, persists across reboots --- build.mjs | 3 +- package.json | 2 + pnpm-lock.yaml | 14 ++++++ src/entry.ts | 19 ++++---- src/lib/metro/caches.ts | 67 +++++++++++++++++++++++++ src/lib/metro/filters.ts | 76 ++++++++++++++++++++--------- src/lib/metro/index.ts | 19 ++------ src/lib/metro/polyfills/redesign.ts | 20 +++++--- src/lib/utils/throttle.ts | 20 ++++++++ 9 files changed, 183 insertions(+), 57 deletions(-) create mode 100644 src/lib/metro/caches.ts create mode 100644 src/lib/utils/throttle.ts diff --git a/build.mjs b/build.mjs index c985652..e9e4df6 100644 --- a/build.mjs +++ b/build.mjs @@ -1,5 +1,6 @@ /* eslint-disable no-restricted-syntax */ // eslint-disable-next-line simple-import-sort/imports +// todo: rewrite aaaaaaaaa import swc from "@swc/core"; import { execSync } from "child_process"; import esbuild from "esbuild"; @@ -122,7 +123,7 @@ try { } } ], - external: ["commit-hash"] + external: [] }); if (shouldWatch) { diff --git a/package.json b/package.json index b2d7207..70ba619 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@types/node": "^20.11.25", "@types/react": "~18.2.0", "@types/react-native": "~0.72.0", + "@types/spark-md5": "^3.0.4", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "@typescript-eslint/typescript-estree": "^7.1.1", @@ -45,6 +46,7 @@ }, "dependencies": { "fuzzysort": "^2.0.4", + "spark-md5": "^3.0.2", "spitroast": "^1.4.4" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9f1c50..8e7b1ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: fuzzysort: specifier: ^2.0.4 version: 2.0.4 + spark-md5: + specifier: ^3.0.2 + version: 3.0.2 spitroast: specifier: ^1.4.4 version: 1.4.4 @@ -37,6 +40,9 @@ devDependencies: '@types/react-native': specifier: ~0.72.0 version: 0.72.3 + '@types/spark-md5': + specifier: ^3.0.4 + version: 3.0.4 '@typescript-eslint/eslint-plugin': specifier: ^7.1.1 version: 7.1.1(@typescript-eslint/parser@7.1.1)(eslint@8.57.0)(typescript@5.4.5) @@ -607,6 +613,10 @@ packages: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} dev: true + /@types/spark-md5@3.0.4: + resolution: {integrity: sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q==} + dev: true + /@typescript-eslint/eslint-plugin@7.1.1(@typescript-eslint/parser@7.1.1)(eslint@8.57.0)(typescript@5.4.5): resolution: {integrity: sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2159,6 +2169,10 @@ packages: engines: {node: '>=8'} dev: true + /spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + dev: false + /spitroast@1.4.4: resolution: {integrity: sha512-S69rzIFjbGoEW5YCLpm1kEdqMEYqC09lmE1ZMY4gyUXfke0wi9qZjKYa0DDSv5eIy/D+/UGJT8yxgJddU3oqlA==} dev: false diff --git a/src/entry.ts b/src/entry.ts index f57c48d..8d515bf 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -4,14 +4,13 @@ import { instead } from "spitroast"; // @ts-ignore - shut up fr globalThis.window = globalThis; -const init = () => { +async function initializeBunny() { try { - // This logs in the native logging implementation, e.g. logcat - console.log("Hello from Bunny! Pyon!"); - // Make 'freeze' and 'seal' do nothing Object.freeze = Object.seal = Object; + await require("@metro/caches").initMetroCache(); + require(".").default(); } catch (e) { const { ClientInfoManager } = require("@lib/api/native/modules"); @@ -25,18 +24,18 @@ const init = () => { stack || e?.toString?.(), ].join("\n")); } -}; +} // @ts-ignore if (typeof globalThis.__r !== "undefined") { - init(); + initializeBunny(); } else { // We hold calls from the native side function onceIndexRequired(originalRequire: any) { const batchedBridge = window.__fbBatchedBridge; const callQueue = new Array; - const unpatch = instead("callFunctionReturnFlushedQueue", batchedBridge, (args, orig) => { + const unpatchHook = instead("callFunctionReturnFlushedQueue", batchedBridge, (args, orig) => { if (args[0] === "AppRegistry" || !batchedBridge.getCallableModule(args[0])) { callQueue.push(args); return batchedBridge.flushedQueue(); @@ -45,9 +44,9 @@ if (typeof globalThis.__r !== "undefined") { return orig.apply(batchedBridge, args); }); - const startDiscord = () => { - init(); - unpatch(); + const startDiscord = async () => { + await initializeBunny(); + unpatchHook(); originalRequire(0); callQueue.forEach(arg => diff --git a/src/lib/metro/caches.ts b/src/lib/metro/caches.ts new file mode 100644 index 0000000..4b6b2c6 --- /dev/null +++ b/src/lib/metro/caches.ts @@ -0,0 +1,67 @@ +import { ClientInfoManager, MMKVManager } from "@lib/api/native/modules"; +import { throttle } from "@lib/utils/throttle"; +import SparkMD5 from "spark-md5"; +import { before } from "spitroast"; + +// fucked types right here +interface MetroCacheStore { + v: 1; + buildNumber: number; + findCache: Record | undefined>; + polyfillCache: Record | undefined>; +} + +const PYON_METRO_CACHE_KEY = "__bunny_metro_cache_key_v1__"; + +let metroCache = null as unknown as MetroCacheStore; + +export function getMetroCache() { + return metroCache; +} +window.getMetroCache = getMetroCache; + +export function getFuncUniqCall() { + const { stack } = new Error(); + return SparkMD5.hash(stack!!); +} + +function buildInitCache() { + return metroCache = { + v: 1, + buildNumber: ClientInfoManager.Build, + findCache: {}, + polyfillCache: {} + } as const; +} + +export async function initMetroCache() { + before("refresh", MMKVManager, ([a]) => void (a.push(PYON_METRO_CACHE_KEY)), true); + + const rawCache = await MMKVManager.getItem(PYON_METRO_CACHE_KEY); + if (rawCache == null) return buildInitCache(); + + try { + metroCache = JSON.parse(rawCache); + if (metroCache.buildNumber !== ClientInfoManager.Build) { + throw "cache invalidated; version mismatch"; + } + } catch { + buildInitCache(); + } +} + +const saveCache = throttle(() => { + MMKVManager.setItem(PYON_METRO_CACHE_KEY, JSON.stringify(metroCache)); +}); + +export function registerModuleFindCacheId(uniqueId: string, moduleId: number, all: boolean) { + (metroCache.findCache[uniqueId] ??= { _: undefined })[moduleId] = 1; + if (all) metroCache.findCache[uniqueId]!._ ||= 1; + saveCache(); +} + +export function registerPolyfillCacheId(name: string, moduleId: number, remove = false) { + if (remove) delete metroCache.polyfillCache[name]?.[moduleId]; + else (metroCache.polyfillCache[name] ??= {})[moduleId] = 1; + saveCache(); +} diff --git a/src/lib/metro/filters.ts b/src/lib/metro/filters.ts index 815ab05..eb6db84 100644 --- a/src/lib/metro/filters.ts +++ b/src/lib/metro/filters.ts @@ -2,16 +2,24 @@ import { instead } from "@lib/api/patcher"; import type { Metro } from "@metro/types"; import { metroRequire, modules } from "@metro/utils"; +import { getFuncUniqCall, getMetroCache, registerModuleFindCacheId } from "./caches"; + export type PropIntellisense

= Record & Record; export type PropsFinder = (...props: T[]) => PropIntellisense; export type PropsFinderAll = (...props: T[]) => PropIntellisense[]; +metroRequire(0); + const blacklistedIds = new Set(); /** Makes the module associated with the specified ID non-enumberable. */ -function blacklistModule(id: Metro.ModuleID) { +function blacklistModule(id: string) { Object.defineProperty(modules, id, { enumerable: false }); - blacklistedIds.add(String(id)); + blacklistedIds.add(id); +} + +function isBadExports(exports: any) { + return !exports || exports === window || exports[""] === null; } const functionToString = Function.prototype.toString; @@ -39,20 +47,15 @@ for (const id in modules) { }; origFunc(...args); - if (moduleObject.exports) onModuleRequire(moduleObject.exports, id); + if (!isBadExports(moduleObject.exports)) { + onModuleRequire(moduleObject.exports, id); + } else { + blacklistModule(id); + } }) as any); // If only spitroast had better types } } -// Blacklist any "bad-actor" modules, e.g. the dreaded null proxy, the window itself, or undefined modules -for (const id in modules) { - if (blacklistedIds.has(id)) continue; - const moduleExports = requireModule(id); - - if (!moduleExports || moduleExports === window || moduleExports["i think pyoncord eol tomorrow idk tho"] === null) - blacklistModule(id); -} - const noopHandler = () => undefined; let patchedInspectSource = false; @@ -101,13 +104,26 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { const prevExports = modules[numberedId - 1]?.publicModule.exports; const inc = prevExports.default?.reactProfilingEnabled ? 1 : -1; if (!modules[numberedId + inc]?.isInitialized) { - blacklistModule(numberedId + inc); + blacklistModule(String(numberedId + inc)); } } + + // Hindi timestamps moment + if (moduleExports.isMoment) { + instead("defineLocale", moduleExports, (args, orig) => { + const origLocale = moduleExports.locale(); + orig(...args); + moduleExports.locale(origLocale); + }); + } } -function requireModule(id: Metro.ModuleID) { - if (modules[id]!.isInitialized) return metroRequire(id); +export function requireModule(id: Metro.ModuleID) { + if (blacklistedIds.has(String(id))) return undefined; + + if (modules[id]?.isInitialized && !modules[id]?.hasError) { + return metroRequire(id); + } // Disable Internal Metro error reporting logic const originalHandler = ErrorUtils.getGlobalHandler(); @@ -117,6 +133,7 @@ function requireModule(id: Metro.ModuleID) { try { moduleExports = metroRequire(id); } catch { + blacklistModule(String(id)); moduleExports = undefined; } @@ -126,11 +143,16 @@ function requireModule(id: Metro.ModuleID) { return moduleExports; } -function* getModuleExports() { - yield require("@metro/polyfills/redesign"); +function* getModules(uniqueId: string, all: boolean) { + yield [-1, require("@metro/polyfills/redesign")]; - for (const id in modules) { - yield requireModule(id); + let cache = getMetroCache().findCache[uniqueId]; + if (all && !cache?._) cache = undefined; + + for (const id in cache ?? modules) { + const exports = requireModule(id); + if (isBadExports(exports)) continue; + yield [id, exports]; } } @@ -148,10 +170,14 @@ function testExports(moduleExports: any, filter: ExportsFilter) { * @param filter find calls filter once for each enumerable module's exports until it finds one where filter returns a thruthy value. */ export function find(filter: ExportsFilter) { - for (const moduleExports of getModuleExports()) { + const uniqueId = getFuncUniqCall(); + + for (const [id, moduleExports] of getModules(uniqueId, false)) { const testedExports = testExports(moduleExports, filter); - if (testedExports !== undefined) + if (testedExports !== undefined) { + if (id !== -1) registerModuleFindCacheId(uniqueId, Number(id), false); return testedExports; + } } } @@ -161,10 +187,14 @@ export function find(filter: ExportsFilter) { */ export function findAll(filter: ExportsFilter) { const foundExports: any[] = []; - for (const moduleExports of getModuleExports()) { + const uniqueId = getFuncUniqCall(); + + for (const [id, moduleExports] of getModules(uniqueId, true)) { const testedExports = testExports(moduleExports, filter); - if (testedExports !== undefined) + if (testedExports !== undefined) { + if (id !== -1) registerModuleFindCacheId(uniqueId, Number(id), true); foundExports.push(testedExports); + } } return foundExports; } diff --git a/src/lib/metro/index.ts b/src/lib/metro/index.ts index 70391bc..d6a7ec6 100644 --- a/src/lib/metro/index.ts +++ b/src/lib/metro/index.ts @@ -1,16 +1,5 @@ -export * from "./utils"; -export { - find, - findAll, - findByDisplayName, - findByDisplayNameAll, - findByName, - findByNameAll, - findByProps, - findByPropsAll, - findByStoreName, - findByTypeName, - findByTypeNameAll -} from "./filters"; -export * as filters from "./filters"; +export * as cache from "./caches"; export * as common from "./common"; +export * from "./filters"; +export * as filters from "./filters"; +export * from "./utils"; diff --git a/src/lib/metro/polyfills/redesign.ts b/src/lib/metro/polyfills/redesign.ts index fa07b6e..0f030bb 100644 --- a/src/lib/metro/polyfills/redesign.ts +++ b/src/lib/metro/polyfills/redesign.ts @@ -1,5 +1,5 @@ -import { metroRequire, modules } from "@metro"; - +import { getMetroCache, registerPolyfillCacheId } from "@lib/metro/caches"; +import { modules, requireModule } from "@metro"; import { LiteralUnion } from "type-fest"; const redesignProps = new Set([ @@ -103,19 +103,23 @@ type Keys = LiteralUnion ? U : string, const redesignPropSource = {} as Record; const redesignModule = {} as Record; -for (const id in modules) { - const moduleExports = metroRequire(id); - polyfillRedesignModule(moduleExports); +const cache = getMetroCache().polyfillCache.redesign_module; +for (const id in cache ?? modules) { + const moduleExports = requireModule(id); + polyfillRedesignModule(moduleExports, Number(id)); } -function polyfillRedesignModule(moduleExports: any) { +function polyfillRedesignModule(moduleExports: any, id: number) { const propMap = new Map(); for (const prop of redesignProps) { - if (moduleExports?.[prop]) + if (moduleExports?.[prop]) { + registerPolyfillCacheId("redesign_module", id); propMap.set(prop, null); - else if (moduleExports?.default?.[prop]) + } else if (moduleExports?.default?.[prop]) { + registerPolyfillCacheId("redesign_module", id); propMap.set(prop, "default"); + } } for (const [prop, defaultKey] of propMap) { diff --git a/src/lib/utils/throttle.ts b/src/lib/utils/throttle.ts new file mode 100644 index 0000000..d40ad8f --- /dev/null +++ b/src/lib/utils/throttle.ts @@ -0,0 +1,20 @@ +/** + * Returns a function that ensures that only one call + * executes every given milliseconds. + * @param funct Function to throttle + * @param time Time to wait before allowing the next call + * @returns Throttled function with the same signature as `funct` + */ +export function throttle any>( + funct: T, + time: number = 200 +): T { + let active = false; + return function (this: ReturnType, ...args: Parameters) { + if (!active) { + active = true; + funct.apply(this, args); + setTimeout(() => { active = false; }, time); + } + } as T; +}