diff --git a/README.md b/README.md index e6ddc10..410e8d3 100644 --- a/README.md +++ b/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) diff --git a/src/reMarkable/reMarkable.m b/src/reMarkable/reMarkable.m index 7760156..512f8de 100644 --- a/src/reMarkable/reMarkable.m +++ b/src/reMarkable/reMarkable.m @@ -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 diff --git a/src/utils/ResourceUtils.h b/src/utils/ResourceUtils.h index a6776e4..f7ca9f7 100644 --- a/src/utils/ResourceUtils.h +++ b/src/utils/ResourceUtils.h @@ -3,6 +3,7 @@ #include #include +#include #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 diff --git a/src/utils/ResourceUtils.m b/src/utils/ResourceUtils.m index d102614..5922a98 100644 --- a/src/utils/ResourceUtils.m +++ b/src/utils/ResourceUtils.m @@ -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 \ No newline at end of file