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:
√(noham)²
2025-12-06 16:47:15 +01:00
parent 9322b0319e
commit 55a15fb035
4 changed files with 272 additions and 15 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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