Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>3.0.0</Version>
<Version>3.1.0</Version>
<Authors>Connor Lindsay</Authors>
<Company>CLindsay94</Company>
<NoWarn>$(NoWarn);CA1416;CS0067;CS1591;CS1573</NoWarn>
Expand Down
60 changes: 55 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">

<img src="docs/assets/remex-banner.svg" alt="RemEx 3.0: remote control for your PC, from your phone" width="100%" />
<img src="docs/assets/remex-banner.svg" alt="RemEx 3.1: remote control for your PC, from your phone" width="100%" />

<br />

Expand All @@ -14,7 +14,7 @@
[![Languages](https://img.shields.io/badge/languages-9-8fd694?style=flat-square)](#everything-it-does)
[![License](https://img.shields.io/github/license/clindsay94/RemEx?style=flat-square)](LICENSE)

**[Get started](#get-started-in-five-minutes)** &nbsp;·&nbsp; **[What's new in 3.0](#whats-new-in-30)** &nbsp;·&nbsp; **[How it works](#how-it-talks-to-your-pc)** &nbsp;·&nbsp; **[Build it](#building-from-source)** &nbsp;·&nbsp; **[Docs](#docs-index)**
**[Get started](#get-started-in-five-minutes)** &nbsp;·&nbsp; **[What's new in 3.1](#whats-new-in-31)** &nbsp;·&nbsp; **[How it works](#how-it-talks-to-your-pc)** &nbsp;·&nbsp; **[Build it](#building-from-source)** &nbsp;·&nbsp; **[Docs](#docs-index)**

</div>

Expand All @@ -24,6 +24,55 @@ RemEx runs your PC from your phone over plain LAN: no relay server, no account,

> **3.0 is the biggest release yet.** 296 commits and 1,436 files changed since 2.5.0, a new look shared across both apps, routines that act on their own, and a startup splash that actually shows RemEx finding your PC. It's also the first release in production on Google Play.

## What's new in 3.1

**3.1 redesigns file sharing on both apps.** The PC and the phone now show both devices in one folder tree, preview the file you pick, and show the SHA-256 fingerprint that proves a file arrived exactly as it was sent.

<table>
<tr>
<td width="33%" valign="top">

### 🗂️ One tree, both devices
The PC's File Transfer page and the phone's Files screen list **This PC** (or **This phone**) and the other device side by side. Open folders level by level; the tree follows you as you browse.

</td>
<td width="33%" valign="top">

### 👁️ Live preview
Pick a file to see it: photos at full quality with zoom, and text, code and logs in colour with line numbers. Turn on **Live** to follow a log as it grows.

</td>
<td width="33%" valign="top">

### 🔐 SHA-256 you can see
Transfers say **Verified** when the fingerprint really matched. Copy a file's fingerprint, compare it with one you paste, or check it against the copy on the other device.

</td>
</tr>
<tr>
<td valign="top">

### ↔️ Send between devices
**Send to PC** and **Save to phone** into any folder you pick, and on the PC drag files from one device to the other. Everything goes through the same verified transfer queue.

</td>
<td valign="top">

### 📁 Phone folders you choose
Add folders from your phone to browse them in RemEx. Your PC only sees one when you turn on **Share with PC**.

</td>
<td valign="top">

### 📷 Scan to get the phone app
The PC's Home page shows a QR code that opens RemEx on Google Play.

</td>
</tr>
</table>

The full 3.1 write-up is in the [release notes](docs/RELEASE-NOTES-3.1.0.md).

## What's new in 3.0

<table>
Expand Down Expand Up @@ -114,9 +163,9 @@ All four share one shader on Android and the PC, and Original Scan, Cosmic Zoom

| Platform | Download | Size |
|---|---|---|
| **Windows** | `RemEx-v3.0.0-Setup.exe` (Inno Setup, installs to run elevated for input and power control) | 55.6 MB |
| **Android** | `RemEx-V3.0.0-release.apk` (sideload) | 38.2 MB |
| **Linux x64** | `remex-agent-v3.0.0-linux-x64.tar.gz`, see [Linux install](docs/LINUX_INSTALL.md) | 69.7 MB |
| **Windows** | `RemEx-v3.1.0-Setup.exe` (Inno Setup, installs to run elevated for input and power control) | — |
| **Android** | `RemEx-V3.1.0-release.apk` (sideload) | 38.6 MB |
| **Linux x64** | `remex-agent-v3.1.0-linux-x64.tar.gz`, see [Linux install](docs/LINUX_INSTALL.md) | 70.1 MB |

**Requires:** Android 14 or newer on the phone, a Windows or Linux PC, and both on the same network. To reach your PC from elsewhere, run [Tailscale](https://tailscale.com) on both ends, because RemEx has no relay of its own.

Expand Down Expand Up @@ -227,6 +276,7 @@ Please don't open a public issue for a vulnerability. See [docs/SECURITY.md](doc

| Doc | What's in it |
|---|---|
| [docs/RELEASE-NOTES-3.1.0.md](docs/RELEASE-NOTES-3.1.0.md) | Readable summary of everything in 3.1 |
| [docs/RELEASE-NOTES-3.0.0.md](docs/RELEASE-NOTES-3.0.0.md) | Readable summary of everything in 3.0 |
| [docs/CHANGELOG.md](docs/CHANGELOG.md) | Full changelog, all versions |
| [docs/INSTALL-ANDROID.md](docs/INSTALL-ANDROID.md) | Google Play, sideloading, Auto Blocker, first pairing |
Expand Down
41 changes: 39 additions & 2 deletions build-remex.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
.EXAMPLE
./build-remex.ps1 -c release -t all
.EXAMPLE
./build-remex.ps1 -v 3.0.0 -t all -c release
./build-remex.ps1 -v 3.1.0 -t all -c release
.EXAMPLE
./build-remex.ps1 -t windows -NoClean
.EXAMPLE
Expand Down Expand Up @@ -265,6 +265,43 @@ function Sync-DirectoryBuildPropsVersion {
} else {
Write-Host "Directory.Build.props is already synced at version $DesiredVersion" -ForegroundColor DarkGray
}

Sync-PinnedVersionStrings -RepoRoot (Split-Path -Parent $BuildPropsPath) -DesiredVersion $DesiredVersion
}

function Sync-PinnedVersionStrings {
# The two places that hold the PC version as text rather than reading Directory.Build.props: the Windows
# manifest's assemblyIdentity (four parts) and the Inno Setup script's fallback for a bare `iscc` run. Both had
# drifted for releases (1.13.0.0 and 2.2.0 at 3.0.0), so every version change now rewrites them too, and
# VersionSourceOfTruthTests fails the build if they disagree with Directory.Build.props.
param(
[Parameter(Mandatory=$true)][string]$RepoRoot,
[Parameter(Mandatory=$true)][string]$DesiredVersion
)

$edits = @(
@{ Path = Join-Path $RepoRoot "remex.agent/app.manifest"
Pattern = '(<assemblyIdentity\s+version=")[^"]*(")'
Value = "`${1}$DesiredVersion.0`${2}" },
@{ Path = Join-Path $RepoRoot "installer/RemEx.iss"
Pattern = '(#define AppVersion ")[^"]*(")'
Value = "`${1}$DesiredVersion`${2}" },
@{ Path = Join-Path $RepoRoot "installer/RemEx.iss"
Pattern = '(iscc /DAppVersion=)[0-9.]+'
Value = "`${1}$DesiredVersion" }
)
foreach ($edit in $edits) {
if (-not (Test-Path $edit.Path)) {
Write-Warning "$($edit.Path) not found; its version was left as it was."
continue
}
$text = Get-Content $edit.Path -Raw
$updated = [regex]::Replace($text, $edit.Pattern, $edit.Value)
if ($text -ne $updated) {
Set-Content $edit.Path $updated -NoNewline
Write-Host " Updated the version in $(Split-Path -Leaf $edit.Path) to $DesiredVersion" -ForegroundColor Green
}
}
}

# Force output to support emojis/colored text on Windows PowerShell
Expand Down Expand Up @@ -646,7 +683,7 @@ Sync-DirectoryBuildPropsVersion -BuildPropsPath $BuildPropsPath -DesiredVersion
# 5. Restore .NET Solutions
if ($Target -eq "windows" -or $Target -eq "linux" -or $Target -eq "all") {
Write-Host "=== Restoring .NET Packages ===" -ForegroundColor Yellow
$solutionFile = Join-Paths $RepoRoot "RemEx.sln"
$solutionFile = Join-Paths $RepoRoot "Remex.sln"
dotnet restore $solutionFile
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet restore failed (exit $LASTEXITCODE)"
Expand Down
45 changes: 41 additions & 4 deletions docs/API_CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,34 @@ same way, so the meaning is fixed:
listing from the first page (with totals) rather than failing.
- `nextCursor` is absent on the last page and when the listing was truncated at the total-entry cap.

### Live preview: range reads (`file_read_range_request` / `file_read_range_response`, 2026-10-08)
Up to 1 MiB (`FileTransferLimits.ReadRangeMaxBytes`) of one file, answered on the control channel in base64 so
the reply stays well under the 4 MB message limit. Capability-flagged by `fileCapabilities.readRange`.

```jsonc
// requester → host
{ "requestId": "…", "rootId": "…", "relativePath": "Logs/app.log",
"offset": 0, "length": 1048576, // 1 … 1 MiB
"fromEnd": false } // true: the LAST `length` bytes (live tail)
// host → requester
{ "requestId": "…", "offset": 0, "dataBase64": "…", "fileSize": 52344,
"modifiedUtc": 1759900000000, "eof": true, "errorMessage": null }
```

- Resolved exactly as a download's source is (PC: `ResolvePath` / a consented volume; phone: `facade.resolve`).
A directory, a missing file, an unshared root, a negative offset or a length outside 1 … 1 MiB is answered with
an `errorMessage`, never silence.
- An offset at or past the end returns empty `dataBase64` with `eof: true`, not an error: a tail that polls after a
truncation sees "nothing new", and `fileSize` going down tells it to start over.
- The PC opens with `FileShare.ReadWrite | Delete`, so a log another program is still writing can be tailed.
- The phone answers off its control-message collector and seeks with the provider's file descriptor when it can.
A provider that reports size 0 gets `fileSize` = at least what was read.
- Preview limits shared by both clients: text 2 MiB from the start, or the last 256 KiB when tailing (polled every
2 s while visible); images up to 40 MiB, fetched in 1 MiB reads.

`file_hash_request` (existing) is now also answered by the phone (`fileCapabilities.hash`). The wire stays Base64;
screens show lowercase hex (`HashFormat`, C# and Kotlin twins sharing one set of test vectors).

### The PC browsing a paired phone (relay, RemEx-xt0af)
The PC's own File Transfer screen can browse a paired phone. **The connection direction does not
change**: the phone dials `/ws` and `/ws/files` as always, and the PC opens no socket to it. What is
Expand All @@ -124,15 +152,15 @@ them exactly as it would answer any peer. No message type was added.

**Relayed requests (host → phone), and nothing else:** `file_roots_request`, `file_browse_request`,
`file_volumes_request`, `file_search_request`, `file_manifest_request`, `file_metadata_request`,
`file_thumbnail_request`, and (RemEx-fgmne) `file_manage_request`. Root management
(`file_root_manage_request`: which folders the phone shares is only ever the phone's decision), hashing and
the legacy v2 transfer are refused by the relay before they reach the wire.
`file_thumbnail_request`, (2026-10-08) `file_read_range_request` and `file_hash_request`, and (RemEx-fgmne)
`file_manage_request`. Root management (`file_root_manage_request`: which folders the phone shares is only ever
the phone's decision) and the legacy v2 transfer are refused by the relay before they reach the wire.

Rules the host enforces, each pinned by `PhoneFileRelayTests`:
- The target must be **paired** (`PairedClientRegistry`) **and connected** on a session that proved its
identity; otherwise the PC side gets `PhoneNotConnectedException`.
- A relayed request carries **no `clientId`** (it is nulled on the way out).
- A reply (`file_*_response` of the eight types above) is accepted only from a session with
- A reply (`file_*_response` of the ten types above) is accepted only from a session with
`identityProven && !isLoopback`, only for a request this host sent to **that same client id**, and is
delivered only to the relay connection that asked. Unrequested replies are dropped; nothing is
broadcast. Loopback can never answer for a phone (RemEx-4215).
Expand Down Expand Up @@ -177,6 +205,15 @@ browsing only while that switch is on. The phone resolves a root id only if it i
(`IsValidRemoteRelativePath`): the same rules the phone applies. Copy and move are not subject to the 90-second request timeout: they stream the
whole file on the phone before it answers, so they wait up to 30 minutes.

**Reading a phone's files: live preview and hashing (2026-10-08 redesign).** `file_read_range_request` and
`file_hash_request` are relayed READ-ONLY. They need nothing from *Let your PC change files*; the phone resolves
them through the same `facade.resolve` (and so the same `SharedPathPolicy` check against the folders shared right
now) as a browse, on every request. Before the wire the relay checks the root id, the path
(`IsValidRemoteRelativePath`, and never the shared folder itself) and, for a range read, the numbers
(`FileReadRangeValidation`). A hash waits up to 30 minutes (the phone hashes the whole file first); a range read
keeps the 90-second backstop. The phone advertises both with `fileCapabilities.readRange` / `hash` (additive,
default false); a PC hides preview and Compute SHA-256 for a phone that does not.

**Folder upload to a phone** is not a new request either: the PC makes each folder with `mkdir`
(`relativePath` = the parent, `newName` = the folder, shallowest first, a folder that already exists is fine),
then uploads each file as an ordinary `file_transfer_offer` with `mode: "upload"` into that folder. It is offered
Expand Down
Loading
Loading