diff --git a/doc/plan/implementation.md b/doc/plan/implementation.md index acc1fec..a11beeb 100644 --- a/doc/plan/implementation.md +++ b/doc/plan/implementation.md @@ -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 }` (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