mirror of
https://github.com/NohamR/RMHook.git
synced 2026-04-08 07:59:58 +00:00
Introduces MessageBroker to enable communication between the dylib and QML via signals. Updates CMakeLists.txt to include Qml components and conditionally add MessageBroker sources in qmlrebuild mode. reMarkable.m is updated to register the QML type, set up native callbacks, and demonstrate broadcasting signals.
550 lines
20 KiB
Objective-C
550 lines
20 KiB
Objective-C
#import "reMarkable.h"
|
|
#import <Foundation/Foundation.h>
|
|
#import "Constant.h"
|
|
#import "MemoryUtils.h"
|
|
#import "Logger.h"
|
|
#import "ResourceUtils.h"
|
|
#ifdef BUILD_MODE_DEV
|
|
#import "DevHooks.h"
|
|
#endif
|
|
#ifdef BUILD_MODE_QMLREBUILD
|
|
#import "MessageBroker.h"
|
|
#endif
|
|
#import <objc/runtime.h>
|
|
#import <Cocoa/Cocoa.h>
|
|
#include <stdint.h>
|
|
#include <limits.h>
|
|
#include <pthread.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <dispatch/dispatch.h>
|
|
|
|
#include <QtNetwork/QNetworkAccessManager>
|
|
#include <QtNetwork/QNetworkRequest>
|
|
#include <QtNetwork/QNetworkReply>
|
|
#include <QtCore/QDebug>
|
|
#include <QtCore/QIODevice>
|
|
#include <QtCore/QUrl>
|
|
#include <QtCore/QString>
|
|
#include <QtCore/Qt>
|
|
#include <QtWebSockets/QWebSocket>
|
|
#include <QtCore/QSettings>
|
|
#include <QtCore/QVariant>
|
|
#include <QtCore/QAnyStringView>
|
|
|
|
|
|
static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config";
|
|
static NSString *const kReMarkableConfigHostKey = @"host";
|
|
static NSString *const kReMarkableConfigPortKey = @"port";
|
|
static NSString *const kReMarkableDefaultHost = @"example.com";
|
|
static NSNumber *const kReMarkableDefaultPort = @(443);
|
|
|
|
static NSString *gConfiguredHost = @"example.com";
|
|
static NSNumber *gConfiguredPort = @(443);
|
|
static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
static NSString *ReMarkablePreferencesDirectory(void);
|
|
|
|
static NSString *ReMarkablePreferencesDirectory(void) {
|
|
NSArray<NSString *> *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
|
|
NSString *libraryDir = [libraryPaths firstObject];
|
|
if (![libraryDir length]) {
|
|
libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
|
|
}
|
|
return [libraryDir stringByAppendingPathComponent:@"Preferences"];
|
|
}
|
|
|
|
static NSString *ReMarkableConfigFilePath(void) {
|
|
return [ReMarkablePreferencesDirectory() stringByAppendingPathComponent:kReMarkableConfigFileName];
|
|
}
|
|
|
|
static BOOL ReMarkableWriteConfig(NSString *path, NSDictionary<NSString *, id> *config) {
|
|
NSError *error = nil;
|
|
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error];
|
|
if (!jsonData || error) {
|
|
NSLogger(@"[reMarkable] Failed to serialize config: %@", error);
|
|
return NO;
|
|
}
|
|
if (![jsonData writeToFile:path atomically:YES]) {
|
|
NSLogger(@"[reMarkable] Failed to write config file at %@", path);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
static void ReMarkableLoadOrCreateConfig(void) {
|
|
NSString *configPath = ReMarkableConfigFilePath();
|
|
NSString *directory = [configPath stringByDeletingLastPathComponent];
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
BOOL isDirectory = NO;
|
|
NSError *error = nil;
|
|
|
|
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) {
|
|
if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) {
|
|
NSLogger(@"[reMarkable] Failed to create config directory %@: %@", directory, error);
|
|
}
|
|
}
|
|
|
|
NSDictionary<NSString *, id> *defaults = @{kReMarkableConfigHostKey : kReMarkableDefaultHost,
|
|
kReMarkableConfigPortKey : kReMarkableDefaultPort};
|
|
|
|
if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) {
|
|
NSData *data = [NSData dataWithContentsOfFile:configPath];
|
|
if ([data length] > 0) {
|
|
NSError *jsonError = nil;
|
|
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
|
|
if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) {
|
|
NSDictionary *configDict = (NSDictionary *)jsonObject;
|
|
NSString *hostValue = configDict[kReMarkableConfigHostKey];
|
|
NSNumber *portValue = configDict[kReMarkableConfigPortKey];
|
|
|
|
NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kReMarkableDefaultHost;
|
|
NSInteger portCandidate = kReMarkableDefaultPort.integerValue;
|
|
if ([portValue respondsToSelector:@selector(integerValue)]) {
|
|
NSInteger candidate = [portValue integerValue];
|
|
if (candidate > 0 && candidate <= 65535) {
|
|
portCandidate = candidate;
|
|
} else {
|
|
NSLogger(@"[reMarkable] Ignoring invalid port value %@, falling back to default.", portValue);
|
|
}
|
|
}
|
|
|
|
gConfiguredHost = [resolvedHost copy];
|
|
gConfiguredPort = @(portCandidate);
|
|
NSLogger(@"[reMarkable] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHost, gConfiguredPort);
|
|
return;
|
|
} else {
|
|
NSLogger(@"[reMarkable] Failed to parse config file %@: %@", configPath, jsonError);
|
|
}
|
|
} else {
|
|
NSLogger(@"[reMarkable] Config file %@ was empty, rewriting with defaults.", configPath);
|
|
}
|
|
}
|
|
|
|
if (ReMarkableWriteConfig(configPath, defaults)) {
|
|
NSLogger(@"[reMarkable] Created default config at %@", configPath);
|
|
}
|
|
gConfiguredHost = [kReMarkableDefaultHost copy];
|
|
gConfiguredPort = kReMarkableDefaultPort;
|
|
}
|
|
|
|
static inline QString QStringFromNSStringSafe(NSString *string) {
|
|
if (!string) {
|
|
return QString();
|
|
}
|
|
return QString::fromUtf8([string UTF8String]);
|
|
}
|
|
|
|
@interface MenuActionController : NSObject
|
|
@property (strong, nonatomic) NSURL *targetURL;
|
|
- (void)openURLAction:(id)sender;
|
|
+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url;
|
|
+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url withDelay:(NSTimeInterval)delay;
|
|
@end
|
|
|
|
@implementation MenuActionController
|
|
|
|
- (void)openURLAction:(id)sender {
|
|
if (self.targetURL) {
|
|
[[NSWorkspace sharedWorkspace] openURL:self.targetURL];
|
|
NSLogger(@"[+] URL opened successfully: %@", self.targetURL);
|
|
}
|
|
}
|
|
|
|
+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url {
|
|
[self addCustomHelpMenuEntry:title withURL:url withDelay:1.0];
|
|
}
|
|
|
|
+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url withDelay:(NSTimeInterval)delay {
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
MenuActionController *controller = [[MenuActionController alloc] init];
|
|
controller.targetURL = [NSURL URLWithString:url];
|
|
|
|
NSMenu *mainMenu = [NSApp mainMenu];
|
|
if (!mainMenu) {
|
|
NSLogger(@"[-] Main menu not found");
|
|
return;
|
|
}
|
|
|
|
NSMenuItem *helpMenuItem = nil;
|
|
for (NSMenuItem *item in [mainMenu itemArray]) {
|
|
if ([[item title] isEqualToString:@"Help"]) {
|
|
helpMenuItem = item;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!helpMenuItem) {
|
|
NSLogger(@"[-] Help menu item not found");
|
|
return;
|
|
}
|
|
|
|
NSMenu *helpMenu = [helpMenuItem submenu];
|
|
if (!helpMenu) {
|
|
NSLogger(@"[-] Help submenu not found");
|
|
return;
|
|
}
|
|
|
|
if ([helpMenu numberOfItems] > 0) {
|
|
[helpMenu addItem:[NSMenuItem separatorItem]];
|
|
}
|
|
|
|
NSMenuItem *customMenuItem = [[NSMenuItem alloc] initWithTitle:title
|
|
action:@selector(openURLAction:)
|
|
keyEquivalent:@""];
|
|
[customMenuItem setTarget:controller];
|
|
[helpMenu addItem:customMenuItem];
|
|
|
|
objc_setAssociatedObject(helpMenu,
|
|
[title UTF8String],
|
|
controller,
|
|
OBJC_ASSOCIATION_RETAIN);
|
|
|
|
NSLogger(@"[+] Custom menu item '%@' added successfully", title);
|
|
});
|
|
}
|
|
|
|
@end
|
|
|
|
@interface reMarkableDylib : NSObject
|
|
|
|
- (BOOL)hook;
|
|
|
|
@end
|
|
|
|
@implementation reMarkable
|
|
|
|
+ (void)load {
|
|
NSLogger(@"reMarkable dylib loaded successfully");
|
|
|
|
// Initialize the hook
|
|
reMarkableDylib *dylib = [[reMarkableDylib alloc] init];
|
|
[dylib hook];
|
|
|
|
#ifdef BUILD_MODE_RMFAKECLOUD
|
|
// Add custom Help menu entry to open config file
|
|
NSString *configPath = ReMarkableConfigFilePath();
|
|
NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath];
|
|
[MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config"
|
|
withURL:fileURL
|
|
withDelay:2.0];
|
|
#endif
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation reMarkableDylib
|
|
|
|
#ifdef BUILD_MODE_RMFAKECLOUD
|
|
static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
|
|
QNetworkAccessManager *self,
|
|
QNetworkAccessManager::Operation op,
|
|
const QNetworkRequest &request,
|
|
QIODevice *outgoingData) = NULL;
|
|
|
|
static void (*original_qWebSocket_open)(
|
|
QWebSocket *self,
|
|
const QNetworkRequest &request) = NULL;
|
|
#endif
|
|
|
|
#ifdef BUILD_MODE_QMLREBUILD
|
|
static int (*original_qRegisterResourceData)(
|
|
int,
|
|
const unsigned char *,
|
|
const unsigned char *,
|
|
const unsigned char *) = NULL;
|
|
#endif
|
|
|
|
|
|
|
|
#ifdef BUILD_MODE_RMFAKECLOUD
|
|
static inline bool shouldPatchURL(const QString &host) {
|
|
if (host.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
return QString(R"""(
|
|
hwr-production-dot-remarkable-production.appspot.com
|
|
service-manager-production-dot-remarkable-production.appspot.com
|
|
local.appspot.com
|
|
my.remarkable.com
|
|
ping.remarkable.com
|
|
internal.cloud.remarkable.com
|
|
eu.tectonic.remarkable.com
|
|
backtrace-proxy.cloud.remarkable.engineering
|
|
dev.ping.remarkable.com
|
|
dev.tectonic.remarkable.com
|
|
dev.internal.cloud.remarkable.com
|
|
eu.internal.tctn.cloud.remarkable.com
|
|
webapp-prod.cloud.remarkable.engineering
|
|
)""")
|
|
.contains(host, Qt::CaseInsensitive);
|
|
}
|
|
#endif
|
|
|
|
- (BOOL)hook {
|
|
NSLogger(@"[reMarkable] Starting hooks...");
|
|
|
|
#ifdef BUILD_MODE_RMFAKECLOUD
|
|
NSLogger(@"[reMarkable] Build mode: rmfakecloud");
|
|
ReMarkableLoadOrCreateConfig();
|
|
NSLogger(@"[reMarkable] Using override host %@ and port %@", gConfiguredHost, gConfiguredPort);
|
|
|
|
[MemoryUtils hookSymbol:@"QtNetwork"
|
|
symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice"
|
|
hookFunction:(void *)hooked_qNetworkAccessManager_createRequest
|
|
originalFunction:(void **)&original_qNetworkAccessManager_createRequest
|
|
logPrefix:@"[reMarkable]"];
|
|
|
|
[MemoryUtils hookSymbol:@"QtWebSockets"
|
|
symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest"
|
|
hookFunction:(void *)hooked_qWebSocket_open
|
|
originalFunction:(void **)&original_qWebSocket_open
|
|
logPrefix:@"[reMarkable]"];
|
|
#endif
|
|
|
|
#ifdef BUILD_MODE_QMLREBUILD
|
|
NSLogger(@"[reMarkable] Build mode: qmlrebuild");
|
|
|
|
// Register MessageBroker QML type for dylib <-> QML communication
|
|
messagebroker::registerQmlType();
|
|
|
|
// Register native callback to receive signals from QML
|
|
messagebroker::setNativeCallback([](const char *signal, const char *value) {
|
|
NSLogger(@"[reMarkable] Native callback received signal '%s' with value '%s'", signal, value);
|
|
});
|
|
|
|
[MemoryUtils hookSymbol:@"QtCore"
|
|
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
|
|
hookFunction:(void *)hooked_qRegisterResourceData
|
|
originalFunction:(void **)&original_qRegisterResourceData
|
|
logPrefix:@"[reMarkable]"];
|
|
|
|
// Send a delayed broadcast to QML (after UI has loaded)
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
messagebroker::broadcast("signalName", "Hello from dylib!");
|
|
});
|
|
#endif
|
|
|
|
#ifdef BUILD_MODE_DEV
|
|
NSLogger(@"[reMarkable] Build mode: dev/reverse engineering");
|
|
// [MemoryUtils hookSymbol:@"QtCore"
|
|
// symbolName:@"__ZN9QIODevice5writeEPKcx"
|
|
// hookFunction:(void *)hooked_qIODevice_write
|
|
// originalFunction:(void **)&original_qIODevice_write
|
|
// logPrefix:@"[reMarkable]"];
|
|
|
|
// // Hook function at address 0x10015A130
|
|
// [MemoryUtils hookAddress:@"reMarkable"
|
|
// staticAddress:0x10015A130
|
|
// hookFunction:(void *)hooked_function_at_0x10015A130
|
|
// originalFunction:(void **)&original_function_at_0x10015A130
|
|
// logPrefix:@"[reMarkable]"];
|
|
|
|
// // Hook function at address 0x10015BC90
|
|
// [MemoryUtils hookAddress:@"reMarkable"
|
|
// staticAddress:0x10015BC90
|
|
// hookFunction:(void *)hooked_function_at_0x10015BC90
|
|
// originalFunction:(void **)&original_function_at_0x10015BC90
|
|
// logPrefix:@"[reMarkable]"];
|
|
|
|
// // Hook function at address 0x10016D520
|
|
// [MemoryUtils hookAddress:@"reMarkable"
|
|
// staticAddress:0x10016D520
|
|
// hookFunction:(void *)hooked_function_at_0x10016D520
|
|
// originalFunction:(void **)&original_function_at_0x10016D520
|
|
// logPrefix:@"[reMarkable]"];
|
|
|
|
// // Hook function at address 0x1001B6EE0
|
|
// [MemoryUtils hookAddress:@"reMarkable"
|
|
// staticAddress:0x1001B6EE0
|
|
// hookFunction:(void *)hooked_function_at_0x1001B6EE0
|
|
// originalFunction:(void **)&original_function_at_0x1001B6EE0
|
|
// logPrefix:@"[reMarkable]"];
|
|
|
|
// PlatformHelpers.exportFile implementation WIP
|
|
|
|
// // Hook function at address 0x100011790
|
|
// [MemoryUtils hookAddress:@"reMarkable"
|
|
// staticAddress:0x100011790
|
|
// hookFunction:(void *)hooked_function_at_0x100011790
|
|
// originalFunction:(void **)&original_function_at_0x100011790
|
|
// logPrefix:@"[reMarkable]"];
|
|
|
|
// // Hook function at address 0x100011CE0
|
|
// [MemoryUtils hookAddress:@"reMarkable"
|
|
// staticAddress:0x100011CE0
|
|
// hookFunction:(void *)hooked_function_at_0x100011CE0
|
|
// originalFunction:(void **)&original_function_at_0x100011CE0
|
|
// logPrefix:@"[reMarkable]"];
|
|
|
|
// [MemoryUtils hookSymbol:@"QtQml"
|
|
// symbolName:@"__ZN11QQmlPrivate11qmlregisterENS_16RegistrationTypeEPv"
|
|
// hookFunction:(void *)hooked_qmlregister
|
|
// originalFunction:(void **)&original_qmlregister
|
|
// logPrefix:@"[reMarkable]"];
|
|
|
|
#endif
|
|
|
|
return YES;
|
|
}
|
|
|
|
#ifdef BUILD_MODE_RMFAKECLOUD
|
|
extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest(
|
|
QNetworkAccessManager* self,
|
|
QNetworkAccessManager::Operation op,
|
|
const QNetworkRequest& req,
|
|
QIODevice* outgoingData
|
|
) {
|
|
const QString host = req.url().host();
|
|
if (shouldPatchURL(host)) {
|
|
// Clone request to keep original immutable
|
|
QNetworkRequest newReq(req);
|
|
QUrl newUrl = req.url();
|
|
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost);
|
|
newUrl.setHost(overrideHost);
|
|
newUrl.setPort([gConfiguredPort intValue]);
|
|
newReq.setUrl(newUrl);
|
|
|
|
if (original_qNetworkAccessManager_createRequest) {
|
|
return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if (original_qNetworkAccessManager_createRequest) {
|
|
return original_qNetworkAccessManager_createRequest(self, op, req, outgoingData);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
extern "C" void hooked_qWebSocket_open(
|
|
QWebSocket* self,
|
|
const QNetworkRequest& req
|
|
) {
|
|
if (!original_qWebSocket_open) {
|
|
return;
|
|
}
|
|
|
|
const QString host = req.url().host();
|
|
if (shouldPatchURL(host)) {
|
|
QUrl newUrl = req.url();
|
|
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost);
|
|
newUrl.setHost(overrideHost);
|
|
newUrl.setPort([gConfiguredPort intValue]);
|
|
|
|
QNetworkRequest newReq(req);
|
|
newReq.setUrl(newUrl);
|
|
|
|
original_qWebSocket_open(self, newReq);
|
|
return;
|
|
}
|
|
|
|
original_qWebSocket_open(self, req);
|
|
}
|
|
#endif // BUILD_MODE_RMFAKECLOUD
|
|
|
|
#ifdef BUILD_MODE_QMLREBUILD
|
|
|
|
// See https://deepwiki.com/search/once-the-qrr-file-parsed-take_871f24a0-8636-4aee-bddf-7405b6e32584 for details on qmlrebuild replacement strategy
|
|
|
|
extern "C" int hooked_qRegisterResourceData(
|
|
int version,
|
|
const unsigned char *tree,
|
|
const unsigned char *name,
|
|
const unsigned char *data
|
|
) {
|
|
if (!original_qRegisterResourceData) {
|
|
return 0;
|
|
}
|
|
|
|
pthread_mutex_lock(&gResourceMutex);
|
|
struct ResourceRoot resource = {
|
|
.data = (uint8_t *)data,
|
|
.name = (uint8_t *)name,
|
|
.tree = (uint8_t *)tree,
|
|
|
|
.treeSize = 0,
|
|
.dataSize = 0,
|
|
.originalDataSize = 0,
|
|
.nameSize = 0,
|
|
|
|
.entriesAffected = 0,
|
|
};
|
|
|
|
NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p name:%p data:%p",
|
|
version, tree, name, data);
|
|
|
|
statArchive(&resource, 0);
|
|
|
|
// Make a writable copy of the tree (we need to modify offsets)
|
|
resource.tree = (uint8_t *)malloc(resource.treeSize);
|
|
if (!resource.tree) {
|
|
NSLogger(@"[reMarkable] Failed to allocate tree buffer");
|
|
pthread_mutex_unlock(&gResourceMutex);
|
|
return original_qRegisterResourceData(version, tree, name, data);
|
|
}
|
|
memcpy(resource.tree, tree, resource.treeSize);
|
|
|
|
// Process nodes and mark replacements
|
|
processNode(&resource, 0, "");
|
|
NSLogger(@"[reMarkable] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu",
|
|
resource.entriesAffected, resource.dataSize, resource.originalDataSize);
|
|
|
|
const unsigned char *finalTree = tree;
|
|
const unsigned char *finalData = data;
|
|
uint8_t *newDataBuffer = NULL;
|
|
|
|
if (resource.entriesAffected > 0) {
|
|
NSLogger(@"[reMarkable] Rebuilding data tables... (entries: %d)", resource.entriesAffected);
|
|
|
|
// Allocate new data buffer (original size + space for replacements)
|
|
newDataBuffer = (uint8_t *)malloc(resource.dataSize);
|
|
if (!newDataBuffer) {
|
|
NSLogger(@"[reMarkable] Failed to allocate new data buffer (%zu bytes)", resource.dataSize);
|
|
free(resource.tree);
|
|
clearReplacementEntries();
|
|
pthread_mutex_unlock(&gResourceMutex);
|
|
return original_qRegisterResourceData(version, tree, name, data);
|
|
}
|
|
|
|
// Copy original data
|
|
memcpy(newDataBuffer, data, resource.originalDataSize);
|
|
|
|
// Copy replacement entries to their designated offsets
|
|
struct ReplacementEntry *entry = getReplacementEntries();
|
|
while (entry) {
|
|
// Write size prefix (4 bytes, big-endian)
|
|
writeUint32(newDataBuffer, (int)entry->copyToOffset, (uint32_t)entry->size);
|
|
// Write data after size prefix
|
|
memcpy(newDataBuffer + entry->copyToOffset + 4, entry->data, entry->size);
|
|
|
|
NSLogger(@"[reMarkable] Copied replacement for node %d at offset %zu (%zu bytes)",
|
|
entry->node, entry->copyToOffset, entry->size);
|
|
|
|
entry = entry->next;
|
|
}
|
|
|
|
finalTree = resource.tree;
|
|
finalData = newDataBuffer;
|
|
|
|
NSLogger(@"[reMarkable] Data buffer rebuilt: original %zu bytes -> new %zu bytes",
|
|
resource.originalDataSize, resource.dataSize);
|
|
}
|
|
|
|
int status = original_qRegisterResourceData(version, finalTree, name, finalData);
|
|
|
|
// Cleanup
|
|
clearReplacementEntries();
|
|
if (resource.tree && resource.entriesAffected == 0) {
|
|
free(resource.tree);
|
|
}
|
|
// Note: We intentionally don't free newDataBuffer or resource.tree when entriesAffected > 0
|
|
// because Qt will use these buffers for the lifetime of the application
|
|
|
|
pthread_mutex_unlock(&gResourceMutex);
|
|
return status;
|
|
}
|
|
#endif // BUILD_MODE_QMLREBUILD
|
|
|
|
@end |