fix: restore compilability and add Bridgeless-mode injection hook
- Fix BunnyLog compile error in Sideloading.x (leftover from Bunny->Revenge rename; only RevengeLog is defined) - Remove duplicated -loadConfig/+defaultConfig methods in LoaderConfig.m - Repoint CI's ipa-patcher download at a live mirror with a pinned checksum (Cristiandis/ipa-patcher was deleted upstream) - Add a second injection hook on RCTInstance/-_loadScriptFromSource: for React Native's Bridgeless mode, alongside the existing RCTCxxBridge hook. Discord's Android build already runs Bridgeless (confirmed via revenge-xposed's hook targets) and revenge-bundle now pins RN 0.81, which defaults to it - so the legacy bridge hook this tweak relied on was very likely dead. Unverified against a real device; see AUDIT.md. See AUDIT.md for the full iOS-vs-Android comparison and remaining gaps.
This commit is contained in:
parent
35ad28b910
commit
dba11af542
7 changed files with 195 additions and 61 deletions
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
|
|
@ -136,16 +136,18 @@ jobs:
|
||||||
|
|
||||||
- name: Run patcher
|
- name: Run patcher
|
||||||
run: |
|
run: |
|
||||||
curl -L -o patcher https://github.com/Cristiandis/ipa-patcher/releases/download/release-pyon/patcher.mac-amd64
|
curl -L -o patcher https://github.com/bunny-mod/ipa-patcher/releases/download/release-pyon/patcher.mac-amd64
|
||||||
|
echo "08a92b45bb00cea91c2d08d4ac48f7ef3f6bf352fe1e3aa204f953ec2db9766e patcher" > patcher.sha256
|
||||||
|
if ! shasum -a 256 -c patcher.sha256; then
|
||||||
|
echo "SHA256 verification failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
chmod +x patcher
|
chmod +x patcher
|
||||||
./patcher -d discord.ipa -o discord.ipa -i ./ipa-icons.zip
|
./patcher -d discord.ipa -o discord.ipa -i ./ipa-icons.zip
|
||||||
|
|
||||||
- name: Install cyan
|
- name: Install cyan
|
||||||
run: pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow
|
run: pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow
|
||||||
|
|
||||||
- name: Install cyan
|
|
||||||
run: pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow
|
|
||||||
|
|
||||||
- name: Inject tweak
|
- name: Inject tweak
|
||||||
run: |
|
run: |
|
||||||
cyan -duws -i discord.ipa -o ${{ github.workspace }}/${{ env.APP_NAME }}.ipa -f ${{ github.workspace }}/${{ env.DEB_FILE_NAME }}
|
cyan -duws -i discord.ipa -o ${{ github.workspace }}/${{ env.APP_NAME }}.ipa -f ${{ github.workspace }}/${{ env.DEB_FILE_NAME }}
|
||||||
|
|
|
||||||
84
AUDIT.md
Normal file
84
AUDIT.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# iOS parity audit (2026-08-03)
|
||||||
|
|
||||||
|
Revenge dropped official iOS support; this repo was archived upstream in Nov 2024
|
||||||
|
(`revenge-mod/revenge-tweak`, last commit `35ad28b`) while the Android loader
|
||||||
|
(`revenge-mod/revenge-xposed`) kept shipping. This audit compares the two and fixes
|
||||||
|
what could be fixed without a jailbroken device to test against.
|
||||||
|
|
||||||
|
## Fixed in this fork
|
||||||
|
|
||||||
|
- **`Sources/Sideloading.x`**: `BunnyLog(...)` referenced a macro that no longer
|
||||||
|
exists (the project was renamed Bunny → Revenge; only `RevengeLog` is defined in
|
||||||
|
`Headers/Logger.h`). This made the file **fail to compile**. Fixed by using
|
||||||
|
`RevengeLog`.
|
||||||
|
- **`Sources/LoaderConfig.m`**: `-loadConfig` and `+defaultConfig` were each defined
|
||||||
|
twice in the same `@implementation` (leftover merge conflict). Removed the
|
||||||
|
duplicates.
|
||||||
|
- **`.github/workflows/deploy.yml`**: the CI patcher step pointed at
|
||||||
|
`Cristiandis/ipa-patcher` release `release-pyon`, which 404s — that repo no longer
|
||||||
|
exists. Repointed at the still-live fork `bunny-mod/ipa-patcher` (same tag,
|
||||||
|
release build) and pinned it with a verified SHA-256 checksum.
|
||||||
|
(These three match the fixes proposed in the never-merged upstream PR #8, which
|
||||||
|
the sole maintainer confirmed worked but never landed before the repo was archived.)
|
||||||
|
|
||||||
|
## The real blocker: this tweak's hook point is very likely dead
|
||||||
|
|
||||||
|
`Sources/Tweak.x` hooks `RCTCxxBridge -executeApplicationScript:url:async:` — the
|
||||||
|
script-injection point for React Native's **legacy bridge**. Evidence points to
|
||||||
|
Discord having moved off that architecture entirely:
|
||||||
|
|
||||||
|
- `revenge-bundle`'s `package.json` pins `react-native: 0.81.4`, which defaults to
|
||||||
|
the New Architecture ("Bridgeless mode").
|
||||||
|
- `revenge-xposed` (the actively-maintained Android loader) hooks
|
||||||
|
`com.facebook.react.runtime.ReactInstance$loadJSBundle$1` — the Bridgeless-mode
|
||||||
|
class — not the legacy `CatalystInstanceImpl`, and its `DiscordDevSupport.kt`
|
||||||
|
explicitly targets `com.facebook.react.devsupport.BridgelessDevSupportManager`.
|
||||||
|
|
||||||
|
Cross-platform RN apps built from one codebase almost always enable/disable the New
|
||||||
|
Architecture together, so if Android Discord is Bridgeless, iOS Discord almost
|
||||||
|
certainly is too — meaning `RCTCxxBridge` is likely never instantiated on current
|
||||||
|
Discord iOS builds, and the tweak's only injection point silently never fires.
|
||||||
|
|
||||||
|
**Fix applied (unverified):** added a second hook, `%hook RCTInstance` /
|
||||||
|
`-_loadScriptFromSource:`, the Bridgeless-mode equivalent, confirmed against React
|
||||||
|
Native's actual upstream source
|
||||||
|
(`ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm`). Both hooks
|
||||||
|
now share one injection pipeline (`runRevengeInjection`, factored out of the old
|
||||||
|
single hook body); Logos skips a `%hook` target gracefully if the class isn't
|
||||||
|
present at runtime, so shipping both is safe regardless of which architecture the
|
||||||
|
installed Discord build actually uses.
|
||||||
|
|
||||||
|
Since `RCTSource` (the type `_loadScriptFromSource:` takes) has no public
|
||||||
|
initializer, `makeRCTSource()` (`Sources/Utils.m`) builds one by poking its private
|
||||||
|
ivars directly — the same technique RN's own private `RCTSourceCreate()` uses
|
||||||
|
internally.
|
||||||
|
|
||||||
|
**This has not been run against a real device or the current Discord iOS binary.**
|
||||||
|
It's a best-effort port grounded in verified upstream RN source and the confirmed
|
||||||
|
Android hook target, not a tested fix. Needs a jailbreak + Xcode/Theos to validate;
|
||||||
|
if `_loadScriptFromSource:` turns out to have a different selector name in Discord's
|
||||||
|
actual build (obfuscation is unlikely for framework code, but app-specific
|
||||||
|
subclasses could differ), it'll need adjusting via class-dump/Frida against the
|
||||||
|
live binary.
|
||||||
|
|
||||||
|
## Remaining feature gap vs. Android (not attempted here)
|
||||||
|
|
||||||
|
Beyond the injection hook, `revenge-xposed` has grown a much larger feature set
|
||||||
|
that iOS has no equivalent of. Porting these is real native engineering work, not
|
||||||
|
something to guess blind without a device to test each step against:
|
||||||
|
|
||||||
|
| Feature | Android | iOS |
|
||||||
|
|---|---|---|
|
||||||
|
| Script injection | `RevengeScriptLoader` + Bridgeless hook | Fixed above (unverified) |
|
||||||
|
| Theme/font/sideloading | `themes`/`fonts`/`sysColors`, resource-ID fix | `Theme.m`/`Fonts.x`/`Sideloading.x` (present, untouched) |
|
||||||
|
| **Native↔JS RPC bridge** | `RevengeBridge` — bidirectional calls smuggled through existing exposed RN native modules (`RNSVGRenderableManager.getBBox`, `FileReaderModule.readAsDataURL`) | **none** |
|
||||||
|
| **Plugin system** | External plugin loading + repos (`PluginLoader`, `RepoStore`, `RepoInstall`, `Resolver`, ...) | **none** |
|
||||||
|
| Internal plugins | Recovery mode, no-track, OTA-update prevention, example plugin | **none** |
|
||||||
|
| Dev support | LogBox + shake-to-recovery wired into RN's dev menu | **none** |
|
||||||
|
| Caches/lifecycle management | `Caches.kt`, `LifecycleSupport.kt` | **none** |
|
||||||
|
|
||||||
|
The bridge + plugin system is the biggest gap and the most architecturally novel
|
||||||
|
part of the Android side — it's what everything else (internal plugins, dev
|
||||||
|
support) is built on. Recommend treating it as its own follow-up project once the
|
||||||
|
injection hook above is confirmed working on-device, rather than attempting it
|
||||||
|
speculatively.
|
||||||
|
|
@ -4,3 +4,10 @@
|
||||||
NSURL *getPyoncordDirectory(void);
|
NSURL *getPyoncordDirectory(void);
|
||||||
UIColor *hexToUIColor(NSString *hex);
|
UIColor *hexToUIColor(NSString *hex);
|
||||||
void showErrorAlert(NSString *title, NSString *message);
|
void showErrorAlert(NSString *title, NSString *message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an `RCTSource` (react/runtime's bridgeless script container) by poking its private
|
||||||
|
* ivars directly, since React Native doesn't expose a public initializer for it.
|
||||||
|
* Returns nil if the `RCTSource` class isn't present in the running binary.
|
||||||
|
*/
|
||||||
|
id makeRCTSource(NSURL *url, NSData *data);
|
||||||
|
|
@ -55,48 +55,6 @@
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)loadConfig {
|
|
||||||
NSURL *loaderConfigUrl = [getPyoncordDirectory() URLByAppendingPathComponent:@"loader.json"];
|
|
||||||
RevengeLog(@"Attempting to load config from: %@", loaderConfigUrl.path);
|
|
||||||
|
|
||||||
if ([[NSFileManager defaultManager] fileExistsAtPath:loaderConfigUrl.path]) {
|
|
||||||
NSError *error = nil;
|
|
||||||
NSData *data = [NSData dataWithContentsOfURL:loaderConfigUrl];
|
|
||||||
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
RevengeLog(@"Error parsing loader config: %@", error);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json) {
|
|
||||||
NSDictionary *customLoadUrl = json[@"customLoadUrl"];
|
|
||||||
if (customLoadUrl) {
|
|
||||||
self.customLoadUrlEnabled = [customLoadUrl[@"enabled"] boolValue];
|
|
||||||
NSString *urlString = customLoadUrl[@"url"];
|
|
||||||
if (urlString) {
|
|
||||||
self.customLoadUrl = [NSURL URLWithString:urlString];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RevengeLog(@"Loader config loaded - Custom URL %@: %@",
|
|
||||||
self.customLoadUrlEnabled ? @"enabled" : @"disabled",
|
|
||||||
self.customLoadUrl.absoluteString);
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RevengeLog(@"Using default loader config: %@", self.customLoadUrl.absoluteString);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (instancetype)defaultConfig {
|
|
||||||
LoaderConfig *config = [[LoaderConfig alloc] init];
|
|
||||||
config.customLoadUrlEnabled = NO;
|
|
||||||
config.customLoadUrl = [NSURL URLWithString:@"http://localhost:4040/revenge.js"];
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (instancetype)getLoaderConfig {
|
+ (instancetype)getLoaderConfig {
|
||||||
RevengeLog(@"Getting loader config");
|
RevengeLog(@"Getting loader config");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ static BOOL isSelfCall(void) {
|
||||||
|
|
||||||
%hook NSFileManager
|
%hook NSFileManager
|
||||||
- (NSURL *)containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier {
|
- (NSURL *)containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier {
|
||||||
BunnyLog(@"containerURLForSecurityApplicationGroupIdentifier called! %@", groupIdentifier ?: @"nil");
|
RevengeLog(@"containerURLForSecurityApplicationGroupIdentifier called! %@", groupIdentifier ?: @"nil");
|
||||||
|
|
||||||
NSArray *paths = [self URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
|
NSArray *paths = [self URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
|
||||||
NSURL *lastPath = [paths lastObject];
|
NSURL *lastPath = [paths lastObject];
|
||||||
|
|
|
||||||
|
|
@ -12,30 +12,33 @@ static NSString *revengePatchesBundlePath;
|
||||||
static NSURL *pyoncordDirectory;
|
static NSURL *pyoncordDirectory;
|
||||||
static LoaderConfig *loaderConfig;
|
static LoaderConfig *loaderConfig;
|
||||||
|
|
||||||
%hook RCTCxxBridge
|
/**
|
||||||
|
* Runs the full Revenge injection sequence (loader payload, downloaded bundle, stored theme,
|
||||||
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async {
|
* fonts, preloads) and hands each script off to `execute`, then finally executes the host's
|
||||||
if (![url.absoluteString containsString:@"main.jsbundle"]) {
|
* own `script`/`url`/`async` once everything else has run.
|
||||||
return %orig;
|
*
|
||||||
}
|
* Factored out so it can be driven by either the legacy-bridge hook (RCTCxxBridge, used when
|
||||||
|
* the host app runs React Native's old bridge) or the bridgeless-mode hook (RCTInstance, used
|
||||||
|
* under React Native's New Architecture) below - both eventually funnel through here.
|
||||||
|
*/
|
||||||
|
static BOOL runRevengeInjection(NSData *script, NSURL *url, BOOL async, void (^execute)(NSData *data, NSURL *url, BOOL async)) {
|
||||||
NSBundle *revengePatchesBundle = [NSBundle bundleWithPath:revengePatchesBundlePath];
|
NSBundle *revengePatchesBundle = [NSBundle bundleWithPath:revengePatchesBundlePath];
|
||||||
if (!revengePatchesBundle) {
|
if (!revengePatchesBundle) {
|
||||||
RevengeLog(@"Failed to load RevengePatches bundle from path: %@", revengePatchesBundlePath);
|
RevengeLog(@"Failed to load RevengePatches bundle from path: %@", revengePatchesBundlePath);
|
||||||
showErrorAlert(@"Loader Error", @"Failed to initialize mod loader. Please reinstall the tweak.");
|
showErrorAlert(@"Loader Error", @"Failed to initialize mod loader. Please reinstall the tweak.");
|
||||||
return %orig;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSURL *patchPath = [revengePatchesBundle URLForResource:@"payload-base" withExtension:@"js"];
|
NSURL *patchPath = [revengePatchesBundle URLForResource:@"payload-base" withExtension:@"js"];
|
||||||
if (!patchPath) {
|
if (!patchPath) {
|
||||||
RevengeLog(@"Failed to find payload-base.js in bundle");
|
RevengeLog(@"Failed to find payload-base.js in bundle");
|
||||||
showErrorAlert(@"Loader Error", @"Failed to initialize mod loader. Please reinstall the tweak.");
|
showErrorAlert(@"Loader Error", @"Failed to initialize mod loader. Please reinstall the tweak.");
|
||||||
return %orig;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSData *patchData = [NSData dataWithContentsOfURL:patchPath];
|
NSData *patchData = [NSData dataWithContentsOfURL:patchPath];
|
||||||
RevengeLog(@"Injecting loader");
|
RevengeLog(@"Injecting loader");
|
||||||
%orig(patchData, source, YES);
|
execute(patchData, source, YES);
|
||||||
|
|
||||||
__block NSData *bundle = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"]];
|
__block NSData *bundle = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"]];
|
||||||
|
|
||||||
|
|
@ -89,7 +92,7 @@ static LoaderConfig *loaderConfig;
|
||||||
error:nil];
|
error:nil];
|
||||||
if (themeString) {
|
if (themeString) {
|
||||||
NSString *jsCode = [NSString stringWithFormat:@"globalThis.__PYON_LOADER__.storedTheme=%@", themeString];
|
NSString *jsCode = [NSString stringWithFormat:@"globalThis.__PYON_LOADER__.storedTheme=%@", themeString];
|
||||||
%orig([jsCode dataUsingEncoding:NSUTF8StringEncoding], source, async);
|
execute([jsCode dataUsingEncoding:NSUTF8StringEncoding], source, async);
|
||||||
}
|
}
|
||||||
|
|
||||||
NSData *fontData = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"fonts.json"]];
|
NSData *fontData = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"fonts.json"]];
|
||||||
|
|
@ -104,7 +107,7 @@ static LoaderConfig *loaderConfig;
|
||||||
|
|
||||||
if (bundle) {
|
if (bundle) {
|
||||||
RevengeLog(@"Executing JS bundle");
|
RevengeLog(@"Executing JS bundle");
|
||||||
%orig(bundle, source, async);
|
execute(bundle, source, async);
|
||||||
}
|
}
|
||||||
|
|
||||||
NSURL *preloadsDirectory = [pyoncordDirectory URLByAppendingPathComponent:@"preloads"];
|
NSURL *preloadsDirectory = [pyoncordDirectory URLByAppendingPathComponent:@"preloads"];
|
||||||
|
|
@ -120,7 +123,7 @@ static LoaderConfig *loaderConfig;
|
||||||
RevengeLog(@"Executing preload JS file %@", fileURL.absoluteString);
|
RevengeLog(@"Executing preload JS file %@", fileURL.absoluteString);
|
||||||
NSData *data = [NSData dataWithContentsOfURL:fileURL];
|
NSData *data = [NSData dataWithContentsOfURL:fileURL];
|
||||||
if (data) {
|
if (data) {
|
||||||
%orig(data, source, async);
|
execute(data, source, async);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,11 +132,59 @@ static LoaderConfig *loaderConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
%hook RCTCxxBridge
|
||||||
|
|
||||||
|
// Legacy-bridge entry point. Fires when the host app runs React Native's old (non-Bridgeless) bridge.
|
||||||
|
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async {
|
||||||
|
if (![url.absoluteString containsString:@"main.jsbundle"]) {
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL injected = runRevengeInjection(script, url, async, ^(NSData *data, NSURL *scriptUrl, BOOL scriptAsync) {
|
||||||
|
%orig(data, scriptUrl, scriptAsync);
|
||||||
|
});
|
||||||
|
if (!injected) return %orig;
|
||||||
|
|
||||||
%orig(script, url, async);
|
%orig(script, url, async);
|
||||||
}
|
}
|
||||||
|
|
||||||
%end
|
%end
|
||||||
|
|
||||||
|
%hook RCTInstance
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridgeless-mode (React Native's New Architecture) entry point. Discord's Android build moved
|
||||||
|
* to this architecture (see RevengeXposed's `com.facebook.react.runtime.ReactInstance$loadJSBundle$1`
|
||||||
|
* and `BridgelessDevSupportManager` hook targets), and the JS bundle now pins react-native 0.81,
|
||||||
|
* which defaults to it too - so this is likely the entry point that actually fires on current
|
||||||
|
* Discord iOS builds, not `RCTCxxBridge` above.
|
||||||
|
*
|
||||||
|
* `RCTInstance` has no public initializer for `RCTSource`, so `makeRCTSource` pokes its private
|
||||||
|
* ivars directly to build synthetic sources for the loader payload/bundle/theme/preload scripts.
|
||||||
|
* UNVERIFIED against a real device/current Discord binary - test before shipping.
|
||||||
|
*/
|
||||||
|
- (void)_loadScriptFromSource:(id)source {
|
||||||
|
NSURL *url = [source valueForKey:@"url"];
|
||||||
|
NSData *script = [source valueForKey:@"data"];
|
||||||
|
|
||||||
|
if (![url.absoluteString containsString:@"main.jsbundle"]) {
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL injected = runRevengeInjection(script, url, YES, ^(NSData *data, NSURL *scriptUrl, BOOL scriptAsync) {
|
||||||
|
id syntheticSource = makeRCTSource(scriptUrl, data);
|
||||||
|
if (syntheticSource) %orig(syntheticSource);
|
||||||
|
});
|
||||||
|
if (!injected) return %orig;
|
||||||
|
|
||||||
|
%orig(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
%end
|
||||||
|
|
||||||
%ctor {
|
%ctor {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
source = [NSURL URLWithString:@"revenge"];
|
source = [NSURL URLWithString:@"revenge"];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,36 @@
|
||||||
#import "Utils.h"
|
#import "Utils.h"
|
||||||
|
#import <objc/runtime.h>
|
||||||
|
|
||||||
|
id makeRCTSource(NSURL *url, NSData *data) {
|
||||||
|
Class RCTSourceClass = NSClassFromString(@"RCTSource");
|
||||||
|
if (!RCTSourceClass) return nil;
|
||||||
|
|
||||||
|
id source = [RCTSourceClass new];
|
||||||
|
|
||||||
|
// RCTSource has no public initializer (see RCTJavaScriptLoader.mm's private
|
||||||
|
// RCTSourceCreate()), so its private ivars are poked directly instead.
|
||||||
|
Ivar urlIvar = class_getInstanceVariable(RCTSourceClass, "_url");
|
||||||
|
Ivar dataIvar = class_getInstanceVariable(RCTSourceClass, "_data");
|
||||||
|
Ivar lengthIvar = class_getInstanceVariable(RCTSourceClass, "_length");
|
||||||
|
Ivar filesChangedCountIvar = class_getInstanceVariable(RCTSourceClass, "_filesChangedCount");
|
||||||
|
|
||||||
|
if (urlIvar) object_setIvar(source, urlIvar, url);
|
||||||
|
if (dataIvar) object_setIvar(source, dataIvar, data);
|
||||||
|
|
||||||
|
// _length and _filesChangedCount are scalars (NSUInteger / NSInteger), not objects,
|
||||||
|
// so they need direct memory writes at the ivar's offset instead of object_setIvar.
|
||||||
|
if (lengthIvar) {
|
||||||
|
ptrdiff_t offset = ivar_getOffset(lengthIvar);
|
||||||
|
*(NSUInteger *)((uint8_t *)(__bridge void *)source + offset) = data.length;
|
||||||
|
}
|
||||||
|
if (filesChangedCountIvar) {
|
||||||
|
ptrdiff_t offset = ivar_getOffset(filesChangedCountIvar);
|
||||||
|
// -2 == RCTSourceFilesChangedCountNotBuiltByBundler, matching sources not built by Metro.
|
||||||
|
*(NSInteger *)((uint8_t *)(__bridge void *)source + offset) = -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
NSURL *getPyoncordDirectory(void) {
|
NSURL *getPyoncordDirectory(void) {
|
||||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue