mirror of
https://github.com/NohamR/RMHook.git
synced 2026-01-09 05:58:12 +00:00
Add QML resource replacement support for specific files
Implements a mechanism to replace specific QML resource files at runtime by reading replacement files from a designated directory. Updates the resource registration hook to rebuild resource data tables when replacements are present, and adds utility functions and structures for managing replacement entries. Only selected files are eligible for replacement, and the README is updated with instructions for using this feature.
This commit is contained in:
11
README.md
11
README.md
@@ -122,6 +122,9 @@ If the config file doesn't exist, it will be created automatically with default
|
||||
|
||||
## Credits
|
||||
- xovi-rmfakecloud: [asivery/xovi-rmfakecloud](https://github.com/asivery/xovi-rmfakecloud) - Original hooking information
|
||||
- rm-xovi-extensions: [asivery/rm-xovi-extensions](https://github.com/asivery/rm-xovi-extensions) - Extension framework for reMarkable, used as reference for hooking Qt functions
|
||||
- [qt-resource-rebuilder](https://github.com/asivery/rm-xovi-extensions/tree/master/qt-resource-rebuilder)
|
||||
- [xovi-message-broker](https://github.com/asivery/rm-xovi-extensions/tree/master/xovi-message-broker)
|
||||
- tinyhook: [Antibioticss/tinyhook](https://github.com/Antibioticss/tinyhook/) - Function hooking framework
|
||||
- rmfakecloud: [ddvk/rmfakecloud](https://github.com/ddvk/rmfakecloud) - Self-hosted reMarkable cloud
|
||||
- optool: [alexzielenski/optool](https://github.com/alexzielenski/optool) - Mach-O binary modification tool
|
||||
@@ -159,10 +162,16 @@ The build script supports different modes for various use cases:
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| `rmfakecloud` | Redirect reMarkable cloud to rmfakecloud server (default) |
|
||||
| `qmldiff` | Qt resource data registration hooking (WIP) |
|
||||
| `qmldiff` | Qt resource data registration hooking for QML replacement |
|
||||
| `dev` | Development/reverse engineering mode with all hooks |
|
||||
| `all` | Enable all modes |
|
||||
|
||||
**Note (qmldiff mode):** When using the `qmldiff` feature, you must clear the Qt QML cache before launching the app:
|
||||
```bash
|
||||
rm -rf ~/Library/Caches/remarkable
|
||||
```
|
||||
Qt caches compiled QML files, so changes to QML resources won't take effect until the cache is cleared.
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
./scripts/build.sh # Build with rmfakecloud mode (default)
|
||||
|
||||
@@ -442,33 +442,90 @@ extern "C" int hooked_qRegisterResourceData(
|
||||
}
|
||||
|
||||
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);
|
||||
processNode(&resource, 0, "");
|
||||
|
||||
// Make a writable copy of the tree (we need to modify offsets)
|
||||
resource.tree = (uint8_t *)malloc(resource.treeSize);
|
||||
if (resource.tree) {
|
||||
memcpy(resource.tree, tree, 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);
|
||||
}
|
||||
|
||||
NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p (size:%zu) name:%p (size:%zu) data:%p (size:%zu)",
|
||||
version, tree, resource.treeSize, name, resource.nameSize, data, resource.dataSize);
|
||||
|
||||
int status = original_qRegisterResourceData(version, tree, name, data);
|
||||
pthread_mutex_unlock(&gResourceMutex);
|
||||
if (resource.tree) {
|
||||
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_QMLDIFF
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -19,6 +20,16 @@ struct ResourceRoot {
|
||||
int entriesAffected;
|
||||
};
|
||||
|
||||
// Replacement entry for storing new data to be appended
|
||||
struct ReplacementEntry {
|
||||
int node;
|
||||
uint8_t *data;
|
||||
size_t size;
|
||||
size_t copyToOffset;
|
||||
bool freeAfterwards;
|
||||
struct ReplacementEntry *next;
|
||||
};
|
||||
|
||||
#define TREE_ENTRY_SIZE 22
|
||||
#define DIRECTORY 0x02
|
||||
|
||||
@@ -35,8 +46,14 @@ void statArchive(struct ResourceRoot *root, int node);
|
||||
void processNode(struct ResourceRoot *root, int node, const char *rootName);
|
||||
void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char *rootName, const char *fileName, uint16_t flags);
|
||||
|
||||
// Replacement utilities
|
||||
void addReplacementEntry(struct ReplacementEntry *entry);
|
||||
struct ReplacementEntry *getReplacementEntries(void);
|
||||
void clearReplacementEntries(void);
|
||||
void replaceNode(struct ResourceRoot *root, int node, const char *fullPath, int treeOffset);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ResourceUtils_h */
|
||||
#endif
|
||||
|
||||
@@ -344,6 +344,147 @@ void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char
|
||||
}
|
||||
}
|
||||
|
||||
// List of files to process with replaceNode
|
||||
static const char *kFilesToReplace[] = {
|
||||
"/qml/client/dialogs/ExportDialog.qml",
|
||||
"/qml/client/settings/GeneralSettings.qml",
|
||||
NULL // Sentinel to mark end of list
|
||||
};
|
||||
|
||||
static bool shouldReplaceFile(const char *fullPath) {
|
||||
if (!fullPath) return false;
|
||||
for (int i = 0; kFilesToReplace[i] != NULL; i++) {
|
||||
if (strcmp(fullPath, kFilesToReplace[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the path to replacement files directory
|
||||
static NSString *ReMarkableReplacementDirectory(void) {
|
||||
static NSString *replacementDirectory = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *preferencesDir = ReMarkablePreferencesDirectory();
|
||||
NSString *candidate = [preferencesDir stringByAppendingPathComponent:@"replacements"];
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSError *error = nil;
|
||||
if (![fileManager fileExistsAtPath:candidate]) {
|
||||
if (![fileManager createDirectoryAtPath:candidate withIntermediateDirectories:YES attributes:nil error:&error]) {
|
||||
NSLogger(@"[reMarkable] Failed to create replacements directory %@: %@", candidate, error);
|
||||
}
|
||||
}
|
||||
replacementDirectory = [candidate copy];
|
||||
});
|
||||
return replacementDirectory;
|
||||
}
|
||||
|
||||
// Global linked list of replacement entries
|
||||
static struct ReplacementEntry *g_replacementEntries = NULL;
|
||||
|
||||
void addReplacementEntry(struct ReplacementEntry *entry) {
|
||||
entry->next = g_replacementEntries;
|
||||
g_replacementEntries = entry;
|
||||
}
|
||||
|
||||
struct ReplacementEntry *getReplacementEntries(void) {
|
||||
return g_replacementEntries;
|
||||
}
|
||||
|
||||
void clearReplacementEntries(void) {
|
||||
struct ReplacementEntry *current = g_replacementEntries;
|
||||
while (current) {
|
||||
struct ReplacementEntry *next = current->next;
|
||||
if (current->freeAfterwards && current->data) {
|
||||
free(current->data);
|
||||
}
|
||||
free(current);
|
||||
current = next;
|
||||
}
|
||||
g_replacementEntries = NULL;
|
||||
}
|
||||
|
||||
void replaceNode(struct ResourceRoot *root, int node, const char *fullPath, int treeOffset) {
|
||||
NSLogger(@"[reMarkable] replaceNode called for: %s", fullPath);
|
||||
|
||||
if (!root || !root->tree || !fullPath) {
|
||||
NSLogger(@"[reMarkable] replaceNode: invalid parameters");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build path to replacement file on disk
|
||||
NSString *replacementDir = ReMarkableReplacementDirectory();
|
||||
if (![replacementDir length]) {
|
||||
NSLogger(@"[reMarkable] replaceNode: no replacement directory");
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *relativePath = [NSString stringWithUTF8String:fullPath];
|
||||
if ([relativePath hasPrefix:@"/"]) {
|
||||
relativePath = [relativePath substringFromIndex:1];
|
||||
}
|
||||
|
||||
NSString *replacementFilePath = [replacementDir stringByAppendingPathComponent:relativePath];
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
|
||||
if (![fileManager fileExistsAtPath:replacementFilePath]) {
|
||||
NSLogger(@"[reMarkable] replaceNode: replacement file not found at %@", replacementFilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the replacement file
|
||||
NSError *readError = nil;
|
||||
NSData *replacementData = [NSData dataWithContentsOfFile:replacementFilePath options:0 error:&readError];
|
||||
if (!replacementData || readError) {
|
||||
NSLogger(@"[reMarkable] replaceNode: failed to read replacement file %@: %@", replacementFilePath, readError);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t dataSize = [replacementData length];
|
||||
NSLogger(@"[reMarkable] replaceNode: loaded replacement file %@ (%zu bytes)", replacementFilePath, dataSize);
|
||||
|
||||
// Allocate and copy the replacement data
|
||||
uint8_t *newData = (uint8_t *)malloc(dataSize);
|
||||
if (!newData) {
|
||||
NSLogger(@"[reMarkable] replaceNode: failed to allocate %zu bytes", dataSize);
|
||||
return;
|
||||
}
|
||||
memcpy(newData, [replacementData bytes], dataSize);
|
||||
|
||||
// Create a replacement entry
|
||||
struct ReplacementEntry *entry = (struct ReplacementEntry *)malloc(sizeof(struct ReplacementEntry));
|
||||
if (!entry) {
|
||||
NSLogger(@"[reMarkable] replaceNode: failed to allocate replacement entry");
|
||||
free(newData);
|
||||
return;
|
||||
}
|
||||
|
||||
entry->node = node;
|
||||
entry->data = newData;
|
||||
entry->size = dataSize;
|
||||
entry->freeAfterwards = true;
|
||||
entry->copyToOffset = root->dataSize; // Will be appended at the end of data
|
||||
entry->next = NULL;
|
||||
|
||||
// Update the tree entry:
|
||||
writeUint16(root->tree, treeOffset - 2, 0); // Set flag to raw (uncompressed)
|
||||
writeUint32(root->tree, treeOffset + 4, (uint32_t)entry->copyToOffset); // Update data offset
|
||||
|
||||
NSLogger(@"[reMarkable] replaceNode: updated tree - flags at offset %d, dataOffset at offset %d -> %zu",
|
||||
treeOffset - 2, treeOffset + 4, entry->copyToOffset);
|
||||
|
||||
// Update dataSize to account for the new data (size prefix + data)
|
||||
root->dataSize += entry->size + 4;
|
||||
root->entriesAffected++;
|
||||
|
||||
// Add to replacement entries list
|
||||
addReplacementEntry(entry);
|
||||
|
||||
NSLogger(@"[reMarkable] replaceNode: marked for replacement - %s (new offset: %zu, size: %zu)",
|
||||
fullPath, entry->copyToOffset, entry->size);
|
||||
}
|
||||
|
||||
void processNode(struct ResourceRoot *root, int node, const char *rootName) {
|
||||
int offset = findOffset(node) + 4;
|
||||
uint16_t flags = readUInt16(root->tree, offset);
|
||||
@@ -375,9 +516,42 @@ void processNode(struct ResourceRoot *root, int node, const char *rootName) {
|
||||
|
||||
free(tempRoot);
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Processing node %d: %s%s", (int)node, rootName ? rootName : "", nameBuffer);
|
||||
uint16_t fileFlags = readUInt16(root->tree, offset - 2);
|
||||
ReMarkableDumpResourceFile(root, node, rootName ? rootName : "", nameBuffer, fileFlags);
|
||||
uint16_t fileFlag = readUInt16(root->tree, offset - 2);
|
||||
const char *type;
|
||||
if (fileFlag == 1) {
|
||||
type = "zlib";
|
||||
} else if (fileFlag == 4) {
|
||||
type = "zstd";
|
||||
} else if (fileFlag == 0) {
|
||||
type = "raw";
|
||||
} else {
|
||||
type = "unknown";
|
||||
}
|
||||
|
||||
// Build full path: rootName + nameBuffer
|
||||
const size_t rootLen = rootName ? strlen(rootName) : 0;
|
||||
const size_t nameLen = strlen(nameBuffer);
|
||||
char *fullPath = (char *)malloc(rootLen + nameLen + 1);
|
||||
if (fullPath) {
|
||||
if (rootLen > 0) {
|
||||
memcpy(fullPath, rootName, rootLen);
|
||||
}
|
||||
memcpy(fullPath + rootLen, nameBuffer, nameLen);
|
||||
fullPath[rootLen + nameLen] = '\0';
|
||||
|
||||
NSLogger(@"[reMarkable] Processing node %d: %s (type: %s)", (int)node, fullPath, type);
|
||||
|
||||
// Check if this file should be replaced
|
||||
if (shouldReplaceFile(fullPath)) {
|
||||
replaceNode(root, node, fullPath, offset);
|
||||
}
|
||||
|
||||
free(fullPath);
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Processing node %d: %s%s (type: %s)", (int)node, rootName ? rootName : "", nameBuffer, type);
|
||||
}
|
||||
|
||||
// ReMarkableDumpResourceFile(root, node, rootName ? rootName : "", nameBuffer, fileFlag);
|
||||
}
|
||||
}
|
||||
#endif // BUILD_MODE_QMLDIFF
|
||||
Reference in New Issue
Block a user