Implements RFC 6143's Tight encoding: a compression-control byte (low
4 bits reset one of 4 persistent zlib streams; high nibble selects
Fill/JPEG/Basic mode) followed by Fill's 3-byte solid color, JPEG's
compact-length-prefixed baseline JPEG covering the whole rectangle
(decoded via libjpeg-turbo directly, not QImage's plugin, to avoid a
packaging-dependent runtime failure mode), or Basic mode's
compact-length-prefixed zlib payload plus a filter (Copy, Palette, or
Gradient) applied after decompression. Unlike Hextile/ZRLE, Tight has
no internal tiling -- one rectangle is one filtered/compressed unit.
The three filters live in vnc_pixel_codecs.h/.cpp alongside the
Hextile/ZRLE decoders. Adds find_package(JPEG REQUIRED) + JPEG::JPEG
as a new build dependency (confirmed available via libjpeg-turbo on
this dev machine). 5 new tests cover Fill, Basic+Copy, Basic+Palette,
JPEG (round-tripped through a real libjpeg-turbo-encoded fixture,
compared with tolerance since JPEG is lossy), and the stream-reset
flag correctly tearing down and reinitializing a targeted stream
rather than erroring on stale state.
Known, documented gap: this decoder always treats Basic-mode payloads
as zlib-compressed; the real protocol allows very small payloads to
skip compression, which couldn't be verified with confidence against
the RFC text alone and is narrow enough in practice (tiny solid areas
are virtually always sent as Fill instead) to leave unhandled for now
-- it fails that one rectangle's decode cleanly rather than
misinterpreting it silently. The Gradient filter is implemented from
the spec description but is the least exercised of the three in this
pass.
Live-verified against the TightVNC test server that nothing
regressed; that server still consistently chose Raw for actual
framebuffer content regardless of announced encodings, so Tight's live
decode path isn't independently confirmed against a real server here
either -- the unit tests are the primary evidence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements RFC 6143 SS7.7.6: a ZRLE rectangle is a 4-byte compressed
length followed by that many zlib-compressed bytes, decompressing to
64x64 tiles each using one of five subencodings (Raw, Solid, packed
palette, Plain RLE, Palette RLE). The zlib stream persists for the
whole connection rather than being reset per-rectangle or per-update,
so VncSessionBackend now owns a lazily-initialized, persistent
z_stream torn down only in resetProtocolState() on a fresh
connect/reconnect.
Since the entire rectangle's compressed data decompresses into memory
in one shot, tile parsing is a plain synchronous loop rather than
needing its own RfbState values -- only the compressed-length and
compressed-data reads are actual protocol states. Tile decoding (the
five subencodings, including the continuation-byte run-length
encoding shared by two of them) lives in vnc_pixel_codecs.h/.cpp
alongside the Hextile decoder, unit-tested with 6 new tests covering
each subencoding plus a persistence test that splits one continuous
deflate stream across two separate FramebufferUpdate messages -- it
only decodes correctly if the connection's inflate stream is retained
between them.
Adds a top-level find_package(ZLIB REQUIRED) + ZLIB::ZLIB link
(previously only pulled in transitively via vendored FreeRDP's own
smartcard-emulation feature, which happened to have it enabled but
shouldn't be relied on for that).
Live-verified against the TightVNC test server that nothing regressed
(connect, cursor, clipboard); that server consistently sends Raw for
actual framebuffer content regardless of announced encodings, so
Hextile/ZRLE's live decode path isn't independently confirmed against
a real server -- the unit tests are the primary evidence here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
VNC has no equivalent of RDP's MS-RDPEDISP to request a different
resolution from the guest, so a high-resolution remote desktop
previously always got shrunk to fit the window, making text
illegible. Adds a per-tab "Display Mode" choice (tab-bar right-click)
between the existing scale-to-fit behavior and a new actual-size mode
that renders the framebuffer at its native pixel size inside a
QScrollArea. Reuses VncDisplayWidget's existing scale-to-fit render
math unchanged -- it degenerates to an exact 1:1 mapping once the
widget is fixed to the remote's own size. Persisted like the terminal
theme preference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements RFB (RFC 6143) directly against QTcpSocket. No permissively
licensed VNC client library exists to vendor the way FreeRDP was for RDP:
LibVNCClient is GPLv2, gtk-vnc is LGPL but GTK-tied, and vendoring either
would force a licensing decision on the whole (MIT) project. This is
from-scratch protocol code instead, threaded like SshSessionBackend (a
QObject on its own QThread driven by Qt's own async socket signals)
rather than RdpSessionBackend's manual worker-thread/blocking-loop
pattern, since QTcpSocket is already async.
Scope, matching SessionTab's existing SSH/RDP dispatch pattern
(session_backend_factory.cpp, session_tab.cpp's widget construction and
signal wiring) and VncDisplayWidget mirroring RdpDisplayWidget's
scale-to-fit rendering:
- Protocol handshake: RFB 3.3/3.7/3.8 negotiated explicitly (the
SecurityResult message only exists in 3.8; pre-3.8 servers signal auth
failure by closing the socket, which the disconnect handler accounts
for)
- VNC Authentication (DES challenge-response, via OpenSSL's classic DES
API) and no-auth security types
- Raw + CopyRect framebuffer decoding into a persistent QImage, requesting
a fixed 32bpp format whose byte layout matches QImage::Format_RGB32
directly (same zero-conversion trick RdpSessionBackend uses for
FreeRDP's GDI buffer)
- Keyboard (Qt key -> X11 keysym, including the Unicode-beyond-Latin-1
keysym convention) and mouse/wheel input forwarding
Explicit non-goals for this pass (see docs/PROGRESS.md for the full
list): Apple's Screen Sharing auth (so this can't yet reach macOS's
built-in VNC server), compression encodings beyond Raw/CopyRect, dynamic
resize, remote cursor shape sync, clipboard sync.
19 unit tests (tests/test_vnc_session_backend.cpp): pure-function
coverage (DES key prep verified against an independently documented test
vector for password "COW", X11 keysym mapping, socket-error mapping) plus
state-machine coverage against a scripted in-process fake RFB server
covering all three protocol-version handshake shapes, auth success/
failure, unsupported security types, and pixel-accurate Raw decoding.
That harness caught a real re-entrancy bug: QAbstractSocket::abort()
synchronously re-emits disconnected() before returning, so
failConnection() calling it was silently letting a second, generic
disconnected-socket handler overwrite an already-correct, specific error
message.
Also verified live against a real, independently implemented VNC server
(TightVNC on Windows): connect with VNC Authentication, correct
framebuffer dimensions and pixel data, clean disconnect, reconnect.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Linux, macOS, and Windows have all been validated against the current
feature set (SSH, RDP display/keyboard/clipboard/cursor, Tab forwarding,
key repeat, profile dialog, single-window UI), with the platform-specific
bugs found along the way fixed and tracked individually on the issue
tracker (#8-#16). VNC validation remains out of scope while Milestone 6
stays deferred.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>