5 Commits
v1.1 ... dev

Author SHA1 Message Date
√(noham)²
400e698765 Add native HTTP server, CLI scripts and docs
Implement a native macOS HTTP server for RMHook and wire it into the app. Adds HttpServer.h/.mm (CFSocket-based) with start/stop/isRunning APIs, broadcasts incoming POST /exportFile and /documentAccepted requests to the QML MessageBroker, and starts the server on port 8080 from reMarkable.m. Update CMakeLists to include HttpServer.mm and link Qt Qml when building qmlrebuild mode. Add documentation (docs/HTTP_SERVER.md) and QML snippets for MessageBroker integration, plus Python helper scripts (scripts/http_server.py and scripts/test_http_server.py) for invoking the endpoints. Also add small MessageBroker examples (src/utils/mb.m, src/utils/mb.qml) and update ResourceUtils to include additional QML resources.
2026-02-02 19:52:58 +01:00
√(noham)²
3e89d8118e Add MessageBroker for QML and native communication
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.
2025-12-06 17:39:06 +01:00
√(noham)²
3765bcd584 Rename build mode from qmldiff to qmlrebuild
Replaces all references to the 'qmldiff' build mode with 'qmlrebuild' across CMakeLists.txt, README.md, build scripts, and source files.
2025-12-06 16:51:28 +01:00
√(noham)²
55a15fb035 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.
2025-12-06 16:47:15 +01:00
√(noham)²
9322b0319e Move dev hooks to separate DevHooks files
Extracted development/reverse engineering hooks and helpers from reMarkable.m into new DevHooks.h and DevHooks.m files. Updated CMakeLists.txt to include the new files and their directory. This improves code organization and maintainability for development-only instrumentation.
2025-12-05 18:28:59 +01:00
19 changed files with 2072 additions and 242 deletions

View File

@@ -8,10 +8,10 @@ set(CMAKE_CXX_STANDARD 17)
# Build mode options
# - rmfakecloud: Redirect reMarkable cloud to rmfakecloud server (default)
# - qmldiff: Qt resource data registration hooking (WIP)
# - qmlrebuild: Qt resource data registration hooking
# - dev: Development/reverse engineering mode with all hooks
option(BUILD_MODE_RMFAKECLOUD "Build with rmfakecloud support" ON)
option(BUILD_MODE_QMLDIFF "Build with QML diff/resource hooking" OFF)
option(BUILD_MODE_QMLREBUILD "Build with QML resource rebuilding" OFF)
option(BUILD_MODE_DEV "Build with dev/reverse engineering hooks" OFF)
# Compiler settings for macOS
@@ -28,6 +28,8 @@ set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
include_directories(
${PROJECT_ROOT_DIR}/src/core
${PROJECT_ROOT_DIR}/src/utils
${PROJECT_ROOT_DIR}/src/reMarkable
${PROJECT_ROOT_DIR}/libs/include
)
# Find required libraries
@@ -55,13 +57,13 @@ foreach(_qt_root ${_qt_candidate_roots})
endif()
endforeach()
find_package(Qt6 COMPONENTS Core Network WebSockets QUIET)
find_package(Qt6 COMPONENTS Core Network WebSockets Qml QUIET)
if(Qt6_FOUND)
set(QT_LIB_TARGETS Qt6::Core Qt6::Network Qt6::WebSockets)
set(QT_LIB_TARGETS Qt6::Core Qt6::Network Qt6::WebSockets Qt6::Qml)
else()
find_package(Qt5 COMPONENTS Core Network WebSockets QUIET)
find_package(Qt5 COMPONENTS Core Network WebSockets Qml QUIET)
if(Qt5_FOUND)
set(QT_LIB_TARGETS Qt5::Core Qt5::Network Qt5::WebSockets)
set(QT_LIB_TARGETS Qt5::Core Qt5::Network Qt5::WebSockets Qt5::Qml)
endif()
endif()
@@ -79,6 +81,7 @@ set(COMMON_SOURCES
# reMarkable dylib
set(REMARKABLE_SOURCES
${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m
${PROJECT_ROOT_DIR}/src/reMarkable/DevHooks.m
)
add_library(reMarkable SHARED
@@ -102,15 +105,34 @@ set_target_properties(reMarkable PROPERTIES
add_definitions(-DQT_NO_VERSION_TAGGING)
# Add build mode compile definitions
# Add build mode compile definitions and conditionally add sources
if(BUILD_MODE_RMFAKECLOUD)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_RMFAKECLOUD=1)
message(STATUS "Build mode: rmfakecloud (cloud redirection)")
endif()
if(BUILD_MODE_QMLDIFF)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLDIFF=1)
message(STATUS "Build mode: qmldiff (resource hooking)")
if(BUILD_MODE_QMLREBUILD)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1)
# Enable Qt MOC for MessageBroker (HttpServer is pure Obj-C/Foundation, no MOC needed)
set_target_properties(reMarkable PROPERTIES AUTOMOC ON)
# Add MessageBroker (needs MOC) and HttpServer (native macOS)
target_sources(reMarkable PRIVATE
${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm
${PROJECT_ROOT_DIR}/src/utils/HttpServer.mm
)
find_package(Qt6 COMPONENTS Qml QUIET)
if(Qt6Qml_FOUND)
target_link_libraries(reMarkable PRIVATE Qt6::Qml)
else()
find_package(Qt5 COMPONENTS Qml QUIET)
if(Qt5Qml_FOUND)
target_link_libraries(reMarkable PRIVATE Qt5::Qml)
endif()
endif()
message(STATUS "Build mode: qmlrebuild (resource hooking)")
endif()
if(BUILD_MODE_DEV)

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) |
| `qmlrebuild` | Qt resource data registration hooking for QML replacement |
| `dev` | Development/reverse engineering mode with all hooks |
| `all` | Enable all modes |
**Note (qmlrebuild mode):** When using the `qmlrebuild` 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

@@ -0,0 +1,54 @@
// Add this MessageBroker to a QML component that has access to PlatformHelpers
// This listens for documentAccepted signals from the HTTP server
// and calls PlatformHelpers.documentAccepted()
import net.noham.MessageBroker
MessageBroker {
id: documentAcceptedBroker
listeningFor: ["documentAccepted"]
onSignalReceived: (signal, message) => {
console.log("[DocumentAccepted.MessageBroker] Received signal:", signal);
console.log("[DocumentAccepted.MessageBroker] Message data:", message);
try {
// Parse JSON message from HTTP server
const data = JSON.parse(message);
console.log("[DocumentAccepted.MessageBroker] Parsed request:", JSON.stringify(data));
// Extract parameters with defaults
const url = data.url || "";
const password = data.password || "";
const directoryId = data.directoryId || "";
const flag1 = data.flag1 !== undefined ? data.flag1 : false;
const flag2 = data.flag2 !== undefined ? data.flag2 : false;
console.log("[DocumentAccepted.MessageBroker] Parameters:");
console.log("[DocumentAccepted.MessageBroker] url:", url);
console.log("[DocumentAccepted.MessageBroker] password:", password ? "(set)" : "(empty)");
console.log("[DocumentAccepted.MessageBroker] directoryId:", directoryId);
console.log("[DocumentAccepted.MessageBroker] flag1:", flag1);
console.log("[DocumentAccepted.MessageBroker] flag2:", flag2);
// Validate required parameters
if (!url) {
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'url' parameter");
return;
}
if (!directoryId) {
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'directoryId' parameter");
return;
}
// Call PlatformHelpers.documentAccepted
console.log("[DocumentAccepted.MessageBroker] Calling PlatformHelpers.documentAccepted...");
PlatformHelpers.documentAccepted(url, password, directoryId, flag1, flag2);
console.log("[DocumentAccepted.MessageBroker] Document accepted successfully");
} catch (error) {
console.error("[DocumentAccepted.MessageBroker] ERROR parsing request:", error);
console.error("[DocumentAccepted.MessageBroker] Message was:", message);
}
}
}

View File

@@ -0,0 +1,67 @@
// Add this MessageBroker to ExportDialog.qml after the PopupDialog definition
// This should be added near the top of the component, after property definitions
import net.noham.MessageBroker
// ... existing properties ...
// MessageBroker for HTTP server export requests
MessageBroker {
id: exportBroker
listeningFor: ["exportFile"]
onSignalReceived: (signal, message) => {
console.log("[ExportDialog.MessageBroker] Received signal:", signal);
console.log("[ExportDialog.MessageBroker] Message data:", message);
try {
// Parse JSON message from HTTP server
const data = JSON.parse(message);
console.log("[ExportDialog.MessageBroker] Parsed export request:", JSON.stringify(data));
// Extract parameters
const target = data.target || "";
const documentId = data.id || data.documentId || "";
const format = data.format !== undefined ? data.format : PlatformHelpers.ExportPdf;
const password = data.password || "";
const keepPassword = data.keepPassword !== undefined ? data.keepPassword : true;
const grayscale = data.grayscale !== undefined ? data.grayscale : false;
const pageSelection = data.pageSelection || [];
console.log("[ExportDialog.MessageBroker] Export parameters:");
console.log("[ExportDialog.MessageBroker] target:", target);
console.log("[ExportDialog.MessageBroker] documentId:", documentId);
console.log("[ExportDialog.MessageBroker] format:", format);
console.log("[ExportDialog.MessageBroker] keepPassword:", keepPassword);
console.log("[ExportDialog.MessageBroker] grayscale:", grayscale);
console.log("[ExportDialog.MessageBroker] pageSelection:", JSON.stringify(pageSelection));
// Validate required parameters
if (!target) {
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'target' parameter");
return;
}
if (!documentId) {
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'id' or 'documentId' parameter");
return;
}
// Call PlatformHelpers.exportFile
console.log("[ExportDialog.MessageBroker] Calling PlatformHelpers.exportFile...");
if (pageSelection && pageSelection.length > 0) {
console.log("[ExportDialog.MessageBroker] Exporting with page selection");
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale, pageSelection);
} else {
console.log("[ExportDialog.MessageBroker] Exporting full document");
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale);
}
console.log("[ExportDialog.MessageBroker] Export completed successfully");
} catch (error) {
console.error("[ExportDialog.MessageBroker] ERROR parsing export request:", error);
console.error("[ExportDialog.MessageBroker] Message was:", message);
}
}
}

293
docs/HTTP_SERVER.md Normal file
View File

@@ -0,0 +1,293 @@
# HTTP Server for Export Requests
The RMHook dylib includes an HTTP server that accepts export requests and forwards them to the reMarkable application via MessageBroker.
## Server Details
- **Host**: `localhost`
- **Port**: `8080`
- **Base URL**: `http://localhost:8080`
## Endpoints
### `POST /exportFile`
Trigger a document export from the reMarkable application.
**Request Body** (JSON):
```json
{
"target": "file:///Users/username/Desktop/output.pdf",
"id": "document-uuid-here",
"format": 0,
"password": "",
"keepPassword": true,
"grayscale": false,
"pageSelection": []
}
```
**Parameters**:
- `target` (string, required): File path or folder URL for the export. Use `file://` prefix for local paths.
- `id` or `documentId` (string, required): The UUID of the document to export.
- `format` (integer, optional): Export format. Default: `0` (PDF)
- `0`: PDF
- `1`: PNG
- `2`: SVG
- `3`: RmBundle
- `4`: RmHtml
- `password` (string, optional): Password for password-protected documents. Default: `""`
- `keepPassword` (boolean, optional): Whether to keep password protection on PDF exports. Default: `true`
- `grayscale` (boolean, optional): Export with grayscale pens. Default: `false`
- `pageSelection` (array, optional): Array of page indices to export. If empty or omitted, exports all pages. Example: `[0, 1, 2]`
**Response**:
```json
{
"status": "success",
"message": "Export request sent to application"
}
```
**Error Response**:
```json
{
"error": "Error description"
}
```
### `POST /documentAccepted`
Import/accept a document into the reMarkable application.
**Request Body** (JSON):
```json
{
"url": "file:///Users/username/Desktop/test.pdf",
"password": "",
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
"flag1": false,
"flag2": false
}
```
**Parameters**:
- `url` (string, required): File URL to import. Use `file://` prefix for local paths.
- `password` (string, optional): Password for password-protected documents. Default: `""`
- `directoryId` (string, required): The UUID of the target directory/folder where the document should be imported.
- `flag1` (boolean, optional): Purpose unclear. Default: `false`
- `flag2` (boolean, optional): Purpose unclear. Default: `false`
**Response**:
```json
{
"status": "success",
"message": "Document accepted request sent to application"
}
```
**Error Response**:
```json
{
"error": "Error description"
}
```
### `GET /health`
Health check endpoint.
**Response**:
```json
{
"status": "ok",
"service": "RMHook HTTP Server"
}
```
## Example Requests
### Export a document to PDF
```bash
curl -X POST http://localhost:8080/exportFile \
-H "Content-Type: application/json" \
-d '{
"target": "file:///Users/noham/Desktop/export.pdf",
"id": "12345678-1234-1234-1234-123456789abc",
"format": 0,
"grayscale": false,
"keepPassword": true
}'
```
### Export specific pages as PNG
```bash
curl -X POST http://localhost:8080/exportFile \
-H "Content-Type: application/json" \
-d '{
"target": "file:///Users/noham/Desktop/pages",
"id": "12345678-1234-1234-1234-123456789abc",
"format": 1,
"pageSelection": [0, 1, 2],
"grayscale": true
}'
```
### Export to RmBundle format
```bash
curl -X POST http://localhost:8080/exportFile \
-H "Content-Type: application/json" \
-d '{
"target": "file:///Users/noham/Desktop/MyDocument",
"id": "12345678-1234-1234-1234-123456789abc",
"format": 3
}'
```
### Import/Accept a document
```bash
curl -X POST http://localhost:8080/documentAccepted \
-H "Content-Type: application/json" \
-d '{
"url": "file:///Users/noham/Desktop/test.pdf",
"password": "",
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
"flag1": false,
"flag2": false
}'
```
### Python Example - Export
```python
import requests
import json
# Export configuration
export_data = {
"target": "file:///Users/noham/Desktop/output.pdf",
"id": "12345678-1234-1234-1234-123456789abc",
"format": 0, # PDF
"grayscale": False,
"keepPassword": True
}
# Send request
response = requests.post(
"http://localhost:8080/exportFile",
json=export_data
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
```
### Python Example - Import Document
```python
import requests
# Import configuration
import_data = {
"url": "file:///Users/noham/Desktop/test.pdf",
"password": "",
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
"flag1": False,
"flag2": False
}
# Send request
response = requests.post(
"http://localhost:8080/documentAccepted",
json=import_data
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
```
### JavaScript Example - Export
```javascript
// Export configuration
const exportData = {
target: "file:///Users/noham/Desktop/output.pdf",
id: "12345678-1234-1234-1234-123456789abc",
format: 0, // PDF
grayscale: false,
keepPassword: true
};
// Send request
fetch("http://localhost:8080/exportFile", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(exportData)
})
.then(response => response.json())
.then(data => console.log("Success:", data))
.catch(error => console.error("Error:", error));
```
### JavaScript Example - Import Document
```javascript
// Import configuration
const importData = {
url: "file:///Users/noham/Desktop/test.pdf",
password: "",
directoryId: "2166c19d-d2cc-456c-9f0e-49482031092a",
flag1: false,
flag2: false
};
// Send request
fetch("http://localhost:8080/documentAccepted", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(importData)
})
.then(response => response.json())
.then(data => console.log("Success:", data))
.catch(error => console.error("Error:", error));
```
## Integration with QML
### Export Dialog Integration
Add the MessageBroker snippet from `docs/ExportDialog_MessageBroker_snippet.qml` to your ExportDialog.qml replacement file. This will enable the QML side to receive export requests from the HTTP server.
The MessageBroker listens for "exportFile" signals and automatically calls `PlatformHelpers.exportFile()` with the provided parameters.
### Document Import Integration
Add the MessageBroker snippet from `docs/DocumentAccepted_MessageBroker_snippet.qml` to a QML component (such as GeneralSettings.qml) that has access to PlatformHelpers. This will enable the QML side to receive document import requests from the HTTP server.
The MessageBroker listens for "documentAccepted" signals and automatically calls `PlatformHelpers.documentAccepted()` with the provided parameters.
## Document ID and Directory ID Discovery
To find document and directory IDs, you can:
1. Check the reMarkable application logs when opening documents or folders
2. Use the reMarkable Cloud API
3. Access the local database at `~/Library/Application Support/remarkable/desktop-app/`
4. For the root directory ID, check the logs when navigating to "My Files"
## Troubleshooting
- Ensure the HTTP server started successfully by checking the logs: `2025-12-08 17:32:22.288 reMarkable[19574:1316287] [HttpServer] HTTP server started successfully on http://localhost:8080`
- Test the health endpoint: `curl http://localhost:8080/health`
- Check the Console.app for detailed logging from the MessageBroker and HttpServer
- Verify the document ID and directory ID are correct UUIDs
- Ensure the target/url path is accessible and uses the `file://` prefix
- For imports, verify the target directory exists and is accessible

View File

@@ -3,7 +3,7 @@
# Build modes:
# rmfakecloud - Redirect reMarkable cloud to rmfakecloud server (default)
# qmldiff - Qt resource data registration hooking (WIP)
# qmlrebuild - Qt resource data registration hooking
# dev - Development/reverse engineering mode with all hooks
PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd)
@@ -19,8 +19,8 @@ case "$BUILD_MODE" in
rmfakecloud)
DYLIB_NAME="rmfakecloud.dylib"
;;
qmldiff)
DYLIB_NAME="qmldiff.dylib"
qmlrebuild)
DYLIB_NAME="qmlrebuild.dylib"
;;
dev)
DYLIB_NAME="dev.dylib"
@@ -37,20 +37,20 @@ esac
CMAKE_OPTIONS=""
case "$BUILD_MODE" in
rmfakecloud)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=ON -DBUILD_MODE_QMLDIFF=OFF -DBUILD_MODE_DEV=OFF"
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=ON -DBUILD_MODE_QMLREBUILD=OFF -DBUILD_MODE_DEV=OFF"
;;
qmldiff)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLDIFF=ON -DBUILD_MODE_DEV=OFF"
qmlrebuild)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLREBUILD=ON -DBUILD_MODE_DEV=OFF"
;;
dev)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLDIFF=OFF -DBUILD_MODE_DEV=ON"
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLREBUILD=OFF -DBUILD_MODE_DEV=ON"
;;
all)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=ON -DBUILD_MODE_QMLDIFF=ON -DBUILD_MODE_DEV=ON"
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=ON -DBUILD_MODE_QMLREBUILD=ON -DBUILD_MODE_DEV=ON"
;;
*)
echo "❌ Unknown build mode: $BUILD_MODE"
echo "Available modes: rmfakecloud (default), qmldiff, dev, all"
echo "Available modes: rmfakecloud (default), qmlrebuild, dev, all"
exit 1
;;
esac

200
scripts/http_server.py Normal file
View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python3
import requests
import json
import sys
import argparse
BASE_URL = "http://localhost:8080"
def export_document(document_id, target_path, format_type=0, grayscale=False,
keep_password=True, password="", page_selection=None):
"""
Export a document via HTTP API
Args:
document_id: UUID of the document to export
target_path: Target path for the export (use file:// prefix)
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
grayscale: Export with grayscale pens
keep_password: Keep password protection (for PDFs)
password: Password for protected documents
page_selection: List of page indices to export (None = all pages)
"""
print(f"\nExporting document {document_id}...")
print(f"Target: {target_path}")
print(f"Format: {format_type}")
data = {
"target": target_path,
"id": document_id,
"format": format_type,
"grayscale": grayscale,
"keepPassword": keep_password,
"password": password
}
if page_selection:
data["pageSelection"] = page_selection
print(f"Pages: {page_selection}")
try:
response = requests.post(
f"{BASE_URL}/exportFile",
json=data,
timeout=10
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def import_document(file_url, directory_id, password="", flag1=False, flag2=False):
"""
Import a document via HTTP API
Args:
file_url: File URL to import (use file:// prefix)
directory_id: UUID of the target directory
password: Password for protected documents (optional)
flag1: Additional flag parameter
flag2: Additional flag parameter
"""
print(f"\nImporting document from {file_url}...")
print(f"Target directory: {directory_id}")
data = {
"url": file_url,
"password": password,
"directoryId": directory_id,
"flag1": flag1,
"flag2": flag2
}
try:
response = requests.post(
f"{BASE_URL}/documentAccepted",
json=data,
timeout=10
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def main():
parser = argparse.ArgumentParser(
description='reMarkable HTTP Server API Client',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
Export a document as PDF:
%(prog)s export 12345678-1234-1234-1234-123456789abc file:///Users/noham/Desktop/test.pdf
Export as PNG with grayscale:
%(prog)s export <doc-id> <target> --format 1 --grayscale
Export specific pages:
%(prog)s export <doc-id> <target> --pages 0 1 2
Import a document:
%(prog)s import file:///Users/noham/Desktop/test.pdf 2166c19d-d2cc-456c-9f0e-49482031092a
Import with password:
%(prog)s import <file-url> <directory-id> --password mypassword
'''
)
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Export command
export_parser = subparsers.add_parser('export', help='Export a document')
export_parser.add_argument('document_id', help='UUID of the document to export')
export_parser.add_argument('target_path', help='Target path for export (use file:// prefix)')
export_parser.add_argument(
'--format', '-f',
type=int,
default=0,
choices=[0, 1, 2, 3, 4],
help='Export format: 0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml (default: 0)'
)
export_parser.add_argument(
'--grayscale', '-g',
action='store_true',
help='Export with grayscale pens'
)
export_parser.add_argument(
'--no-keep-password',
action='store_true',
help='Do not keep password protection (for PDFs)'
)
export_parser.add_argument(
'--password', '-p',
default='',
help='Password for protected documents'
)
export_parser.add_argument(
'--pages',
type=int,
nargs='+',
help='List of page indices to export (default: all pages)'
)
# Import command
import_parser = subparsers.add_parser('import', help='Import a document')
import_parser.add_argument('file_url', help='File URL to import (use file:// prefix)')
import_parser.add_argument('directory_id', help='UUID of the target directory')
import_parser.add_argument(
'--password', '-p',
default='',
help='Password for protected documents'
)
import_parser.add_argument(
'--flag1',
action='store_true',
help='Additional flag parameter'
)
import_parser.add_argument(
'--flag2',
action='store_true',
help='Additional flag parameter'
)
args = parser.parse_args()
if args.command == 'export':
success = export_document(
document_id=args.document_id,
target_path=args.target_path,
format_type=args.format,
grayscale=args.grayscale,
keep_password=not args.no_keep_password,
password=args.password,
page_selection=args.pages
)
if success:
print("\n✅ Export request sent successfully!")
else:
print("\n❌ Export request failed!")
sys.exit(1)
elif args.command == 'import':
success = import_document(
file_url=args.file_url,
directory_id=args.directory_id,
password=args.password,
flag1=args.flag1,
flag2=args.flag2
)
if success:
print("\n✅ Import request sent successfully!")
else:
print("\n❌ Import request failed!")
sys.exit(1)
else:
parser.print_help()
if __name__ == "__main__":
main()

165
scripts/test_http_server.py Normal file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
Test script for RMHook HTTP Server
Demonstrates how to trigger exports and imports via HTTP API
"""
import requests
import json
import sys
BASE_URL = "http://localhost:8080"
def test_health():
"""Test the health endpoint"""
print("Testing /health endpoint...")
try:
response = requests.get(f"{BASE_URL}/health", timeout=5)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def export_document(document_id, target_path, format_type=0, grayscale=False,
keep_password=True, password="", page_selection=None):
"""
Export a document via HTTP API
Args:
document_id: UUID of the document to export
target_path: Target path for the export (use file:// prefix)
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
grayscale: Export with grayscale pens
keep_password: Keep password protection (for PDFs)
password: Password for protected documents
page_selection: List of page indices to export (None = all pages)
"""
print(f"\nExporting document {document_id}...")
print(f"Target: {target_path}")
print(f"Format: {format_type}")
data = {
"target": target_path,
"id": document_id,
"format": format_type,
"grayscale": grayscale,
"keepPassword": keep_password,
"password": password
}
if page_selection:
data["pageSelection"] = page_selection
print(f"Pages: {page_selection}")
try:
response = requests.post(
f"{BASE_URL}/exportFile",
json=data,
timeout=10
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def import_document(file_url, directory_id, password=""):
"""
Import a document via HTTP API
Args:
file_url: File URL to import (use file:// prefix)
directory_id: UUID of the target directory
password: Password for protected documents (optional)
"""
print(f"\nImporting document from {file_url}...")
print(f"Target directory: {directory_id}")
data = {
"url": file_url,
"password": password,
"directoryId": directory_id,
"flag1": False,
"flag2": False
}
try:
response = requests.post(
f"{BASE_URL}/documentAccepted",
json=data,
timeout=10
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def main():
print("=" * 60)
print("RMHook HTTP Server Test Script")
print("=" * 60)
# Test health endpoint
if not test_health():
print("\n❌ Health check failed. Is the server running?")
print("Make sure reMarkable app is running with the dylib injected.")
sys.exit(1)
print("\n✅ Server is running!")
# Command line interface
if len(sys.argv) < 2:
print("\n" + "=" * 60)
print("Usage Examples")
print("=" * 60)
print("\n1. Export a document:")
print(' python3 test_http_server.py export <doc-id> <target-path> [format] [grayscale]')
print("\n Example:")
print(' python3 test_http_server.py export "abc-123" "file:///Users/noham/Desktop/test.pdf" 0 false')
print("\n2. Import a document:")
print(' python3 test_http_server.py import <file-url> <directory-id>')
print("\n Example:")
print(' python3 test_http_server.py import "file:///Users/noham/Desktop/test.pdf" "2166c19d-d2cc-456c-9f0e-49482031092a"')
sys.exit(0)
command = sys.argv[1].lower()
if command == "export" and len(sys.argv) >= 4:
doc_id = sys.argv[2]
target = sys.argv[3]
format_type = int(sys.argv[4]) if len(sys.argv) > 4 else 0
grayscale = sys.argv[5].lower() == "true" if len(sys.argv) > 5 else False
success = export_document(doc_id, target, format_type, grayscale)
if success:
print("\n✅ Export request sent successfully!")
else:
print("\n❌ Export request failed!")
sys.exit(1)
elif command == "import" and len(sys.argv) >= 4:
file_url = sys.argv[2]
directory_id = sys.argv[3]
password = sys.argv[4] if len(sys.argv) > 4 else ""
success = import_document(file_url, directory_id, password)
if success:
print("\n✅ Import request sent successfully!")
else:
print("\n❌ Import request failed!")
sys.exit(1)
else:
print(f"\n❌ Invalid command or missing arguments: {' '.join(sys.argv[1:])}")
print("Run without arguments to see usage examples.")
sys.exit(1)
if __name__ == "__main__":
main()

47
src/reMarkable/DevHooks.h Normal file
View File

@@ -0,0 +1,47 @@
#ifndef DEV_HOOKS_H
#define DEV_HOOKS_H
#ifdef BUILD_MODE_DEV
#import <Foundation/Foundation.h>
#include <stdint.h>
// Forward declarations for Qt types
class QIODevice;
class QObject;
namespace QtSharedPointer {
struct ExternalRefCountData;
}
extern ssize_t (*original_qIODevice_write)(QIODevice *self, const char *data, int64_t maxSize);
extern int64_t (*original_qmlregister)(int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int, int64_t, int, int64_t);
extern int64_t (*original_function_at_0x100011790)(uint64_t *a1);
extern int64_t (*original_function_at_0x100011CE0)(int64_t, const QObject *, unsigned char, int64_t, QtSharedPointer::ExternalRefCountData *);
extern int64_t (*original_function_at_0x10015A130)(int64_t, int64_t);
extern void (*original_function_at_0x10015BC90)(int64_t, int64_t);
extern int64_t (*original_function_at_0x10016D520)(int64_t, int64_t *, unsigned int, int64_t);
extern void (*original_function_at_0x1001B6EE0)(int64_t, int64_t *, unsigned int);
#ifdef __cplusplus
extern "C" {
#endif
ssize_t hooked_qIODevice_write(QIODevice *self, const char *data, int64_t maxSize);
int64_t hooked_qmlregister(int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int, int64_t, int, int64_t);
int64_t hooked_function_at_0x100011790(uint64_t *a1);
int64_t hooked_function_at_0x100011CE0(int64_t, const QObject *, unsigned char, int64_t, QtSharedPointer::ExternalRefCountData *);
int64_t hooked_function_at_0x10015A130(int64_t, int64_t);
void hooked_function_at_0x10015BC90(int64_t, int64_t);
int64_t hooked_function_at_0x10016D520(int64_t, int64_t *, unsigned int, int64_t);
void hooked_function_at_0x1001B6EE0(int64_t, int64_t *, unsigned int);
#ifdef __cplusplus
}
#endif
void logMemory(const char *label, void *address, size_t length);
void logStackTrace(const char *label);
#endif // BUILD_MODE_DEV
#endif // DEV_HOOKS_H

389
src/reMarkable/DevHooks.m Normal file
View File

@@ -0,0 +1,389 @@
#ifdef BUILD_MODE_DEV
#import "DevHooks.h"
#import "Logger.h"
#import <Foundation/Foundation.h>
#include <stdint.h>
#include <string.h>
#include <QtCore/QIODevice>
#include <QtCore/QObject>
// Original function pointers
ssize_t (*original_qIODevice_write)(QIODevice *self, const char *data, int64_t maxSize) = NULL;
int64_t (*original_qmlregister)(
int64_t a1,
int64_t a2,
int64_t a3,
int64_t a4,
int64_t a5,
int64_t a6,
int a7,
int64_t a8,
int a9,
int64_t a10) = NULL;
int64_t (*original_function_at_0x100011790)(uint64_t *a1) = NULL;
int64_t (*original_function_at_0x100011CE0)(int64_t a1, const QObject *a2, unsigned char a3, int64_t a4, QtSharedPointer::ExternalRefCountData *a5) = NULL;
int64_t (*original_function_at_0x10015A130)(int64_t a1, int64_t a2) = NULL;
void (*original_function_at_0x10015BC90)(int64_t a1, int64_t a2) = NULL;
int64_t (*original_function_at_0x10016D520)(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) = NULL;
void (*original_function_at_0x1001B6EE0)(int64_t a1, int64_t *a2, unsigned int a3) = NULL;
#pragma mark - Helper Functions
void logMemory(const char *label, void *address, size_t length) {
if (!address) {
NSLogger(@"[reMarkable] %s: (null)", label);
return;
}
unsigned char *ptr = (unsigned char *)address;
NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[reMarkable] %s: ", label];
for (size_t i = 0; i < length; i++) {
[hexLine appendFormat:@"%02x ", ptr[i]];
if ((i + 1) % 16 == 0 && i < length - 1) {
NSLogger(@"%@", hexLine);
hexLine = [NSMutableString stringWithString:@"[reMarkable] "];
}
}
// Log remaining bytes if any
if ([hexLine length] > 28) { // More than just the prefix
NSLogger(@"%@", hexLine);
}
}
void logStackTrace(const char *label) {
NSLogger(@"[reMarkable] %s - Stack trace:", label);
NSArray<NSString *> *callStack = [NSThread callStackSymbols];
NSUInteger count = [callStack count];
for (NSUInteger i = 0; i < count; i++) {
NSString *frame = callStack[i];
NSLogger(@"[reMarkable] #%lu: %@", (unsigned long)i, frame);
}
}
#pragma mark - Hook Implementations
extern "C" ssize_t hooked_qIODevice_write(
QIODevice *self,
const char *data,
int64_t maxSize) {
NSLogger(@"[reMarkable] QIODevice::write called with maxSize: %lld", (long long)maxSize);
logStackTrace("QIODevice::write call stack");
logMemory("Data to write", (void *)data, (size_t)(maxSize < 64 ? maxSize : 64));
if (original_qIODevice_write) {
ssize_t result = original_qIODevice_write(self, data, maxSize);
NSLogger(@"[reMarkable] QIODevice::write result: %zd", result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original QIODevice::write not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x100011790(uint64_t *a1) {
NSLogger(@"[reMarkable] Hook at 0x100011790 called!");
NSLogger(@"[reMarkable] a1 = %p", a1);
if (a1) {
NSLogger(@"[reMarkable] *a1 = 0x%llx", (unsigned long long)*a1);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at *a1", (void *)(*a1), 64);
} else {
NSLogger(@"[reMarkable] a1 is NULL");
}
if (original_function_at_0x100011790) {
int64_t result = original_function_at_0x100011790(a1);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x100011790 not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x100011CE0(
int64_t a1,
const QObject *a2,
unsigned char a3,
int64_t a4,
QtSharedPointer::ExternalRefCountData *a5) {
// This function appears to be a QML type registration wrapper
// It calls QQmlPrivate::qmlregister(3, &registrationData)
//
// Based on IDA analysis:
// - a1: stored at offset +0x8 in registration struct (likely type metadata ptr)
// - a2: NOT actually a QObject* - low bits used as: ((_WORD)a2 << 8) | a3
// This suggests a2's low 16 bits are a version/revision number
// - a3: combined with a2 to form v17 (flags/version field)
// - a4: stored at offset +0x18 (likely URI or type info pointer)
// - a5: ExternalRefCountData* for shared pointer ref counting
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[reMarkable] Hook at 0x100011CE0 (QML Type Registration)");
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[reMarkable] a1 (typeMetadata?) = 0x%llx", (unsigned long long)a1);
uint16_t a2_low = (uint16_t)(uintptr_t)a2;
uint16_t combined_v17 = (a2_low << 8) | a3;
NSLogger(@"[reMarkable] a2 (raw) = %p (0x%llx)", a2, (unsigned long long)(uintptr_t)a2);
NSLogger(@"[reMarkable] a2 low 16 bits = 0x%04x (%u)", a2_low, a2_low);
NSLogger(@"[reMarkable] a3 (flags/version) = 0x%02x (%u)", a3, a3);
NSLogger(@"[reMarkable] v17 = (a2<<8)|a3 = 0x%04x (%u)", combined_v17, combined_v17);
NSLogger(@"[reMarkable] a4 (typeInfo/URI?) = 0x%llx", (unsigned long long)a4);
NSLogger(@"[reMarkable] a5 (refCountData) = %p", a5);
if (a1) {
logMemory("Memory at a1 (typeMetadata)", (void *)a1, 64);
void **vtable = (void **)a1;
NSLogger(@"[reMarkable] a1 vtable/first ptr = %p", *vtable);
}
if (a4) {
logMemory("Memory at a4 (typeInfo)", (void *)a4, 64);
const char *maybeStr = (const char *)a4;
bool isPrintable = true;
int len = 0;
for (int i = 0; i < 64 && maybeStr[i]; i++) {
if (maybeStr[i] < 0x20 || maybeStr[i] > 0x7e) {
isPrintable = false;
break;
}
len++;
}
if (isPrintable && len > 0) {
NSLogger(@"[reMarkable] a4 as string: \"%.*s\"", len, maybeStr);
}
}
if (a5) {
logMemory("Memory at a5 (refCountData)", (void *)a5, 32);
}
logStackTrace("QML Registration context");
if (original_function_at_0x100011CE0) {
int64_t result = original_function_at_0x100011CE0(a1, a2, a3, a4, a5);
NSLogger(@"[reMarkable] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result);
NSLogger(@"[reMarkable] ========================================");
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x100011CE0 not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x10015A130(int64_t a1, int64_t a2) {
NSLogger(@"[reMarkable] Hook at 0x10015A130 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015A130) {
int64_t result = original_function_at_0x10015A130(a1, a2);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x10015A130 not available, returning 0");
return 0;
}
extern "C" void hooked_function_at_0x10015BC90(int64_t a1, int64_t a2) {
NSLogger(@"[reMarkable] Hook at 0x10015BC90 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015BC90) {
original_function_at_0x10015BC90(a1, a2);
NSLogger(@"[reMarkable] original function returned (void)");
return;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x10015BC90 not available");
}
extern "C" int64_t hooked_function_at_0x10016D520(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) {
NSLogger(@"[reMarkable] Hook at 0x10016D520 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = %p", a2);
if (a2) {
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
NSLogger(@"[reMarkable] a4 = 0x%llx", (unsigned long long)a4);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (a2 && *a2 != 0) {
logMemory("Memory at *a2", (void *)*a2, 64);
}
logMemory("Memory at a4", (void *)a4, 64);
if (original_function_at_0x10016D520) {
int64_t result = original_function_at_0x10016D520(a1, a2, a3, a4);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function not available, returning 0");
return 0;
}
extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned int a3) {
NSLogger(@"[reMarkable] Hook at 0x1001B6EE0 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
// At a1 (PdfExporter object):
// +0x10 contains a QString (likely document name)
NSLogger(@"[reMarkable] Reading QString at a1+0x10:");
logMemory("a1 + 0x10 (raw)", (void *)(a1 + 0x10), 64);
void **qstrPtr = (void **)(a1 + 0x10);
void *dataPtr = *qstrPtr;
if (!dataPtr) {
NSLogger(@"[reMarkable] QString has null data pointer");
return;
}
// Try reading potential size fields near dataPtr
int32_t size = 0;
for (int delta = 4; delta <= 32; delta += 4) {
int32_t candidate = *(int32_t *)((char *)dataPtr - delta);
if (candidate > 0 && candidate < 10000) {
size = candidate;
NSLogger(@"[reMarkable] QString plausible size=%d (found at -%d)", size, delta);
break;
}
}
if (size > 0) {
NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size];
NSLogger(@"[reMarkable] QString value: \"%@\"", qstringValue);
} else {
NSLogger(@"[reMarkable] QString: could not find valid size");
}
NSLogger(@"[reMarkable] a2 = %p", a2);
if (a2) {
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
if (original_function_at_0x1001B6EE0) {
original_function_at_0x1001B6EE0(a1, a2, a3);
NSLogger(@"[reMarkable] Original function at 0x1001B6EE0 executed");
} else {
NSLogger(@"[reMarkable] WARNING: Original function not available");
}
}
extern "C" int64_t hooked_qmlregister(
int64_t a1,
int64_t a2,
int64_t a3,
int64_t a4,
int64_t a5,
int64_t a6,
int a7,
int64_t a8,
int a9,
int64_t a10) {
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[reMarkable] QQmlPrivate::qmlregister called!");
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[reMarkable] a1 (RegistrationType) = 0x%llx (%lld)", (unsigned long long)a1, (long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx (%lld)", (unsigned long long)a2, (long long)a2);
NSLogger(@"[reMarkable] a3 = 0x%llx (%lld)", (unsigned long long)a3, (long long)a3);
NSLogger(@"[reMarkable] a4 = 0x%llx (%lld)", (unsigned long long)a4, (long long)a4);
NSLogger(@"[reMarkable] a5 = 0x%llx (%lld)", (unsigned long long)a5, (long long)a5);
NSLogger(@"[reMarkable] a6 = 0x%llx (%lld)", (unsigned long long)a6, (long long)a6);
NSLogger(@"[reMarkable] a7 = 0x%x (%d)", a7, a7);
NSLogger(@"[reMarkable] a8 = 0x%llx (%lld)", (unsigned long long)a8, (long long)a8);
NSLogger(@"[reMarkable] a9 = 0x%x (%d)", a9, a9);
NSLogger(@"[reMarkable] a10 = 0x%llx (%lld)", (unsigned long long)a10, (long long)a10);
// Check for PlatformHelpers registration
// a1 == 0 means TypeRegistration (object registration)
// a4 must be a valid pointer (not a small integer like 0, 1, 2, etc.)
if (a1 == 0 && a4 > 0x10000) {
const char *typeName = (const char *)a4;
int len = 0;
bool isValid = true;
for (int i = 0; i < 256; i++) {
char c = typeName[i];
if (c == '\0') {
break;
}
if (c < 0x20 || c > 0x7e) {
isValid = false;
break;
}
len++;
}
if (isValid && len > 0) {
NSLogger(@"[reMarkable] typeName (a4) = \"%.*s\"", len, typeName);
if (len == 15 && strncmp(typeName, "PlatformHelpers", 15) == 0) {
NSLogger(@"[reMarkable] !!! FOUND PlatformHelpers type registration !!!");
NSLogger(@"[reMarkable] factory ptr (a2) = %p", (void *)a2);
NSLogger(@"[reMarkable] a3 (metaObject?) = %p", (void *)a3);
NSLogger(@"[reMarkable] a5 = %p", (void *)a5);
NSLogger(@"[reMarkable] a6 = %p", (void *)a6);
logMemory("Factory ptr memory", (void *)a2, 64);
logMemory("a3 memory (metaObject?)", (void *)a3, 64);
logStackTrace("PlatformHelpers registration");
}
}
}
// Try to interpret a2 as memory region for other registration types
if (a2 > 0x10000 && a1 != 0) {
logMemory("Memory at a2", (void *)a2, 64);
const char *maybeStr = (const char *)a2;
bool isPrintable = true;
int len = 0;
for (int i = 0; i < 128 && maybeStr[i]; i++) {
if (maybeStr[i] < 0x20 || maybeStr[i] > 0x7e) {
isPrintable = false;
break;
}
len++;
}
if (isPrintable && len > 0) {
NSLogger(@"[reMarkable] a2 as string: \"%.*s\"", len, maybeStr);
}
}
int64_t result = 0;
if (original_qmlregister) {
result = original_qmlregister(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
NSLogger(@"[reMarkable] result = 0x%llx (%lld)", (unsigned long long)result, (long long)result);
} else {
NSLogger(@"[reMarkable] WARNING: Original qmlregister not available!");
}
NSLogger(@"[reMarkable] ========================================");
return result;
}
#endif // BUILD_MODE_DEV

View File

@@ -4,6 +4,13 @@
#import "MemoryUtils.h"
#import "Logger.h"
#import "ResourceUtils.h"
#ifdef BUILD_MODE_DEV
#import "DevHooks.h"
#endif
#ifdef BUILD_MODE_QMLREBUILD
#import "MessageBroker.h"
#import "HttpServer.h"
#endif
#import <objc/runtime.h>
#import <Cocoa/Cocoa.h>
#include <stdint.h>
@@ -26,6 +33,7 @@
#include <QtCore/QVariant>
#include <QtCore/QAnyStringView>
static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config";
static NSString *const kReMarkableConfigHostKey = @"host";
static NSString *const kReMarkableConfigPortKey = @"port";
@@ -240,7 +248,7 @@ static void (*original_qWebSocket_open)(
const QNetworkRequest &request) = NULL;
#endif
#ifdef BUILD_MODE_QMLDIFF
#ifdef BUILD_MODE_QMLREBUILD
static int (*original_qRegisterResourceData)(
int,
const unsigned char *,
@@ -248,63 +256,7 @@ static int (*original_qRegisterResourceData)(
const unsigned char *) = NULL;
#endif
#ifdef BUILD_MODE_DEV
static ssize_t (*original_qIODevice_write)(
QIODevice *self,
const char *data,
qint64 maxSize) = NULL;
// Hook for function at 0x10015A130
static int64_t (*original_function_at_0x10015A130)(int64_t a1, int64_t a2) = NULL;
// Hook for function at 0x10015BC90
static void (*original_function_at_0x10015BC90)(int64_t a1, int64_t a2) = NULL;
// Hook for function at 0x10016D520
static int64_t (*original_function_at_0x10016D520)(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) = NULL;
// Hook for function at 0x1001B6EE0
static void (*original_function_at_0x1001B6EE0)(int64_t a1, int64_t *a2, unsigned int a3) = NULL;
#endif
#if defined(BUILD_MODE_DEV)
// Memory logging helper function
static void logMemory(const char *label, void *address, size_t length) {
if (!address) {
NSLogger(@"[reMarkable] %s: (null)", label);
return;
}
unsigned char *ptr = (unsigned char *)address;
NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[reMarkable] %s: ", label];
for (size_t i = 0; i < length; i++) {
[hexLine appendFormat:@"%02x ", ptr[i]];
if ((i + 1) % 16 == 0 && i < length - 1) {
NSLogger(@"%@", hexLine);
hexLine = [NSMutableString stringWithString:@"[reMarkable] "];
}
}
// Log remaining bytes if any
if ([hexLine length] > 28) { // More than just the prefix
NSLogger(@"%@", hexLine);
}
}
// Stack trace logging helper function
static void logStackTrace(const char *label) {
NSLogger(@"[reMarkable] %s - Stack trace:", label);
NSArray<NSString *> *callStack = [NSThread callStackSymbols];
NSUInteger count = [callStack count];
// Skip first 2 frames (this function and the immediate caller's logging statement)
for (NSUInteger i = 0; i < count; i++) {
NSString *frame = callStack[i];
NSLogger(@"[reMarkable] #%lu: %@", (unsigned long)i, frame);
}
}
#endif
#ifdef BUILD_MODE_RMFAKECLOUD
static inline bool shouldPatchURL(const QString &host) {
@@ -352,23 +304,37 @@ static inline bool shouldPatchURL(const QString &host) {
logPrefix:@"[reMarkable]"];
#endif
#ifdef BUILD_MODE_QMLDIFF
NSLogger(@"[reMarkable] Build mode: qmldiff");
#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);
});
// Start HTTP server for export requests
if (httpserver::start(8080)) {
NSLogger(@"[reMarkable] HTTP server started on http://localhost:8080");
} else {
NSLogger(@"[reMarkable] Failed to start HTTP server");
}
[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
// // Hook function at address 0x1????
// [MemoryUtils hookAddress:@"reMarkable"
// staticAddress:0x????
// hookFunction:(void *)hooked_function_at_0x????
// originalFunction:(void **)&original_function_at_0x????
// logPrefix:@"[reMarkable]"];
NSLogger(@"[reMarkable] Build mode: dev/reverse engineering");
// [MemoryUtils hookSymbol:@"QtCore"
// symbolName:@"__ZN9QIODevice5writeEPKcx"
@@ -403,6 +369,29 @@ static inline bool shouldPatchURL(const QString &host) {
// 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;
@@ -463,9 +452,9 @@ extern "C" void hooked_qWebSocket_open(
}
#endif // BUILD_MODE_RMFAKECLOUD
#ifdef BUILD_MODE_QMLDIFF
#ifdef BUILD_MODE_QMLREBUILD
// See https://deepwiki.com/search/once-the-qrr-file-parsed-take_871f24a0-8636-4aee-bddf-7405b6e32584 for details on qmldiff replacement strategy
// 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,
@@ -478,172 +467,92 @@ 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) {
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);
}
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);
// Copy original data
memcpy(newDataBuffer, data, resource.originalDataSize);
int status = original_qRegisterResourceData(version, tree, name, data);
pthread_mutex_unlock(&gResourceMutex);
if (resource.tree) {
// 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_QMLDIFF
#ifdef BUILD_MODE_DEV
extern "C" ssize_t hooked_qIODevice_write(
QIODevice *self,
const char *data,
qint64 maxSize) {
NSLogger(@"[reMarkable] QIODevice::write called with maxSize: %lld", (long long)maxSize);
// Log the call stack
logStackTrace("QIODevice::write call stack");
// Log the data to write
logMemory("Data to write", (void *)data, (size_t)(maxSize < 64 ? maxSize : 64));
if (original_qIODevice_write) {
ssize_t result = original_qIODevice_write(self, data, maxSize);
NSLogger(@"[reMarkable] QIODevice::write result: %zd", result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original QIODevice::write not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x10015A130(int64_t a1, int64_t a2) {
NSLogger(@"[reMarkable] Hook at 0x10015A130 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015A130) {
int64_t result = original_function_at_0x10015A130(a1, a2);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x10015A130 not available, returning 0");
return 0;
}
extern "C" void hooked_function_at_0x10015BC90(int64_t a1, int64_t a2) {
NSLogger(@"[reMarkable] Hook at 0x10015BC90 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015BC90) {
original_function_at_0x10015BC90(a1, a2);
NSLogger(@"[reMarkable] original function returned (void)");
return;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x10015BC90 not available");
}
extern "C" int64_t hooked_function_at_0x10016D520(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) {
NSLogger(@"[reMarkable] Hook at 0x10016D520 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = %p", a2);
if (a2) {
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
NSLogger(@"[reMarkable] a4 = 0x%llx", (unsigned long long)a4);
// Log memory contents using helper function
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (a2 && *a2 != 0) {
logMemory("Memory at *a2", (void *)*a2, 64);
}
logMemory("Memory at a4", (void *)a4, 64);
if (original_function_at_0x10016D520) {
int64_t result = original_function_at_0x10016D520(a1, a2, a3, a4);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function not available, returning 0");
return 0;
}
extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned int a3) {
NSLogger(@"[reMarkable] Hook at 0x1001B6EE0 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
// At a1 (PdfExporter object at 0x7ff4c17391e0):
// +0x10 0x000600043EC10 QString (likely document name)
NSLogger(@"[reMarkable] Reading QString at a1+0x10:");
logMemory("a1 + 0x10 (raw)", (void *)(a1 + 0x10), 64);
void **qstrPtr = (void **)(a1 + 0x10);
void *dataPtr = *qstrPtr;
if (!dataPtr) {
NSLogger(@"[reMarkable] QString has null data pointer");
return;
}
// try reading potential size fields near dataPtr
int32_t size = 0;
for (int delta = 4; delta <= 32; delta += 4) {
int32_t candidate = *(int32_t *)((char *)dataPtr - delta);
if (candidate > 0 && candidate < 10000) {
size = candidate;
NSLogger(@"[reMarkable] QString plausible size=%d (found at -%d)", size, delta);
break;
}
}
if (size > 0) {
NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size];
NSLogger(@"[reMarkable] QString value: \"%@\"", qstringValue);
} else {
NSLogger(@"[reMarkable] QString: could not find valid size");
}
NSLogger(@"[reMarkable] a2 = %p", a2);
if (a2) {
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
if (original_function_at_0x1001B6EE0) {
original_function_at_0x1001B6EE0(a1, a2, a3);
NSLogger(@"[reMarkable] Original function at 0x1001B6EE0 executed");
} else {
NSLogger(@"[reMarkable] WARNING: Original function not available");
}
}
#endif // BUILD_MODE_DEV
#endif // BUILD_MODE_QMLREBUILD
@end

23
src/utils/HttpServer.h Normal file
View File

@@ -0,0 +1,23 @@
// HTTP Server for RMHook - native macOS implementation
#pragma once
#import <Foundation/Foundation.h>
#ifdef __cplusplus
extern "C" {
#endif
namespace httpserver {
// Start HTTP server on specified port
bool start(uint16_t port = 8080);
// Stop HTTP server
void stop();
// Check if server is running
bool isRunning();
}
#ifdef __cplusplus
}
#endif

305
src/utils/HttpServer.mm Normal file
View File

@@ -0,0 +1,305 @@
// HTTP Server for RMHook - native macOS implementation using CFSocket
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "HttpServer.h"
#include "MessageBroker.h"
#include "Logger.h"
static CFSocketRef g_serverSocket = NULL;
static uint16_t g_serverPort = 0;
static bool g_isRunning = false;
// Forward declarations
static void handleClientConnection(int clientSocket);
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType);
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData);
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData);
// Socket callback
static void socketCallback(CFSocketRef socket, CFSocketCallBackType type,
CFDataRef address, const void *data, void *info)
{
if (type == kCFSocketAcceptCallBack) {
CFSocketNativeHandle clientSocket = *(CFSocketNativeHandle *)data;
NSLogger(@"[HttpServer] New connection accepted, socket: %d", clientSocket);
// Handle client in background
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
handleClientConnection(clientSocket);
});
}
}
static void handleClientConnection(int clientSocket)
{
@autoreleasepool {
// Read request
char buffer[4096];
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesRead <= 0) {
close(clientSocket);
return;
}
buffer[bytesRead] = '\0';
NSString *request = [NSString stringWithUTF8String:buffer];
NSLogger(@"[HttpServer] Received request (%ld bytes)", (long)bytesRead);
// Parse request line
NSArray *lines = [request componentsSeparatedByString:@"\r\n"];
if (lines.count == 0) {
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request\"}", @"application/json");
close(clientSocket);
return;
}
NSArray *requestLine = [lines[0] componentsSeparatedByString:@" "];
if (requestLine.count < 3) {
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request line\"}", @"application/json");
close(clientSocket);
return;
}
NSString *method = requestLine[0];
NSString *path = requestLine[1];
NSLogger(@"[HttpServer] %@ %@", method, path);
// Find body (after \r\n\r\n)
NSRange bodyRange = [request rangeOfString:@"\r\n\r\n"];
NSString *body = nil;
if (bodyRange.location != NSNotFound) {
body = [request substringFromIndex:bodyRange.location + 4];
}
// Route requests
if ([path isEqualToString:@"/exportFile"] && [method isEqualToString:@"POST"]) {
if (!body || body.length == 0) {
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
close(clientSocket);
return;
}
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error || ![json isKindOfClass:[NSDictionary class]]) {
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
error ? error.localizedDescription : @"Not an object"];
sendResponse(clientSocket, 400, errorMsg, @"application/json");
close(clientSocket);
return;
}
handleExportFileRequest(clientSocket, (NSDictionary *)json);
} else if ([path isEqualToString:@"/documentAccepted"] && [method isEqualToString:@"POST"]) {
if (!body || body.length == 0) {
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
close(clientSocket);
return;
}
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error || ![json isKindOfClass:[NSDictionary class]]) {
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
error ? error.localizedDescription : @"Not an object"];
sendResponse(clientSocket, 400, errorMsg, @"application/json");
close(clientSocket);
return;
}
handleDocumentAcceptedRequest(clientSocket, (NSDictionary *)json);
} else if ([path isEqualToString:@"/health"] || [path isEqualToString:@"/"]) {
sendResponse(clientSocket, 200, @"{\"status\": \"ok\", \"service\": \"RMHook HTTP Server\"}", @"application/json");
} else {
sendResponse(clientSocket, 404, @"{\"error\": \"Endpoint not found\"}", @"application/json");
}
close(clientSocket);
}
}
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData)
{
NSLogger(@"[HttpServer] Processing /exportFile request");
// Convert to JSON string for MessageBroker
NSError *error = nil;
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
options:0
error:&error];
if (error) {
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
error.localizedDescription];
sendResponse(clientSocket, 500, errorMsg, @"application/json");
return;
}
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
NSLogger(@"[HttpServer] Broadcasting exportFile signal with data: %@", jsonStr);
// Broadcast to MessageBroker
messagebroker::broadcast("exportFile", [jsonStr UTF8String]);
// Send success response
sendResponse(clientSocket, 200,
@"{\"status\": \"success\", \"message\": \"Export request sent to application\"}",
@"application/json");
}
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData)
{
NSLogger(@"[HttpServer] Processing /documentAccepted request");
// Convert to JSON string for MessageBroker
NSError *error = nil;
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
options:0
error:&error];
if (error) {
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
error.localizedDescription];
sendResponse(clientSocket, 500, errorMsg, @"application/json");
return;
}
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
NSLogger(@"[HttpServer] Broadcasting documentAccepted signal with data: %@", jsonStr);
// Broadcast to MessageBroker
messagebroker::broadcast("documentAccepted", [jsonStr UTF8String]);
// Send success response
sendResponse(clientSocket, 200,
@"{\"status\": \"success\", \"message\": \"Document accepted request sent to application\"}",
@"application/json");
}
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType)
{
NSString *statusText;
switch (statusCode) {
case 200: statusText = @"OK"; break;
case 400: statusText = @"Bad Request"; break;
case 404: statusText = @"Not Found"; break;
case 500: statusText = @"Internal Server Error"; break;
default: statusText = @"Unknown"; break;
}
NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding];
NSString *response = [NSString stringWithFormat:
@"HTTP/1.1 %d %@\r\n"
@"Content-Type: %@; charset=utf-8\r\n"
@"Content-Length: %lu\r\n"
@"Access-Control-Allow-Origin: *\r\n"
@"Connection: close\r\n"
@"\r\n"
@"%@",
statusCode, statusText, contentType, (unsigned long)bodyData.length, body
];
NSData *responseData = [response dataUsingEncoding:NSUTF8StringEncoding];
send(clientSocket, responseData.bytes, responseData.length, 0);
}
namespace httpserver {
bool start(uint16_t port)
{
if (g_isRunning) {
NSLogger(@"[HttpServer] Server already running on port %d", g_serverPort);
return true;
}
// Create socket
CFSocketContext context = {0, NULL, NULL, NULL, NULL};
g_serverSocket = CFSocketCreate(kCFAllocatorDefault,
PF_INET,
SOCK_STREAM,
IPPROTO_TCP,
kCFSocketAcceptCallBack,
socketCallback,
&context);
if (!g_serverSocket) {
NSLogger(@"[HttpServer] Failed to create socket");
return false;
}
// Set socket options
int yes = 1;
setsockopt(CFSocketGetNative(g_serverSocket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
// Bind to address
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost only
CFDataRef addressData = CFDataCreate(kCFAllocatorDefault,
(const UInt8 *)&addr,
sizeof(addr));
CFSocketError error = CFSocketSetAddress(g_serverSocket, addressData);
CFRelease(addressData);
if (error != kCFSocketSuccess) {
NSLogger(@"[HttpServer] Failed to bind to port %d (error: %ld)", port, (long)error);
CFRelease(g_serverSocket);
g_serverSocket = NULL;
return false;
}
// Add to run loop
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, g_serverSocket, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopCommonModes);
CFRelease(source);
g_serverPort = port;
g_isRunning = true;
NSLogger(@"[HttpServer] HTTP server started successfully on http://localhost:%d", port);
return true;
}
void stop()
{
if (!g_isRunning) {
return;
}
if (g_serverSocket) {
CFSocketInvalidate(g_serverSocket);
CFRelease(g_serverSocket);
g_serverSocket = NULL;
}
g_isRunning = false;
NSLogger(@"[HttpServer] HTTP server stopped");
}
bool isRunning()
{
return g_isRunning;
}
} // namespace httpserver

63
src/utils/MessageBroker.h Normal file
View File

@@ -0,0 +1,63 @@
// Credits: asivery/rm-xovi-extensions
// (https://github.com/asivery/rm-xovi-extensions/blob/master/xovi-message-broker/src/XoviMessageBroker.h)
// Simplified for RMHook dylib <-> QML communication
#pragma once
#include <QObject>
#include <QStringList>
#include <QString>
#include <QDebug>
#include <QtQml/QQmlEngine>
// Forward declaration
class MessageBroker;
// Native callback type for C++ listeners
typedef void (*NativeSignalCallback)(const char *signal, const char *value);
namespace messagebroker {
void addBroadcastListener(MessageBroker *ref);
void removeBroadcastListener(MessageBroker *ref);
void broadcast(const char *signal, const char *value);
void registerQmlType();
// Register a native C++ callback to receive all signals
void setNativeCallback(NativeSignalCallback callback);
}
class MessageBroker : public QObject
{
Q_OBJECT
Q_PROPERTY(QStringList listeningFor READ getListeningFor WRITE setListeningFor)
public:
explicit MessageBroker(QObject *parent = nullptr) : QObject(parent) {
messagebroker::addBroadcastListener(this);
}
~MessageBroker() {
messagebroker::removeBroadcastListener(this);
}
// Send a signal from QML to all listeners (including C++ side)
Q_INVOKABLE void sendSignal(const QString &signal, const QString &message) {
QByteArray signalUtf8 = signal.toUtf8();
QByteArray messageUtf8 = message.toUtf8();
messagebroker::broadcast(signalUtf8.constData(), messageUtf8.constData());
}
void setListeningFor(const QStringList &l) {
_listeningFor = l;
}
const QStringList& getListeningFor() const {
return _listeningFor;
}
signals:
void signalReceived(const QString &signal, const QString &message);
private:
QStringList _listeningFor;
};

View File

@@ -0,0 +1,58 @@
// Credits: asivery/rm-xovi-extensions
// (https://github.com/asivery/rm-xovi-extensions/blob/master/xovi-message-broker/src/XoviMessageBroker.h)
#import <Foundation/Foundation.h>
#include "MessageBroker.h"
#include "Logger.h"
#include <vector>
#include <cstring>
#include <algorithm>
static std::vector<MessageBroker *> brokers;
static NativeSignalCallback nativeCallback = nullptr;
void messagebroker::setNativeCallback(NativeSignalCallback callback) {
nativeCallback = callback;
NSLogger(@"[MessageBroker] Native callback registered");
}
void messagebroker::addBroadcastListener(MessageBroker *ref) {
// Cannot have more than one.
if(std::find(brokers.begin(), brokers.end(), ref) == brokers.end()) {
brokers.push_back(ref);
NSLogger(@"[MessageBroker] Added broadcast listener, total: %zu", brokers.size());
}
}
void messagebroker::removeBroadcastListener(MessageBroker *ref) {
std::vector<MessageBroker *>::iterator iter;
if((iter = std::find(brokers.begin(), brokers.end(), ref)) != brokers.end()) {
brokers.erase(iter);
NSLogger(@"[MessageBroker] Removed broadcast listener, remaining: %zu", brokers.size());
}
}
void messagebroker::broadcast(const char *signal, const char *value) {
QString qSignal(signal), qValue(value);
NSLogger(@"[MessageBroker] Broadcasting signal '%s' with value '%s'", signal, value);
// Call native C++ callback if registered
if (nativeCallback) {
nativeCallback(signal, value);
}
// Notify QML listeners
for(auto &ref : brokers) {
if(ref->getListeningFor().contains(qSignal)) {
emit ref->signalReceived(qSignal, qValue);
}
}
}
void messagebroker::registerQmlType() {
qmlRegisterType<MessageBroker>("net.noham.MessageBroker", 1, 0, "MessageBroker");
NSLogger(@"[MessageBroker] Registered QML type net.noham.MessageBroker");
}
// Include MOC output for MessageBroker class (generated by Qt's Meta-Object Compiler)
#include "moc_MessageBroker.cpp"

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

@@ -36,7 +36,7 @@ static NSString *ReMarkableDumpRootDirectory(void) {
return dumpDirectory;
}
#ifdef BUILD_MODE_QMLDIFF
#ifdef BUILD_MODE_QMLREBUILD
uint32_t readUInt32(uint8_t *addr, int offset) {
return (uint32_t)(addr[offset + 0] << 24) |
(uint32_t)(addr[offset + 1] << 16) |
@@ -344,6 +344,149 @@ 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",
"/qml/client/dialogs/ExportUtils.js",
"/qml/client/desktop/FileImportDialog.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 +518,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
#endif // BUILD_MODE_QMLREBUILD

17
src/utils/mb.m Normal file
View File

@@ -0,0 +1,17 @@
// Example usage of MessageBroker from C++/Objective-C
#include <QObject>
#include <QProcess>
#include <QString>
#include <QQmlApplicationEngine>
#include "MessageBroker.h"
// Example: Register MessageBroker QML type (called from dylib init)
void initMessageBroker() {
messagebroker::registerQmlType();
}
// Example: Send a signal from C++ to QML
void sendSignal() {
messagebroker::broadcast("demoSignal", "Hello from C!");
}

16
src/utils/mb.qml Normal file
View File

@@ -0,0 +1,16 @@
import net.noham.MessageBroker
MessageBroker {
id: demoBroker
listeningFor: ["demoSignal"]
onSignalReceived: (signal, message) => {
console.log("Got message", signal, message);
}
}
MouseArea {
onClicked: () => {
demoBroker.sendSignal("mySignalName", "Hello from QML!");
}
}