mirror of
https://github.com/NohamR/RMHook.git
synced 2026-04-08 07:59:58 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
400e698765 | ||
|
|
3e89d8118e | ||
|
|
3765bcd584 | ||
|
|
55a15fb035 | ||
|
|
9322b0319e |
@@ -8,10 +8,10 @@ set(CMAKE_CXX_STANDARD 17)
|
|||||||
|
|
||||||
# Build mode options
|
# Build mode options
|
||||||
# - rmfakecloud: Redirect reMarkable cloud to rmfakecloud server (default)
|
# - 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
|
# - dev: Development/reverse engineering mode with all hooks
|
||||||
option(BUILD_MODE_RMFAKECLOUD "Build with rmfakecloud support" ON)
|
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)
|
option(BUILD_MODE_DEV "Build with dev/reverse engineering hooks" OFF)
|
||||||
|
|
||||||
# Compiler settings for macOS
|
# Compiler settings for macOS
|
||||||
@@ -28,6 +28,8 @@ set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
|
|||||||
include_directories(
|
include_directories(
|
||||||
${PROJECT_ROOT_DIR}/src/core
|
${PROJECT_ROOT_DIR}/src/core
|
||||||
${PROJECT_ROOT_DIR}/src/utils
|
${PROJECT_ROOT_DIR}/src/utils
|
||||||
|
${PROJECT_ROOT_DIR}/src/reMarkable
|
||||||
|
${PROJECT_ROOT_DIR}/libs/include
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find required libraries
|
# Find required libraries
|
||||||
@@ -55,13 +57,13 @@ foreach(_qt_root ${_qt_candidate_roots})
|
|||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
find_package(Qt6 COMPONENTS Core Network WebSockets QUIET)
|
find_package(Qt6 COMPONENTS Core Network WebSockets Qml QUIET)
|
||||||
if(Qt6_FOUND)
|
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()
|
else()
|
||||||
find_package(Qt5 COMPONENTS Core Network WebSockets QUIET)
|
find_package(Qt5 COMPONENTS Core Network WebSockets Qml QUIET)
|
||||||
if(Qt5_FOUND)
|
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()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -79,6 +81,7 @@ set(COMMON_SOURCES
|
|||||||
# reMarkable dylib
|
# reMarkable dylib
|
||||||
set(REMARKABLE_SOURCES
|
set(REMARKABLE_SOURCES
|
||||||
${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m
|
${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m
|
||||||
|
${PROJECT_ROOT_DIR}/src/reMarkable/DevHooks.m
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(reMarkable SHARED
|
add_library(reMarkable SHARED
|
||||||
@@ -102,15 +105,34 @@ set_target_properties(reMarkable PROPERTIES
|
|||||||
|
|
||||||
add_definitions(-DQT_NO_VERSION_TAGGING)
|
add_definitions(-DQT_NO_VERSION_TAGGING)
|
||||||
|
|
||||||
# Add build mode compile definitions
|
# Add build mode compile definitions and conditionally add sources
|
||||||
if(BUILD_MODE_RMFAKECLOUD)
|
if(BUILD_MODE_RMFAKECLOUD)
|
||||||
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_RMFAKECLOUD=1)
|
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_RMFAKECLOUD=1)
|
||||||
message(STATUS "Build mode: rmfakecloud (cloud redirection)")
|
message(STATUS "Build mode: rmfakecloud (cloud redirection)")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(BUILD_MODE_QMLDIFF)
|
if(BUILD_MODE_QMLREBUILD)
|
||||||
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLDIFF=1)
|
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1)
|
||||||
message(STATUS "Build mode: qmldiff (resource hooking)")
|
|
||||||
|
# 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()
|
endif()
|
||||||
|
|
||||||
if(BUILD_MODE_DEV)
|
if(BUILD_MODE_DEV)
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -122,6 +122,9 @@ If the config file doesn't exist, it will be created automatically with default
|
|||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
- xovi-rmfakecloud: [asivery/xovi-rmfakecloud](https://github.com/asivery/xovi-rmfakecloud) - Original hooking information
|
- 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
|
- tinyhook: [Antibioticss/tinyhook](https://github.com/Antibioticss/tinyhook/) - Function hooking framework
|
||||||
- rmfakecloud: [ddvk/rmfakecloud](https://github.com/ddvk/rmfakecloud) - Self-hosted reMarkable cloud
|
- 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
|
- 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 |
|
| Mode | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `rmfakecloud` | Redirect reMarkable cloud to rmfakecloud server (default) |
|
| `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 |
|
| `dev` | Development/reverse engineering mode with all hooks |
|
||||||
| `all` | Enable all modes |
|
| `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:
|
Examples:
|
||||||
```bash
|
```bash
|
||||||
./scripts/build.sh # Build with rmfakecloud mode (default)
|
./scripts/build.sh # Build with rmfakecloud mode (default)
|
||||||
|
|||||||
54
docs/DocumentAccepted_MessageBroker_snippet.qml
Normal file
54
docs/DocumentAccepted_MessageBroker_snippet.qml
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
docs/ExportDialog_MessageBroker_snippet.qml
Normal file
67
docs/ExportDialog_MessageBroker_snippet.qml
Normal 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
293
docs/HTTP_SERVER.md
Normal 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
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# Build modes:
|
# Build modes:
|
||||||
# rmfakecloud - Redirect reMarkable cloud to rmfakecloud server (default)
|
# 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
|
# dev - Development/reverse engineering mode with all hooks
|
||||||
|
|
||||||
PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
@@ -19,8 +19,8 @@ case "$BUILD_MODE" in
|
|||||||
rmfakecloud)
|
rmfakecloud)
|
||||||
DYLIB_NAME="rmfakecloud.dylib"
|
DYLIB_NAME="rmfakecloud.dylib"
|
||||||
;;
|
;;
|
||||||
qmldiff)
|
qmlrebuild)
|
||||||
DYLIB_NAME="qmldiff.dylib"
|
DYLIB_NAME="qmlrebuild.dylib"
|
||||||
;;
|
;;
|
||||||
dev)
|
dev)
|
||||||
DYLIB_NAME="dev.dylib"
|
DYLIB_NAME="dev.dylib"
|
||||||
@@ -37,20 +37,20 @@ esac
|
|||||||
CMAKE_OPTIONS=""
|
CMAKE_OPTIONS=""
|
||||||
case "$BUILD_MODE" in
|
case "$BUILD_MODE" in
|
||||||
rmfakecloud)
|
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)
|
qmlrebuild)
|
||||||
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLDIFF=ON -DBUILD_MODE_DEV=OFF"
|
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLREBUILD=ON -DBUILD_MODE_DEV=OFF"
|
||||||
;;
|
;;
|
||||||
dev)
|
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)
|
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 "❌ Unknown build mode: $BUILD_MODE"
|
||||||
echo "Available modes: rmfakecloud (default), qmldiff, dev, all"
|
echo "Available modes: rmfakecloud (default), qmlrebuild, dev, all"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
200
scripts/http_server.py
Normal file
200
scripts/http_server.py
Normal 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
165
scripts/test_http_server.py
Normal 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
47
src/reMarkable/DevHooks.h
Normal 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
389
src/reMarkable/DevHooks.m
Normal 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, ®istrationData)
|
||||||
|
//
|
||||||
|
// 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
|
||||||
@@ -4,6 +4,13 @@
|
|||||||
#import "MemoryUtils.h"
|
#import "MemoryUtils.h"
|
||||||
#import "Logger.h"
|
#import "Logger.h"
|
||||||
#import "ResourceUtils.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 <objc/runtime.h>
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
@@ -26,6 +33,7 @@
|
|||||||
#include <QtCore/QVariant>
|
#include <QtCore/QVariant>
|
||||||
#include <QtCore/QAnyStringView>
|
#include <QtCore/QAnyStringView>
|
||||||
|
|
||||||
|
|
||||||
static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config";
|
static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config";
|
||||||
static NSString *const kReMarkableConfigHostKey = @"host";
|
static NSString *const kReMarkableConfigHostKey = @"host";
|
||||||
static NSString *const kReMarkableConfigPortKey = @"port";
|
static NSString *const kReMarkableConfigPortKey = @"port";
|
||||||
@@ -240,7 +248,7 @@ static void (*original_qWebSocket_open)(
|
|||||||
const QNetworkRequest &request) = NULL;
|
const QNetworkRequest &request) = NULL;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef BUILD_MODE_QMLDIFF
|
#ifdef BUILD_MODE_QMLREBUILD
|
||||||
static int (*original_qRegisterResourceData)(
|
static int (*original_qRegisterResourceData)(
|
||||||
int,
|
int,
|
||||||
const unsigned char *,
|
const unsigned char *,
|
||||||
@@ -248,63 +256,7 @@ static int (*original_qRegisterResourceData)(
|
|||||||
const unsigned char *) = NULL;
|
const unsigned char *) = NULL;
|
||||||
#endif
|
#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
|
#ifdef BUILD_MODE_RMFAKECLOUD
|
||||||
static inline bool shouldPatchURL(const QString &host) {
|
static inline bool shouldPatchURL(const QString &host) {
|
||||||
@@ -352,23 +304,37 @@ static inline bool shouldPatchURL(const QString &host) {
|
|||||||
logPrefix:@"[reMarkable]"];
|
logPrefix:@"[reMarkable]"];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef BUILD_MODE_QMLDIFF
|
#ifdef BUILD_MODE_QMLREBUILD
|
||||||
NSLogger(@"[reMarkable] Build mode: qmldiff");
|
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"
|
[MemoryUtils hookSymbol:@"QtCore"
|
||||||
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
|
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
|
||||||
hookFunction:(void *)hooked_qRegisterResourceData
|
hookFunction:(void *)hooked_qRegisterResourceData
|
||||||
originalFunction:(void **)&original_qRegisterResourceData
|
originalFunction:(void **)&original_qRegisterResourceData
|
||||||
logPrefix:@"[reMarkable]"];
|
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
|
#endif
|
||||||
|
|
||||||
#ifdef BUILD_MODE_DEV
|
#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");
|
NSLogger(@"[reMarkable] Build mode: dev/reverse engineering");
|
||||||
// [MemoryUtils hookSymbol:@"QtCore"
|
// [MemoryUtils hookSymbol:@"QtCore"
|
||||||
// symbolName:@"__ZN9QIODevice5writeEPKcx"
|
// symbolName:@"__ZN9QIODevice5writeEPKcx"
|
||||||
@@ -403,6 +369,29 @@ static inline bool shouldPatchURL(const QString &host) {
|
|||||||
// hookFunction:(void *)hooked_function_at_0x1001B6EE0
|
// hookFunction:(void *)hooked_function_at_0x1001B6EE0
|
||||||
// originalFunction:(void **)&original_function_at_0x1001B6EE0
|
// originalFunction:(void **)&original_function_at_0x1001B6EE0
|
||||||
// logPrefix:@"[reMarkable]"];
|
// 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
|
#endif
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
@@ -463,9 +452,9 @@ extern "C" void hooked_qWebSocket_open(
|
|||||||
}
|
}
|
||||||
#endif // BUILD_MODE_RMFAKECLOUD
|
#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(
|
extern "C" int hooked_qRegisterResourceData(
|
||||||
int version,
|
int version,
|
||||||
@@ -478,172 +467,92 @@ extern "C" int hooked_qRegisterResourceData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_lock(&gResourceMutex);
|
pthread_mutex_lock(&gResourceMutex);
|
||||||
|
|
||||||
struct ResourceRoot resource = {
|
struct ResourceRoot resource = {
|
||||||
.data = (uint8_t *)data,
|
.data = (uint8_t *)data,
|
||||||
.name = (uint8_t *)name,
|
.name = (uint8_t *)name,
|
||||||
.tree = (uint8_t *)tree,
|
.tree = (uint8_t *)tree,
|
||||||
|
|
||||||
.treeSize = 0,
|
.treeSize = 0,
|
||||||
.dataSize = 0,
|
.dataSize = 0,
|
||||||
.originalDataSize = 0,
|
.originalDataSize = 0,
|
||||||
.nameSize = 0,
|
.nameSize = 0,
|
||||||
|
|
||||||
.entriesAffected = 0,
|
.entriesAffected = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p name:%p data:%p",
|
||||||
|
version, tree, name, data);
|
||||||
|
|
||||||
statArchive(&resource, 0);
|
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);
|
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);
|
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)",
|
// Copy original data
|
||||||
version, tree, resource.treeSize, name, resource.nameSize, data, resource.dataSize);
|
memcpy(newDataBuffer, data, resource.originalDataSize);
|
||||||
|
|
||||||
int status = original_qRegisterResourceData(version, tree, name, data);
|
// Copy replacement entries to their designated offsets
|
||||||
pthread_mutex_unlock(&gResourceMutex);
|
struct ReplacementEntry *entry = getReplacementEntries();
|
||||||
if (resource.tree) {
|
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);
|
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;
|
return status;
|
||||||
}
|
}
|
||||||
#endif // BUILD_MODE_QMLDIFF
|
#endif // BUILD_MODE_QMLREBUILD
|
||||||
|
|
||||||
#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
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
23
src/utils/HttpServer.h
Normal file
23
src/utils/HttpServer.h
Normal 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
305
src/utils/HttpServer.mm
Normal 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
63
src/utils/MessageBroker.h
Normal 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;
|
||||||
|
};
|
||||||
58
src/utils/MessageBroker.mm
Normal file
58
src/utils/MessageBroker.mm
Normal 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"
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -19,6 +20,16 @@ struct ResourceRoot {
|
|||||||
int entriesAffected;
|
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 TREE_ENTRY_SIZE 22
|
||||||
#define DIRECTORY 0x02
|
#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 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);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif /* ResourceUtils_h */
|
#endif
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ static NSString *ReMarkableDumpRootDirectory(void) {
|
|||||||
return dumpDirectory;
|
return dumpDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef BUILD_MODE_QMLDIFF
|
#ifdef BUILD_MODE_QMLREBUILD
|
||||||
uint32_t readUInt32(uint8_t *addr, int offset) {
|
uint32_t readUInt32(uint8_t *addr, int offset) {
|
||||||
return (uint32_t)(addr[offset + 0] << 24) |
|
return (uint32_t)(addr[offset + 0] << 24) |
|
||||||
(uint32_t)(addr[offset + 1] << 16) |
|
(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) {
|
void processNode(struct ResourceRoot *root, int node, const char *rootName) {
|
||||||
int offset = findOffset(node) + 4;
|
int offset = findOffset(node) + 4;
|
||||||
uint16_t flags = readUInt16(root->tree, offset);
|
uint16_t flags = readUInt16(root->tree, offset);
|
||||||
@@ -375,9 +518,42 @@ void processNode(struct ResourceRoot *root, int node, const char *rootName) {
|
|||||||
|
|
||||||
free(tempRoot);
|
free(tempRoot);
|
||||||
} else {
|
} else {
|
||||||
NSLogger(@"[reMarkable] Processing node %d: %s%s", (int)node, rootName ? rootName : "", nameBuffer);
|
uint16_t fileFlag = readUInt16(root->tree, offset - 2);
|
||||||
uint16_t fileFlags = readUInt16(root->tree, offset - 2);
|
const char *type;
|
||||||
ReMarkableDumpResourceFile(root, node, rootName ? rootName : "", nameBuffer, fileFlags);
|
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
17
src/utils/mb.m
Normal 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
16
src/utils/mb.qml
Normal 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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user