Merge pull request #1 from everestmcarthur/ios-parity-audit
Ios parity audit
This commit is contained in:
commit
c452e52809
14 changed files with 2676 additions and 64 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 }}
|
||||||
|
|
|
||||||
101
AUDIT.md
Normal file
101
AUDIT.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
**This is now confirmed, not just theorized.** [RainTweak](https://codeberg.org/raincord/RainTweak)
|
||||||
|
is the iOS loader for `raincord/rain`, a sibling Revenge/Bunny-derived mod that is
|
||||||
|
*actively shipping* (last commit 2026-07-17, vs. this repo's Nov 2024). Its `%ctor`
|
||||||
|
explicitly checks `RCTNewArchEnabled` from Info.plist and refuses to run with
|
||||||
|
"Please update Discord to v332 or above" if it's off — i.e. current Discord iOS is
|
||||||
|
Bridgeless-only, exactly as the Android-side evidence predicted, and RainTweak has
|
||||||
|
real users validating this on-device.
|
||||||
|
|
||||||
|
**Fix applied**, ported from RainTweak's verified mechanism (better than the
|
||||||
|
`RCTInstance -_loadScriptFromSource:` + fake-`RCTSource` approach this fork
|
||||||
|
originally tried — see below for why that was replaced):
|
||||||
|
|
||||||
|
- `%hook RCTHost` / `-instance:didInitializeRuntime:` (`Headers/RCTHost.h`,
|
||||||
|
`Sources/Tweak.xm`) — fires once the Bridgeless JS runtime is ready. Discord's own
|
||||||
|
bundle is left completely untouched via `%orig` first; Revenge's own scripts
|
||||||
|
(loader payload, downloaded bundle, theme, preloads) are then run directly against
|
||||||
|
the live `facebook::jsi::Runtime` via `[JSI evaluate:tag:runtime:]`
|
||||||
|
(`Headers/JSI.h` / `Sources/JSI.mm`, ported near-verbatim from RainTweak's
|
||||||
|
`JSI.mm`) instead of being smuggled in as fake bundle loads.
|
||||||
|
- This sidesteps the problem the first attempt ran into: `RCTSource` (what
|
||||||
|
`-_loadScriptFromSource:` takes) has no public initializer, so faking one meant
|
||||||
|
poking private ivars — fragile and, per RainTweak's actual production code, not
|
||||||
|
even the path they use. Evaluating directly against the runtime via JSI is both
|
||||||
|
simpler and how the proven-working reference implementation does it.
|
||||||
|
- The legacy `RCTCxxBridge -executeApplicationScript:...` hook is kept as a
|
||||||
|
no-cost fallback for any host still on the old bridge (Logos skips hooking
|
||||||
|
classes that aren't present at runtime, and a `didInject` guard stops both hooks
|
||||||
|
from double-firing if they somehow both resolve).
|
||||||
|
- `Headers/jsi/{jsi.h,jsi-inl.h,instrumentation.h}` are vendored copies of Meta's
|
||||||
|
JSI headers (MIT-licensed, same as RN itself) — Theos doesn't ship them, and
|
||||||
|
RainTweak vendors the identical files for the same reason.
|
||||||
|
|
||||||
|
**Still unverified against a real device or the current Discord binary** — I don't
|
||||||
|
have a jailbreak or Xcode/Theos here. But unlike the first attempt, this is no
|
||||||
|
longer a guess extrapolated from RN's public source: the mechanism (`RCTHost`
|
||||||
|
hook + JSI evaluation) is the exact one running in a comparable, currently-shipping
|
||||||
|
mod. Confidence is meaningfully higher; it should still be tested before relying on
|
||||||
|
it, since Discord's specific build could in principle differ.
|
||||||
|
|
||||||
|
## 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.
|
||||||
16
Headers/JSI.h
Normal file
16
Headers/JSI.h
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <jsi/jsi.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal JSI helper: evaluates a script directly against a live `facebook::jsi::Runtime`,
|
||||||
|
* instead of re-entering React Native's own bundle-loading machinery. Ported from RainTweak
|
||||||
|
* (codeberg.org/raincord/RainTweak), an actively-maintained loader for a sibling Revenge/Bunny
|
||||||
|
* fork that verified this approach works on current (Bridgeless) Discord iOS builds.
|
||||||
|
*/
|
||||||
|
@interface JSI : NSObject
|
||||||
|
|
||||||
|
+ (void)evaluate:(NSData *)scriptData tag:(NSString *)tag runtime:(facebook::jsi::Runtime &)runtime;
|
||||||
|
|
||||||
|
@end
|
||||||
15
Headers/RCTHost.h
Normal file
15
Headers/RCTHost.h
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <jsi/jsi.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward declaration of React Native's Bridgeless-mode host class, just enough of it for Logos
|
||||||
|
* to hook `-instance:didInitializeRuntime:`. React Native ships no public header exposing this
|
||||||
|
* (it's an internal `ReactCommon/react/runtime` class), so the signature is declared by hand here -
|
||||||
|
* matches `RCTHost.h` in RainTweak (codeberg.org/raincord/RainTweak), which hooks the same method
|
||||||
|
* on a currently-shipping Discord iOS build.
|
||||||
|
*/
|
||||||
|
@interface RCTHost : NSObject
|
||||||
|
|
||||||
|
- (void)instance:(id)instance didInitializeRuntime:(facebook::jsi::Runtime &)runtime;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
@ -4,3 +4,6 @@
|
||||||
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);
|
||||||
|
|
||||||
|
/** Detects the Hermes bytecode magic header ("Herm"), as opposed to plain-text JS. */
|
||||||
|
BOOL isHermesBytecode(NSData *data);
|
||||||
132
Headers/jsi/instrumentation.h
Normal file
132
Headers/jsi/instrumentation.h
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <iosfwd>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <jsi/jsi.h>
|
||||||
|
|
||||||
|
namespace facebook {
|
||||||
|
namespace jsi {
|
||||||
|
|
||||||
|
/// Methods for starting and collecting instrumentation, an \c Instrumentation
|
||||||
|
/// instance is associated with a particular \c Runtime instance, which it
|
||||||
|
/// controls the instrumentation of.
|
||||||
|
/// None of these functions should return newly created jsi values, nor should
|
||||||
|
/// it modify the values of any jsi values in the heap (although GCs are fine).
|
||||||
|
class JSI_EXPORT Instrumentation {
|
||||||
|
public:
|
||||||
|
/// Additional options controlling what to include when capturing a heap
|
||||||
|
/// snapshot.
|
||||||
|
struct HeapSnapshotOptions {
|
||||||
|
bool captureNumericValue{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual ~Instrumentation() = default;
|
||||||
|
|
||||||
|
/// Returns GC statistics as a JSON-encoded string, with an object containing
|
||||||
|
/// "type" and "version" fields outermost. "type" is a string, unique to a
|
||||||
|
/// particular implementation of \c jsi::Instrumentation, and "version" is a
|
||||||
|
/// number to indicate any revision to that implementation and its output
|
||||||
|
/// format.
|
||||||
|
///
|
||||||
|
/// \pre This call can only be made on the instrumentation instance of a
|
||||||
|
/// runtime initialised to collect GC statistics.
|
||||||
|
///
|
||||||
|
/// \post All cumulative measurements mentioned in the output are accumulated
|
||||||
|
/// across the entire lifetime of the Runtime.
|
||||||
|
///
|
||||||
|
/// \return the GC statistics collected so far, as a JSON-encoded string.
|
||||||
|
virtual std::string getRecordedGCStats() = 0;
|
||||||
|
|
||||||
|
/// Request statistics about the current state of the runtime's heap. This
|
||||||
|
/// function can be called at any time, and should produce information that is
|
||||||
|
/// correct at the instant it is called (i.e, not stale).
|
||||||
|
///
|
||||||
|
/// \return a map from a string key to a number associated with that
|
||||||
|
/// statistic.
|
||||||
|
virtual std::unordered_map<std::string, int64_t> getHeapInfo(
|
||||||
|
bool includeExpensive) = 0;
|
||||||
|
|
||||||
|
/// Perform a full garbage collection.
|
||||||
|
/// \param cause The cause of this collection, as it should be reported in
|
||||||
|
/// logs.
|
||||||
|
virtual void collectGarbage(std::string cause) = 0;
|
||||||
|
|
||||||
|
/// A HeapStatsUpdate is a tuple of the fragment index, the number of objects
|
||||||
|
/// in that fragment, and the number of bytes used by those objects.
|
||||||
|
/// A "fragment" is a view of all objects allocated within a time slice.
|
||||||
|
using HeapStatsUpdate = std::tuple<uint64_t, uint64_t, uint64_t>;
|
||||||
|
|
||||||
|
/// Start capturing JS stack-traces for all JS heap allocated objects. These
|
||||||
|
/// can be accessed via \c ::createSnapshotToFile().
|
||||||
|
/// \param fragmentCallback If present, invoke this callback every so often
|
||||||
|
/// with the most recently seen object ID, and a list of fragments that have
|
||||||
|
/// been updated. This callback will be invoked on the same thread that the
|
||||||
|
/// runtime is using.
|
||||||
|
virtual void startTrackingHeapObjectStackTraces(
|
||||||
|
std::function<void(
|
||||||
|
uint64_t lastSeenObjectID,
|
||||||
|
std::chrono::microseconds timestamp,
|
||||||
|
std::vector<HeapStatsUpdate> stats)> fragmentCallback) = 0;
|
||||||
|
|
||||||
|
/// Stop capture JS stack-traces for JS heap allocated objects.
|
||||||
|
virtual void stopTrackingHeapObjectStackTraces() = 0;
|
||||||
|
|
||||||
|
/// Start a heap sampling profiler that will sample heap allocations, and the
|
||||||
|
/// stack trace they were allocated at. Reports a summary of which functions
|
||||||
|
/// allocated the most.
|
||||||
|
/// \param samplingInterval The number of bytes allocated to wait between
|
||||||
|
/// samples. This will be used as the expected value of a poisson
|
||||||
|
/// distribution.
|
||||||
|
virtual void startHeapSampling(size_t samplingInterval) = 0;
|
||||||
|
|
||||||
|
/// Turns off the heap sampling profiler previously enabled via
|
||||||
|
/// \c startHeapSampling. Writes the output of the sampling heap profiler to
|
||||||
|
/// \p os. The output is a JSON formatted string.
|
||||||
|
virtual void stopHeapSampling(std::ostream& os) = 0;
|
||||||
|
|
||||||
|
/// Captures the heap to a file
|
||||||
|
///
|
||||||
|
/// \param path to save the heap capture.
|
||||||
|
/// \param options additional options for what to capture.
|
||||||
|
virtual void createSnapshotToFile(
|
||||||
|
const std::string& path,
|
||||||
|
const HeapSnapshotOptions& options = {false}) = 0;
|
||||||
|
|
||||||
|
/// Captures the heap to an output stream
|
||||||
|
///
|
||||||
|
/// \param os output stream to write to.
|
||||||
|
/// \param options additional options for what to capture.
|
||||||
|
virtual void createSnapshotToStream(
|
||||||
|
std::ostream& os,
|
||||||
|
const HeapSnapshotOptions& options = {false}) = 0;
|
||||||
|
|
||||||
|
/// If the runtime has been created to trace to a temp file, flush
|
||||||
|
/// any unwritten parts of the trace of bridge traffic to the file,
|
||||||
|
/// and return the name of the file. Otherwise, return the empty string.
|
||||||
|
/// Tracing is disabled after this call.
|
||||||
|
virtual std::string flushAndDisableBridgeTrafficTrace() = 0;
|
||||||
|
|
||||||
|
/// Write basic block profile trace to the given file name.
|
||||||
|
virtual void writeBasicBlockProfileTraceToFile(
|
||||||
|
const std::string& fileName) const = 0;
|
||||||
|
|
||||||
|
/// Write the opcode stats to the given stream.
|
||||||
|
virtual void dumpOpcodeStats(std::ostream& os) const = 0;
|
||||||
|
|
||||||
|
/// Dump external profiler symbols to the given file name.
|
||||||
|
virtual void dumpProfilerSymbolsToFile(const std::string& fileName) const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace jsi
|
||||||
|
} // namespace facebook
|
||||||
405
Headers/jsi/jsi-inl.h
Normal file
405
Headers/jsi/jsi-inl.h
Normal file
|
|
@ -0,0 +1,405 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace facebook {
|
||||||
|
namespace jsi {
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
inline Value toValue(Runtime&, std::nullptr_t) {
|
||||||
|
return Value::null();
|
||||||
|
}
|
||||||
|
inline Value toValue(Runtime&, bool b) {
|
||||||
|
return Value(b);
|
||||||
|
}
|
||||||
|
inline Value toValue(Runtime&, double d) {
|
||||||
|
return Value(d);
|
||||||
|
}
|
||||||
|
inline Value toValue(Runtime&, float f) {
|
||||||
|
return Value(static_cast<double>(f));
|
||||||
|
}
|
||||||
|
inline Value toValue(Runtime&, int i) {
|
||||||
|
return Value(i);
|
||||||
|
}
|
||||||
|
inline Value toValue(Runtime& runtime, const char* str) {
|
||||||
|
return String::createFromAscii(runtime, str);
|
||||||
|
}
|
||||||
|
inline Value toValue(Runtime& runtime, const std::string& str) {
|
||||||
|
return String::createFromUtf8(runtime, str);
|
||||||
|
}
|
||||||
|
template <typename T>
|
||||||
|
inline Value toValue(Runtime& runtime, const T& other) {
|
||||||
|
static_assert(
|
||||||
|
std::is_base_of<Pointer, T>::value,
|
||||||
|
"This type cannot be converted to Value");
|
||||||
|
return Value(runtime, other);
|
||||||
|
}
|
||||||
|
inline Value toValue(Runtime& runtime, const Value& value) {
|
||||||
|
return Value(runtime, value);
|
||||||
|
}
|
||||||
|
inline Value&& toValue(Runtime&, Value&& value) {
|
||||||
|
return std::move(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline PropNameID toPropNameID(Runtime& runtime, const char* name) {
|
||||||
|
return PropNameID::forAscii(runtime, name);
|
||||||
|
}
|
||||||
|
inline PropNameID toPropNameID(Runtime& runtime, const std::string& name) {
|
||||||
|
return PropNameID::forUtf8(runtime, name);
|
||||||
|
}
|
||||||
|
inline PropNameID&& toPropNameID(Runtime&, PropNameID&& name) {
|
||||||
|
return std::move(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to throw while still compiling with exceptions turned off.
|
||||||
|
template <typename E, typename... Args>
|
||||||
|
[[noreturn]] inline void throwOrDie(Args&&... args) {
|
||||||
|
std::rethrow_exception(
|
||||||
|
std::make_exception_ptr(E{std::forward<Args>(args)...}));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline T Runtime::make(Runtime::PointerValue* pv) {
|
||||||
|
return T(pv);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Runtime::PointerValue* Runtime::getPointerValue(jsi::Pointer& pointer) {
|
||||||
|
return pointer.ptr_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const Runtime::PointerValue* Runtime::getPointerValue(
|
||||||
|
const jsi::Pointer& pointer) {
|
||||||
|
return pointer.ptr_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const Runtime::PointerValue* Runtime::getPointerValue(
|
||||||
|
const jsi::Value& value) {
|
||||||
|
return value.data_.pointer.ptr_;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Runtime::setRuntimeData(
|
||||||
|
const UUID& uuid,
|
||||||
|
const std::shared_ptr<void>& data) {
|
||||||
|
auto* dataPtr = new std::shared_ptr<void>(data);
|
||||||
|
setRuntimeDataImpl(uuid, dataPtr, [](const void* data) {
|
||||||
|
delete (const std::shared_ptr<void>*)data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::shared_ptr<void> Runtime::getRuntimeData(const UUID& uuid) {
|
||||||
|
auto* data = (const std::shared_ptr<void>*)getRuntimeDataImpl(uuid);
|
||||||
|
return data ? *data : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value Object::getPrototype(Runtime& runtime) const {
|
||||||
|
return runtime.getPrototypeOf(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Object::getProperty(Runtime& runtime, const char* name) const {
|
||||||
|
return getProperty(runtime, String::createFromAscii(runtime, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Object::getProperty(Runtime& runtime, const String& name) const {
|
||||||
|
return runtime.getProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Object::getProperty(Runtime& runtime, const PropNameID& name)
|
||||||
|
const {
|
||||||
|
return runtime.getProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Object::getProperty(Runtime& runtime, const Value& name) const {
|
||||||
|
return runtime.getProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool Object::hasProperty(Runtime& runtime, const char* name) const {
|
||||||
|
return hasProperty(runtime, String::createFromAscii(runtime, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool Object::hasProperty(Runtime& runtime, const String& name) const {
|
||||||
|
return runtime.hasProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool Object::hasProperty(Runtime& runtime, const PropNameID& name)
|
||||||
|
const {
|
||||||
|
return runtime.hasProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool Object::hasProperty(Runtime& runtime, const Value& name) const {
|
||||||
|
return runtime.hasProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void Object::setProperty(Runtime& runtime, const char* name, T&& value) const {
|
||||||
|
setProperty(
|
||||||
|
runtime, String::createFromAscii(runtime, name), std::forward<T>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void Object::setProperty(Runtime& runtime, const String& name, T&& value)
|
||||||
|
const {
|
||||||
|
setPropertyValue(
|
||||||
|
runtime, name, detail::toValue(runtime, std::forward<T>(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void Object::setProperty(Runtime& runtime, const PropNameID& name, T&& value)
|
||||||
|
const {
|
||||||
|
setPropertyValue(
|
||||||
|
runtime, name, detail::toValue(runtime, std::forward<T>(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void Object::setProperty(Runtime& runtime, const Value& name, T&& value) const {
|
||||||
|
setPropertyValue(
|
||||||
|
runtime, name, detail::toValue(runtime, std::forward<T>(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Object::deleteProperty(Runtime& runtime, const char* name) const {
|
||||||
|
deleteProperty(runtime, String::createFromAscii(runtime, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Object::deleteProperty(Runtime& runtime, const String& name) const {
|
||||||
|
runtime.deleteProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Object::deleteProperty(Runtime& runtime, const PropNameID& name)
|
||||||
|
const {
|
||||||
|
runtime.deleteProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Object::deleteProperty(Runtime& runtime, const Value& name) const {
|
||||||
|
runtime.deleteProperty(*this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Array Object::getArray(Runtime& runtime) const& {
|
||||||
|
assert(runtime.isArray(*this));
|
||||||
|
(void)runtime; // when assert is disabled we need to mark this as used
|
||||||
|
return Array(runtime.cloneObject(ptr_));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Array Object::getArray(Runtime& runtime) && {
|
||||||
|
assert(runtime.isArray(*this));
|
||||||
|
(void)runtime; // when assert is disabled we need to mark this as used
|
||||||
|
Runtime::PointerValue* value = ptr_;
|
||||||
|
ptr_ = nullptr;
|
||||||
|
return Array(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline ArrayBuffer Object::getArrayBuffer(Runtime& runtime) const& {
|
||||||
|
assert(runtime.isArrayBuffer(*this));
|
||||||
|
(void)runtime; // when assert is disabled we need to mark this as used
|
||||||
|
return ArrayBuffer(runtime.cloneObject(ptr_));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline ArrayBuffer Object::getArrayBuffer(Runtime& runtime) && {
|
||||||
|
assert(runtime.isArrayBuffer(*this));
|
||||||
|
(void)runtime; // when assert is disabled we need to mark this as used
|
||||||
|
Runtime::PointerValue* value = ptr_;
|
||||||
|
ptr_ = nullptr;
|
||||||
|
return ArrayBuffer(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Function Object::getFunction(Runtime& runtime) const& {
|
||||||
|
assert(runtime.isFunction(*this));
|
||||||
|
return Function(runtime.cloneObject(ptr_));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Function Object::getFunction(Runtime& runtime) && {
|
||||||
|
assert(runtime.isFunction(*this));
|
||||||
|
(void)runtime; // when assert is disabled we need to mark this as used
|
||||||
|
Runtime::PointerValue* value = ptr_;
|
||||||
|
ptr_ = nullptr;
|
||||||
|
return Function(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline bool Object::isHostObject(Runtime& runtime) const {
|
||||||
|
return runtime.isHostObject(*this) &&
|
||||||
|
std::dynamic_pointer_cast<T>(runtime.getHostObject(*this));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline bool Object::isHostObject<HostObject>(Runtime& runtime) const {
|
||||||
|
return runtime.isHostObject(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline std::shared_ptr<T> Object::getHostObject(Runtime& runtime) const {
|
||||||
|
assert(isHostObject<T>(runtime));
|
||||||
|
return std::static_pointer_cast<T>(runtime.getHostObject(*this));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline std::shared_ptr<T> Object::asHostObject(Runtime& runtime) const {
|
||||||
|
if (!isHostObject<T>(runtime)) {
|
||||||
|
detail::throwOrDie<JSINativeException>(
|
||||||
|
"Object is not a HostObject of desired type");
|
||||||
|
}
|
||||||
|
return std::static_pointer_cast<T>(runtime.getHostObject(*this));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline std::shared_ptr<HostObject> Object::getHostObject<HostObject>(
|
||||||
|
Runtime& runtime) const {
|
||||||
|
assert(runtime.isHostObject(*this));
|
||||||
|
return runtime.getHostObject(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline bool Object::hasNativeState(Runtime& runtime) const {
|
||||||
|
return runtime.hasNativeState(*this) &&
|
||||||
|
std::dynamic_pointer_cast<T>(runtime.getNativeState(*this));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline bool Object::hasNativeState<NativeState>(Runtime& runtime) const {
|
||||||
|
return runtime.hasNativeState(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
inline std::shared_ptr<T> Object::getNativeState(Runtime& runtime) const {
|
||||||
|
assert(hasNativeState<T>(runtime));
|
||||||
|
return std::static_pointer_cast<T>(runtime.getNativeState(*this));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Object::setNativeState(
|
||||||
|
Runtime& runtime,
|
||||||
|
std::shared_ptr<NativeState> state) const {
|
||||||
|
runtime.setNativeState(*this, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Object::setExternalMemoryPressure(Runtime& runtime, size_t amt)
|
||||||
|
const {
|
||||||
|
runtime.setExternalMemoryPressure(*this, amt);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Array Object::getPropertyNames(Runtime& runtime) const {
|
||||||
|
return runtime.getPropertyNames(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value WeakObject::lock(Runtime& runtime) const {
|
||||||
|
return runtime.lockWeakObject(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void Array::setValueAtIndex(Runtime& runtime, size_t i, T&& value) const {
|
||||||
|
setValueAtIndexImpl(
|
||||||
|
runtime, i, detail::toValue(runtime, std::forward<T>(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Array::getValueAtIndex(Runtime& runtime, size_t i) const {
|
||||||
|
return runtime.getValueAtIndex(*this, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Function Function::createFromHostFunction(
|
||||||
|
Runtime& runtime,
|
||||||
|
const jsi::PropNameID& name,
|
||||||
|
unsigned int paramCount,
|
||||||
|
jsi::HostFunctionType func) {
|
||||||
|
return runtime.createFunctionFromHostFunction(
|
||||||
|
name, paramCount, std::move(func));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Function::call(Runtime& runtime, const Value* args, size_t count)
|
||||||
|
const {
|
||||||
|
return runtime.call(*this, Value::undefined(), args, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Function::call(Runtime& runtime, std::initializer_list<Value> args)
|
||||||
|
const {
|
||||||
|
return call(runtime, args.begin(), args.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
inline Value Function::call(Runtime& runtime, Args&&... args) const {
|
||||||
|
// A more awesome version of this would be able to create raw values
|
||||||
|
// which can be used directly without wrapping and unwrapping, but
|
||||||
|
// this will do for now.
|
||||||
|
return call(runtime, {detail::toValue(runtime, std::forward<Args>(args))...});
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Function::callWithThis(
|
||||||
|
Runtime& runtime,
|
||||||
|
const Object& jsThis,
|
||||||
|
const Value* args,
|
||||||
|
size_t count) const {
|
||||||
|
return runtime.call(*this, Value(runtime, jsThis), args, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Function::callWithThis(
|
||||||
|
Runtime& runtime,
|
||||||
|
const Object& jsThis,
|
||||||
|
std::initializer_list<Value> args) const {
|
||||||
|
return callWithThis(runtime, jsThis, args.begin(), args.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
inline Value Function::callWithThis(
|
||||||
|
Runtime& runtime,
|
||||||
|
const Object& jsThis,
|
||||||
|
Args&&... args) const {
|
||||||
|
// A more awesome version of this would be able to create raw values
|
||||||
|
// which can be used directly without wrapping and unwrapping, but
|
||||||
|
// this will do for now.
|
||||||
|
return callWithThis(
|
||||||
|
runtime, jsThis, {detail::toValue(runtime, std::forward<Args>(args))...});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
inline Array Array::createWithElements(Runtime& runtime, Args&&... args) {
|
||||||
|
return createWithElements(
|
||||||
|
runtime, {detail::toValue(runtime, std::forward<Args>(args))...});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
inline std::vector<PropNameID> PropNameID::names(
|
||||||
|
Runtime& runtime,
|
||||||
|
Args&&... args) {
|
||||||
|
return names({detail::toPropNameID(runtime, std::forward<Args>(args))...});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
inline std::vector<PropNameID> PropNameID::names(
|
||||||
|
PropNameID (&&propertyNames)[N]) {
|
||||||
|
std::vector<PropNameID> result;
|
||||||
|
result.reserve(N);
|
||||||
|
for (auto& name : propertyNames) {
|
||||||
|
result.push_back(std::move(name));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Function::callAsConstructor(
|
||||||
|
Runtime& runtime,
|
||||||
|
const Value* args,
|
||||||
|
size_t count) const {
|
||||||
|
return runtime.callAsConstructor(*this, args, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Value Function::callAsConstructor(
|
||||||
|
Runtime& runtime,
|
||||||
|
std::initializer_list<Value> args) const {
|
||||||
|
return callAsConstructor(runtime, args.begin(), args.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
inline Value Function::callAsConstructor(Runtime& runtime, Args&&... args)
|
||||||
|
const {
|
||||||
|
return callAsConstructor(
|
||||||
|
runtime, {detail::toValue(runtime, std::forward<Args>(args))...});
|
||||||
|
}
|
||||||
|
|
||||||
|
String BigInt::toString(Runtime& runtime, int radix) const {
|
||||||
|
return runtime.bigintToString(*this, radix);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace jsi
|
||||||
|
} // namespace facebook
|
||||||
1867
Headers/jsi/jsi.h
Normal file
1867
Headers/jsi/jsi.h
Normal file
File diff suppressed because it is too large
Load diff
3
Makefile
3
Makefile
|
|
@ -7,8 +7,9 @@ include $(THEOS)/makefiles/common.mk
|
||||||
TWEAK_NAME = Revenge
|
TWEAK_NAME = Revenge
|
||||||
BUNDLE_NAME = RevengeResources
|
BUNDLE_NAME = RevengeResources
|
||||||
|
|
||||||
Revenge_FILES = $(wildcard Sources/*.x Sources/*.m Sources/**/*.x Sources/**/*.m)
|
Revenge_FILES = $(wildcard Sources/*.x Sources/*.xm Sources/*.m Sources/*.mm Sources/**/*.x Sources/**/*.xm Sources/**/*.m Sources/**/*.mm)
|
||||||
Revenge_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' -I$(THEOS_PROJECT_DIR)/Headers
|
Revenge_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' -I$(THEOS_PROJECT_DIR)/Headers
|
||||||
|
Revenge_CCFLAGS = -std=c++17 -Wno-c++11-narrowing -I$(THEOS_PROJECT_DIR)/Headers
|
||||||
Revenge_FRAMEWORKS = Foundation UIKit CoreGraphics CoreText CoreFoundation
|
Revenge_FRAMEWORKS = Foundation UIKit CoreGraphics CoreText CoreFoundation
|
||||||
|
|
||||||
RevengeResources_INSTALL_PATH = "/Library/Application\ Support/"
|
RevengeResources_INSTALL_PATH = "/Library/Application\ Support/"
|
||||||
|
|
|
||||||
49
Sources/JSI.mm
Normal file
49
Sources/JSI.mm
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#import "JSI.h"
|
||||||
|
#import "Logger.h"
|
||||||
|
#import "Utils.h"
|
||||||
|
|
||||||
|
using namespace facebook;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class NSDataBuffer : public jsi::Buffer {
|
||||||
|
public:
|
||||||
|
explicit NSDataBuffer(NSData *data) : data_(data) {}
|
||||||
|
|
||||||
|
size_t size() const override {
|
||||||
|
return data_.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *data() const override {
|
||||||
|
return static_cast<const uint8_t *>(data_.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
NSData *data_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@implementation JSI
|
||||||
|
|
||||||
|
+ (void)evaluate:(NSData *)scriptData tag:(NSString *)tag runtime:(jsi::Runtime &)runtime {
|
||||||
|
if (scriptData.length == 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isHermesBytecode(scriptData)) {
|
||||||
|
auto buffer = std::make_shared<NSDataBuffer>(scriptData);
|
||||||
|
auto prepared = runtime.prepareJavaScript(buffer, std::string(tag.UTF8String));
|
||||||
|
runtime.evaluatePreparedJavaScript(prepared);
|
||||||
|
} else {
|
||||||
|
std::string source((const char *)scriptData.bytes, scriptData.length);
|
||||||
|
auto buffer = std::make_shared<jsi::StringBuffer>(std::move(source));
|
||||||
|
runtime.evaluateJavaScript(buffer, std::string(tag.UTF8String));
|
||||||
|
}
|
||||||
|
} catch (const jsi::JSError &e) {
|
||||||
|
RevengeLog(@"[JSI] evaluate error: JSError for '%@': %s", tag, e.what());
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
RevengeLog(@"[JSI] evaluate error: exception for '%@': %s", tag, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,53 @@
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <jsi/jsi.h>
|
||||||
#import "Utils.h"
|
#import "Utils.h"
|
||||||
#import "Logger.h"
|
#import "Logger.h"
|
||||||
#import "Theme.h"
|
#import "Theme.h"
|
||||||
#import "Fonts.h"
|
#import "Fonts.h"
|
||||||
#import "LoaderConfig.h"
|
#import "LoaderConfig.h"
|
||||||
|
#import "JSI.h"
|
||||||
|
#import "RCTHost.h"
|
||||||
|
|
||||||
|
using namespace facebook;
|
||||||
|
|
||||||
static NSURL *source;
|
static NSURL *source;
|
||||||
static BOOL isJailbroken;
|
static BOOL isJailbroken;
|
||||||
static NSString *revengePatchesBundlePath;
|
static NSString *revengePatchesBundlePath;
|
||||||
static NSURL *pyoncordDirectory;
|
static NSURL *pyoncordDirectory;
|
||||||
static LoaderConfig *loaderConfig;
|
static LoaderConfig *loaderConfig;
|
||||||
|
static BOOL didInject = NO;
|
||||||
|
|
||||||
%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's data off to `execute`.
|
||||||
if (![url.absoluteString containsString:@"main.jsbundle"]) {
|
*
|
||||||
return %orig;
|
* Historically this fed straight into `RCTCxxBridge -executeApplicationScript:url:async:`
|
||||||
}
|
* (React Native's legacy bridge), which is what `execute` still does below for old-arch hosts.
|
||||||
|
* On current Discord iOS builds (Bridgeless / New Architecture, confirmed via RainTweak -
|
||||||
|
* codeberg.org/raincord/RainTweak, an actively-shipping loader for a sibling Revenge/Bunny fork
|
||||||
|
* that explicitly requires "Discord v332 or above" for Bridgeless), that hook point never fires
|
||||||
|
* at all, so `execute` is instead wired to `JSI evaluate:` against the live runtime - see the
|
||||||
|
* `RCTHost` hook below, which is the verified entry point.
|
||||||
|
*/
|
||||||
|
static BOOL runRevengeInjection(BOOL async, void (^execute)(NSData *data, 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, YES);
|
||||||
|
|
||||||
__block NSData *bundle = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"]];
|
__block NSData *bundle = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"]];
|
||||||
|
|
||||||
|
|
@ -89,7 +101,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], async);
|
||||||
}
|
}
|
||||||
|
|
||||||
NSData *fontData = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"fonts.json"]];
|
NSData *fontData = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"fonts.json"]];
|
||||||
|
|
@ -104,7 +116,7 @@ static LoaderConfig *loaderConfig;
|
||||||
|
|
||||||
if (bundle) {
|
if (bundle) {
|
||||||
RevengeLog(@"Executing JS bundle");
|
RevengeLog(@"Executing JS bundle");
|
||||||
%orig(bundle, source, async);
|
execute(bundle, async);
|
||||||
}
|
}
|
||||||
|
|
||||||
NSURL *preloadsDirectory = [pyoncordDirectory URLByAppendingPathComponent:@"preloads"];
|
NSURL *preloadsDirectory = [pyoncordDirectory URLByAppendingPathComponent:@"preloads"];
|
||||||
|
|
@ -120,7 +132,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, async);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +141,50 @@ static LoaderConfig *loaderConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
%hook RCTHost
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridgeless-mode entry point (React Native's New Architecture) - fires once the JS runtime is
|
||||||
|
* ready, letting Discord's own bundle load completely untouched via %orig first. Revenge's own
|
||||||
|
* scripts are then evaluated directly against the same runtime via JSI, rather than trying to
|
||||||
|
* masquerade as another RCTSource bundle load (which has no public constructor). This mirrors
|
||||||
|
* RainTweak's verified approach - see comment on `runRevengeInjection` above.
|
||||||
|
*/
|
||||||
|
- (void)instance:(id)instance didInitializeRuntime:(jsi::Runtime &)runtime {
|
||||||
|
%orig;
|
||||||
|
|
||||||
|
if (didInject) return;
|
||||||
|
didInject = YES;
|
||||||
|
|
||||||
|
runRevengeInjection(YES, ^(NSData *data, BOOL async) {
|
||||||
|
[JSI evaluate:data tag:@"revenge" runtime:runtime];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
%end
|
||||||
|
|
||||||
|
%hook RCTCxxBridge
|
||||||
|
|
||||||
|
// Legacy-bridge entry point. Fires when the host app runs React Native's old (non-Bridgeless)
|
||||||
|
// bridge. Kept for hosts that haven't moved to Bridgeless; harmless if RCTCxxBridge is never
|
||||||
|
// instantiated (Logos skips hooking classes that aren't present at runtime).
|
||||||
|
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async {
|
||||||
|
if (![url.absoluteString containsString:@"main.jsbundle"] || didInject) {
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
didInject = YES;
|
||||||
|
|
||||||
|
BOOL injected = runRevengeInjection(async, ^(NSData *data, BOOL scriptAsync) {
|
||||||
|
%orig(data, source, scriptAsync);
|
||||||
|
});
|
||||||
|
if (!injected) {
|
||||||
|
didInject = NO;
|
||||||
|
return %orig;
|
||||||
|
}
|
||||||
|
|
||||||
%orig(script, url, async);
|
%orig(script, url, async);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +218,7 @@ static LoaderConfig *loaderConfig;
|
||||||
RevengeLog(@"Bundle contents: %@", bundleContents);
|
RevengeLog(@"Bundle contents: %@", bundleContents);
|
||||||
}
|
}
|
||||||
|
|
||||||
pyoncordDirectory = getPyoncordDirectory();
|
pyoncordDirectory = getPyoncordDirectory();
|
||||||
loaderConfig = [[LoaderConfig alloc] init];
|
loaderConfig = [[LoaderConfig alloc] init];
|
||||||
[loaderConfig loadConfig];
|
[loaderConfig loadConfig];
|
||||||
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
#import "Utils.h"
|
#import "Utils.h"
|
||||||
|
|
||||||
|
BOOL isHermesBytecode(NSData *data) {
|
||||||
|
if (!data || data.length < 4) return NO;
|
||||||
|
const uint8_t *bytes = (const uint8_t *)data.bytes;
|
||||||
|
// Hermes bytecode magic: "Herm"
|
||||||
|
return bytes[0] == 0x48 && bytes[1] == 0x65 && bytes[2] == 0x72 && bytes[3] == 0x6d;
|
||||||
|
}
|
||||||
|
|
||||||
NSURL *getPyoncordDirectory(void) {
|
NSURL *getPyoncordDirectory(void) {
|
||||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
NSURL *documentDirectoryURL = [[fileManager URLsForDirectory:NSDocumentDirectory
|
NSURL *documentDirectoryURL = [[fileManager URLsForDirectory:NSDocumentDirectory
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue