fix: reconcile implementation plan with decompiled Java source
Cross-referenced every wire-format claim against rc-src and corrected several errors that would have caused stream desync during implementation: - rect header is 4×u16 + i32 (12 bytes), not varints + u8 - ping payload is i32 (4 bytes), not 1 byte - PointerEvent is 8 bytes (includes trailing extra_u16) - ServerInit is 19 bytes, not 16 - pixel-format struct is variable-length, not fixed 25 bytes - string primitive uses modified-UTF-8, not ISO-8859-1 - bandwidth probe is read-only (no echo response) - Phase 1 uses sync Read traits (async deferred to Phase 2) - corrected all stale line-number references in ac.java/aw.java Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,8 +38,12 @@ blekin/
|
||||
Every byte derives from `aw.java`, `ac.java`, and `ByteColorRFBRenderer.java`.
|
||||
|
||||
**Wire facts:**
|
||||
- Read primitives are mixed: u8, u16-BE, varint (1–3 bytes, per `aw.int()` at
|
||||
line 484). Write primitives are u8 and u16-BE only.
|
||||
- Read primitives from `h.java`: `h.new()` = u8, `h.try()` = i8, `h.int()` =
|
||||
u16-BE, `h.char()` = i16-BE, `h.do()` = i32-BE, `h.byte()` = u16-length-
|
||||
prefixed modified-UTF-8 string. Write primitives are u8 and u16-BE only.
|
||||
- Varint (1–3 bytes) is defined in `aw.int()` at line 484. Used for Tight
|
||||
compressed-stream lengths and a few other places — **not** for rectangle
|
||||
headers.
|
||||
- Pixel format is **8bpp RGB332** (mask 7 / 56 / 192). One byte per pixel on
|
||||
the wire. Decoded to 24-bit RGB via a 256-entry lookup table.
|
||||
- Four hardcoded sub-palettes referenced by Hextile/Tight: 1bpp B/W, 2bpp gray,
|
||||
@@ -53,21 +57,21 @@ Every byte derives from `aw.java`, `ac.java`, and `ByteColorRFBRenderer.java`.
|
||||
| Step | Direction | Bytes | Source |
|
||||
|------|-----------|-------|--------|
|
||||
| 1 | C→S | 75 bytes: `"e-RIC AUTH=" + APPLET_ID`, zero-padded, ISO-8859-1 | line 259 |
|
||||
| 2 | S→C | 1 byte status. `101 ('e')` = OK. `3` = error (read 4 more, see `aw.a(int)`) | line 264 |
|
||||
| 3 | S→C | 15 bytes: `"-RIC RFB MM.NN\n"` (digits ASCII) | `int(by)` line 395 |
|
||||
| 2 | S→C | 1 byte status. `101 ('e')` = OK. `3` = error (read i32 error code, see `aw.a(int)` line 350) | line 264 |
|
||||
| 3 | S→C | 15 bytes: `"-RIC RFB MM.NN\n"` (digits ASCII). Combined with the `e` from step 2, the full banner is `"e-RIC RFB MM.NN\n"` | `int(by)` line 395 |
|
||||
| 4 | S→C | 1 byte sync (`w.new()`) | line 270 |
|
||||
| 5 | S→C | varint length + N bytes server name | `l()` line 413 |
|
||||
| 5 | S→C | 1 pad byte + modified-UTF-8 string (u16 length prefix + N bytes) = server name | `l()` line 413 |
|
||||
| 6 | S→C | 1 byte sync | line 272 |
|
||||
| 7 | S→C | 25 bytes pixel-format-ish struct | `i()` line 519 |
|
||||
| 7 | S→C | Variable-length pixel-format struct: 1 byte flag + u16 int + u16 string-length + N bytes string | `i()` line 519 |
|
||||
| 8 | C→S | 16 bytes: `"e-RIC RFB " + PROTOCOL_VERSION + "\n"` | `try()` line 405 |
|
||||
| 9 | C→S | 2 bytes: `[shared_flag, port_id]` | `char()` line 425 |
|
||||
| 10 | S→C | 1 byte sync | line 276 |
|
||||
| 11 | S→C | 16 bytes ServerInit: `[bX, w_u16, h_u16, bE, a, try, a3, x_u16, ao_u16, v_u16, bM, bW, bU, 3 pad]` | `k()` line 435 |
|
||||
| 11 | S→C | 19 bytes ServerInit: `[bX(u8), w(u16), h(u16), bE(u8), a(u8), try(u8), a3(u8), x(u16), ao(u16), v(u16), bM(u8), bW(u8), bU(u8), 3 pad]` | `k()` line 435 |
|
||||
|
||||
After step 11, the client sends `SetEncodings` and a non-incremental
|
||||
`FramebufferUpdateRequest`, then enters the message dispatch loop.
|
||||
|
||||
**Server-to-client message types** (from `ac.char()` switch at line 244 of
|
||||
**Server-to-client message types** (from `ac.char()` switch at line 292 of
|
||||
`ac.java`):
|
||||
|
||||
| Type | Reader | Purpose |
|
||||
@@ -77,18 +81,18 @@ After step 11, the client sends `SetEncodings` and a non-incremental
|
||||
| 2 | — | Bell |
|
||||
| 3 | `aw.goto()` | ServerCutText |
|
||||
| 7 | `aw.l()` | server-name update |
|
||||
| 8 | `aw.e()` | pixel-format change (reads 25-byte struct) |
|
||||
| 8 | `aw.e()` line 537 | pixel-format change (1 pad + 4×u8 + 8×u16 = 21 bytes) |
|
||||
| 9 | `aw.else()` | layout/locale string |
|
||||
| 16 | `aw.i()` | desktop resize + name |
|
||||
| 17 | `aw.for()` | 2-byte ack (no-op) |
|
||||
| 128 | `aw.k()` | mode change / framebuffer reinit |
|
||||
| 131 | `aw.d()` | debug string |
|
||||
| 132 | `aw.long()` | RFB-command channel: two strings (key, value) |
|
||||
| 148 | `aw.b()` | **ping** — must respond with `aw.if(0)` (msg 149) |
|
||||
| 150 | `aw.do()` | bandwidth measurement probe |
|
||||
| 148 | `aw.b()` | **ping** — read 3 pad bytes + i32 payload, must respond with `aw.if(n)` (msg 149) |
|
||||
| 150 | `aw.do()` | bandwidth measurement probe (1 pad + u16 length + N bytes, read-only, no response) |
|
||||
| 161 | `aw.case()` | RDP/Host-Direct mode events |
|
||||
|
||||
**Per-rectangle encoding dispatch** (`ac.e()` at line 233 of `ac.java`):
|
||||
**Per-rectangle encoding dispatch** (`ac.e()` at line 218 of `ac.java`, switch at line 226):
|
||||
|
||||
| Encoding | Renderer method | Notes |
|
||||
|----------|-----------------|-------|
|
||||
@@ -107,9 +111,9 @@ After step 11, the client sends `SetEncodings` and a non-incremental
|
||||
| 2 | `a(int[], int)` line 597 | SetEncodings |
|
||||
| 3 | `a(...)` line 562 | FramebufferUpdateRequest |
|
||||
| 4 | `a(byte)` line 655 | KeyEvent (partial — see Phase 5) |
|
||||
| 5 | `a(false,...)` line 612 | PointerEvent: `[5, mask, x_u16, y_u16]` |
|
||||
| 5 | `a(false,...)` line 612 | PointerEvent: `[5, mask, x_u16, y_u16, extra_u16]` (8 bytes; extra=0 in absolute mode) |
|
||||
| 7 | `a(String)` line 418 | ClientCutText |
|
||||
| 149 | `if(int)` line 636 | PingResponse `[149, 0,0,0, n_u32]` |
|
||||
| 149 | `if(int)` line 636 | PingResponse `[-107(=149 unsigned), 0,0,0, n_i32]` (8 bytes) |
|
||||
|
||||
---
|
||||
|
||||
@@ -131,25 +135,33 @@ After step 11, the client sends `SetEncodings` and a non-incremental
|
||||
|
||||
## Phase 1 — Protocol primitives (`ericrfb/src/proto.rs`)
|
||||
|
||||
**Goal:** Faithful Rust port of `aw.java`'s read/write helpers. No I/O — all
|
||||
operations on `&mut impl AsyncRead` / `&mut impl AsyncWrite` (or `Buf`/`BufMut`
|
||||
for unit tests).
|
||||
**Goal:** Faithful Rust port of `h.java` + `aw.java` read/write helpers. No
|
||||
network I/O — all operations on `&[u8]` / `Cursor<&[u8]>` (sync `Read`/`Write`
|
||||
traits) so the codec is testable without tokio. Async wrappers added in Phase 2.
|
||||
|
||||
**Java references:**
|
||||
- `aw.int()` line 484 — varint, 1–3 bytes, top bit = continuation. **The single
|
||||
most important primitive; document it in a code comment with the exact byte
|
||||
examples.**
|
||||
- `aw.new()` line 454 — `read_u8`
|
||||
- `h.try()`, `h.do()`, `h.char()`, `h.byte()` — wrapped i8/u8/i16/string reads
|
||||
through the `h.java` adapter class. Map to `read_u8`, `read_u16_be`,
|
||||
read-length-prefixed-string.
|
||||
- `aw.f()` line 468 — rectangle header reader (varint x, varint y, varint w,
|
||||
varint h, u8 encoding). **Critical — these coords are varints, not u16.**
|
||||
- `h.java` — the low-level I/O adapter. Key methods:
|
||||
- `h.new()` line 106 — read 1 byte as unsigned int (u8)
|
||||
- `h.try()` line 91 — read 1 byte as signed (i8)
|
||||
- `h.int()` line 138 — read 2 bytes big-endian as unsigned int (u16)
|
||||
- `h.char()` line 121 — read 2 bytes big-endian as signed short (i16)
|
||||
- `h.do()` line 172 — read 4 bytes big-endian as signed int (i32)
|
||||
- `h.byte()` line 188 — read u16 length + N bytes of modified-UTF-8 string
|
||||
- `aw.int()` line 484 — varint, 1–3 bytes, top bit = continuation. Used for
|
||||
Tight compressed-stream lengths. Document with exact byte examples.
|
||||
- `aw.new()` line 454 — `read_u8` (delegates to `h` with a byte-count update)
|
||||
- `aw.f()` line 468 — rectangle header reader: `[x(u16), y(u16), w(u16),
|
||||
h(u16), encoding(i32)]` = **12 bytes fixed**. Coords use `h.int()` (u16-BE),
|
||||
encoding uses `h.do()` (i32-BE). **Do not confuse `this.w.int()` (h.int =
|
||||
u16) with `aw.int()` (varint) — the obfuscated names collide.**
|
||||
|
||||
**Tasks:**
|
||||
- [ ] `read_varint` / `write_*` primitives with property tests
|
||||
- [ ] `RectHeader { x: u32, y: u32, w: u32, h: u32, encoding: u8 }`
|
||||
- [ ] Length-prefixed-string reader with `ISO-8859-1` decode
|
||||
- [ ] `read_u8`, `read_i8`, `read_u16_be`, `read_i16_be`, `read_i32_be`
|
||||
primitives matching `h.java`
|
||||
- [ ] `read_varint` / `write_varint` with property tests
|
||||
- [ ] `RectHeader { x: u16, y: u16, w: u16, h: u16, encoding: i32 }` (12 bytes)
|
||||
- [ ] Modified-UTF-8 string reader (u16 length prefix + Java modified-UTF-8
|
||||
body, per `h.byte()` line 188)
|
||||
- [ ] Unit tests using fixed byte vectors — at least one for each primitive
|
||||
|
||||
**Deliverable:** `cargo test -p ericrfb proto::` covers every primitive used
|
||||
@@ -171,9 +183,10 @@ read_half, write_half)`.
|
||||
bytes)
|
||||
- `aw.char()` line 425 — 2-byte port-init message
|
||||
- `aw.k()` line 435 — ServerInit reader
|
||||
- `aw.i()` line 519 — pixel-format struct reader
|
||||
- `aw.a(int)` line 350 — error code → string (1=no perm, 2=exclusive, 6=auth
|
||||
failed, etc.)
|
||||
- `aw.i()` line 519 — pixel-format struct reader (variable-length: 1 byte flag +
|
||||
u16 int + u16 string-length + N bytes)
|
||||
- `aw.a(int)` line 350 — error code → string (1=no perm, 2=exclusive, 3=manually
|
||||
rejected, 4=password disabled, 5=loopback, 6=auth failed, 7=port denied)
|
||||
|
||||
**Tasks:**
|
||||
- [ ] `Config { host, port, applet_id, protocol_version, port_id, shared }`
|
||||
@@ -196,10 +209,12 @@ to a `Framebuffer { width, height, pixels: Vec<u8> }` (8bpp), expand to RGBA via
|
||||
LUT, write a PNG to disk.
|
||||
|
||||
**Java references:**
|
||||
- `ac.char()` line 244 (of `ac.java`) — server-msg dispatch loop
|
||||
- `ac.f()` line 213 → `ac.e()` line 165 — FramebufferUpdate reader
|
||||
- `aw.null()` line 459 — FramebufferUpdate header (1 pad + u16 num_rects)
|
||||
- `aw.f()` line 468 — per-rectangle header
|
||||
- `ac.char()` line 283 (of `ac.java`), switch at line 292 — server-msg dispatch
|
||||
- `ac.f()` line 273 → `ac.e()` line 218 — FramebufferUpdate handler → per-rect
|
||||
encoding dispatch (switch at line 226)
|
||||
- `aw.null()` line 459 — FramebufferUpdate header (1 pad byte via `h.try()` +
|
||||
u16 num_rects via `h.int()`)
|
||||
- `aw.f()` line 468 — per-rectangle header (4×u16 + i32 = 12 bytes)
|
||||
- `ByteColorRFBRenderer.if(x,y,w,h)` line 98 — Raw decoder (read w*h bytes,
|
||||
blit at offset)
|
||||
- `ByteColorRFBRenderer` constructor lines 57–74 — RGB332 → 24bpp LUT
|
||||
@@ -233,15 +248,18 @@ us a long way.
|
||||
Hextile uses 1-byte direct color tokens only.
|
||||
- `ByteColorRFBRenderer.a(srcX, srcY, x, y, w, h)` line 165 — CopyRect: trivial
|
||||
source→dest blit within the framebuffer
|
||||
- `aw.b()` line 629 — ping reader (3 bytes ignored + 1 byte payload)
|
||||
- `aw.if(int)` line 636 — ping response writer
|
||||
- `aw.b()` line 629 — ping reader: 3 pad bytes (`h.try()` × 3) + i32 payload
|
||||
(`h.do()`). Total 7 bytes.
|
||||
- `aw.if(int)` line 636 — ping response writer: `[-107, 0, 0, 0, n_i32]` = 8
|
||||
bytes. Echo the i32 payload from `aw.b()` back.
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Hextile decoder, with subrect bit-flag handling matching lines 192–238
|
||||
- [ ] CopyRect handler with overlap-safe `copy_within` semantics
|
||||
- [ ] Wire up message types 2 (Bell, log only), 17 (no-op ack), 131 (debug string,
|
||||
log), 132 (RFB command, log key=value), 148 (ping → respond), 150
|
||||
(bandwidth probe — echo per `aw.do()` line 642)
|
||||
- [ ] Wire up message types 2 (Bell, log only), 17 (no-op ack, reads 2 bytes per
|
||||
`aw.for()` line 553), 131 (debug string, log), 132 (RFB command, log
|
||||
key=value), 148 (ping → echo i32 payload back as msg 149), 150 (bandwidth
|
||||
probe — read and discard per `aw.do()` line 642, no response needed)
|
||||
- [ ] Type 16 (resize) + type 128 (mode change) trigger framebuffer
|
||||
reallocation; emit a `Event::Resize(w, h)` to consumers
|
||||
- [ ] `examples/record.rs`: 30-second session, save 1 PNG/sec to `out/`
|
||||
@@ -296,9 +314,10 @@ identical for static screens.
|
||||
actually usable.
|
||||
|
||||
**Java references:**
|
||||
- `aw.a(boolean, int, int, int, int, int)` line 612 — PointerEvent writer.
|
||||
Payload: `[5, button_mask, x_u16, y_u16]`. With `bl=true`, byte 0 becomes 147
|
||||
for "exclusive/relative" mode — ignore for v1.
|
||||
- `aw.a(boolean, int, int, int, int)` line 612 — PointerEvent writer.
|
||||
Payload: `[5, button_mask, x_u16, y_u16, extra_u16]` = 8 bytes. `extra` is
|
||||
always 0 in absolute mode. With `bl=true`, byte 0 becomes 147 for
|
||||
"exclusive/relative" mode — ignore for v1.
|
||||
- `MouseHandlerAbsolute.java` — confirms button-mask bit layout matches RFB
|
||||
(bit 0 = left, bit 1 = middle, bit 2 = right, bit 3/4 = wheel up/down)
|
||||
- `KeyEventHandler.java` + `nn.pp.rckbd.*` — the keyboard handler is more
|
||||
@@ -309,7 +328,8 @@ actually usable.
|
||||
- `KbdMapping_en.java` (in rcsoftkbd) — alternate mapping path
|
||||
|
||||
**Tasks:**
|
||||
- [ ] `PointerEvent { mask: u8, x: u16, y: u16 }` writer
|
||||
- [ ] `PointerEvent { mask: u8, x: u16, y: u16, extra: u16 }` writer (8 bytes;
|
||||
extra=0 for absolute mode)
|
||||
- [ ] Browser-side: capture `mousemove`/`mousedown`/`mouseup` → WS frames
|
||||
- [ ] Initial keyboard: send raw scancodes via msg type 4 with the 104pc layout
|
||||
table verbatim from `KbdLayout_104pc.java`
|
||||
@@ -431,7 +451,7 @@ be needed.
|
||||
| Encoding 9 needed for usable framerates | Low | SetEncodings doesn't advertise 9 unless user picks "advanced" mode in `RemoteConsoleApplet.if(d2)` line 423; default `[255, 7, -250]` works fine |
|
||||
| Keyboard wire format more complex than `aw.a(byte)` suggests | Medium | `KeyEventHandler.java` is small; read it before Phase 6 |
|
||||
| HTTP login flow has CSRF / session-binding quirks | Medium | Capture with DevTools first; any irregularities are scriptable |
|
||||
| Varint vs u16 confusion in `aw` | High during development | Strict types on `proto.rs` primitives; never use raw integers |
|
||||
| `h.int()` (u16) vs `aw.int()` (varint) name collision | High during development | `aw.f()` rect headers use `h.int()` = u16; `aw.int()` varint is only for Tight stream lengths. Strict newtypes on `proto.rs` primitives; never use raw integers |
|
||||
| Zlib stream desync from dropped/skipped rectangles | High if architecture wrong | Decoder owns the stream; consumers can drop output but never input |
|
||||
|
||||
## Success criteria
|
||||
|
||||
Reference in New Issue
Block a user