Internal
Public Access
Add VNC Tight decoding
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>
This commit is contained in:
@@ -15,6 +15,7 @@ include(GNUInstallDirs)
|
|||||||
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets Sql Network)
|
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets Sql Network)
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
find_package(ZLIB REQUIRED)
|
find_package(ZLIB REQUIRED)
|
||||||
|
find_package(JPEG REQUIRED)
|
||||||
|
|
||||||
qt_standard_project_setup()
|
qt_standard_project_setup()
|
||||||
|
|
||||||
@@ -157,6 +158,7 @@ target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql Qt6::Network)
|
|||||||
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
|
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
|
||||||
target_link_libraries(orbithub PRIVATE OpenSSL::Crypto)
|
target_link_libraries(orbithub PRIVATE OpenSSL::Crypto)
|
||||||
target_link_libraries(orbithub PRIVATE ZLIB::ZLIB)
|
target_link_libraries(orbithub PRIVATE ZLIB::ZLIB)
|
||||||
|
target_link_libraries(orbithub PRIVATE JPEG::JPEG)
|
||||||
target_compile_definitions(orbithub PRIVATE ORBITHUB_VERSION_STRING="${PROJECT_VERSION}")
|
target_compile_definitions(orbithub PRIVATE ORBITHUB_VERSION_STRING="${PROJECT_VERSION}")
|
||||||
if(TARGET freerdp AND TARGET winpr)
|
if(TARGET freerdp AND TARGET winpr)
|
||||||
target_compile_definitions(orbithub PRIVATE ORBITHUB_HAS_FREERDP)
|
target_compile_definitions(orbithub PRIVATE ORBITHUB_HAS_FREERDP)
|
||||||
|
|||||||
+44
-19
@@ -121,25 +121,26 @@ Delivered:
|
|||||||
keyboard (Qt key -> X11 keysym mapping) and mouse/wheel input forwarding
|
keyboard (Qt key -> X11 keysym mapping) and mouse/wheel input forwarding
|
||||||
- `VncDisplayWidget` mirroring `RdpDisplayWidget`'s scale-to-fit rendering
|
- `VncDisplayWidget` mirroring `RdpDisplayWidget`'s scale-to-fit rendering
|
||||||
and input-forwarding shape
|
and input-forwarding shape
|
||||||
- 36 unit tests (`tests/test_vnc_session_backend.cpp`): pure-function
|
- 41 unit tests (`tests/test_vnc_session_backend.cpp`): pure-function
|
||||||
coverage (DES key prep verified against an independently documented test
|
coverage (DES key prep verified against an independently documented test
|
||||||
vector, keysym mapping, socket-error mapping) plus state-machine
|
vector, keysym mapping, socket-error mapping) plus state-machine
|
||||||
coverage against a scripted in-process fake RFB server (all three
|
coverage against a scripted in-process fake RFB server (all three
|
||||||
protocol-version handshake shapes, auth success/failure, unsupported
|
protocol-version handshake shapes, auth success/failure, unsupported
|
||||||
security types, pixel-accurate Raw/Hextile/ZRLE decoding including a
|
security types, pixel-accurate Raw/Hextile/ZRLE/Tight decoding including
|
||||||
ZRLE zlib-stream-persistence test across two separate
|
a ZRLE zlib-stream-persistence test across two separate
|
||||||
`FramebufferUpdate` messages, cursor/clipboard round trips) -- caught
|
`FramebufferUpdate` messages, a Tight stream-reset-flag test, cursor/
|
||||||
and fixed a real re-entrancy bug (`abort()` synchronously re-firing
|
clipboard round trips) -- caught and fixed a real re-entrancy bug
|
||||||
`disconnected()` mid-`failConnection()`, silently overwriting a specific
|
(`abort()` synchronously re-firing `disconnected()` mid-
|
||||||
error with a generic one)
|
`failConnection()`, silently overwriting a specific error with a
|
||||||
|
generic one)
|
||||||
- Verified live against a real, independently implemented VNC server
|
- Verified live against a real, independently implemented VNC server
|
||||||
(TightVNC on Windows): connect with VNC Authentication, correct
|
(TightVNC on Windows): connect with VNC Authentication, correct
|
||||||
framebuffer dimensions and pixel data, clean disconnect, reconnect,
|
framebuffer dimensions and pixel data, clean disconnect, reconnect,
|
||||||
live cursor-shape and clipboard-send checks. That particular server
|
live cursor-shape and clipboard-send checks. That particular server
|
||||||
consistently chose Raw for actual framebuffer content regardless of
|
consistently chose Raw for actual framebuffer content regardless of
|
||||||
which compression encodings were announced, so Hextile/ZRLE's
|
which compression encodings were announced, so Hextile/ZRLE/Tight's
|
||||||
real-world decode path isn't independently confirmed live -- the unit
|
real-world decode path isn't independently confirmed live -- the unit
|
||||||
tests are the primary correctness evidence for those two
|
tests are the primary correctness evidence for those
|
||||||
- Per-tab VNC-only display mode toggle (tab-bar right-click ->
|
- Per-tab VNC-only display mode toggle (tab-bar right-click ->
|
||||||
`Display Mode`): `Scale to Fit` (default, matches RDP's behavior) or
|
`Display Mode`): `Scale to Fit` (default, matches RDP's behavior) or
|
||||||
`Actual Size (Scrollbars)` -- renders the remote framebuffer at its
|
`Actual Size (Scrollbars)` -- renders the remote framebuffer at its
|
||||||
@@ -158,22 +159,46 @@ Delivered:
|
|||||||
Unicode extension) in both directions
|
Unicode extension) in both directions
|
||||||
- Remote cursor shape sync via RFB's Cursor pseudo-encoding, mirroring
|
- Remote cursor shape sync via RFB's Cursor pseudo-encoding, mirroring
|
||||||
`RdpDisplayWidget`'s cursor handling in `VncDisplayWidget`
|
`RdpDisplayWidget`'s cursor handling in `VncDisplayWidget`
|
||||||
- Hextile and ZRLE compression encodings, in addition to Raw + CopyRect --
|
- Hextile, ZRLE, and Tight compression encodings, in addition to Raw +
|
||||||
meaningfully reduces bandwidth over slower links versus Raw alone. Pure
|
CopyRect -- meaningfully reduces bandwidth over slower links versus Raw
|
||||||
tile/pixel decode logic lives in `src/vnc_pixel_codecs.h/.cpp`, kept
|
alone; Tight in particular is the encoding most real VNC servers prefer
|
||||||
separate from the wire-sequencing state machine so it's unit-testable
|
when the client offers it. Pure tile/pixel decode logic (including
|
||||||
without a socket. ZRLE decoding links `ZLIB::ZLIB` (found via a fresh
|
Tight's three filters -- Copy, Palette, Gradient) lives in
|
||||||
top-level `find_package(ZLIB REQUIRED)`, independent of whether
|
`src/vnc_pixel_codecs.h/.cpp`, kept separate from the wire-sequencing
|
||||||
vendored FreeRDP's own internal zlib usage stays enabled)
|
state machine so it's unit-testable without a socket. ZRLE and Tight's
|
||||||
|
Basic mode share the same "persistent zlib stream(s), decompress
|
||||||
|
in-memory, decode synchronously" approach (Tight has 4 independent
|
||||||
|
streams selected per-rectangle, individually reset via the
|
||||||
|
compression-control byte's low 4 bits). Tight's JPEG sub-mode decodes
|
||||||
|
via libjpeg-turbo directly (`find_package(JPEG REQUIRED)` ->
|
||||||
|
`JPEG::JPEG`), not `QImage`'s own JPEG plugin, to avoid a
|
||||||
|
packaging-dependent runtime failure mode. ZRLE/Tight link `ZLIB::ZLIB`
|
||||||
|
(found via a fresh top-level `find_package(ZLIB REQUIRED)`, independent
|
||||||
|
of whether vendored FreeRDP's own internal zlib usage stays enabled)
|
||||||
|
- 41 unit tests total, 15 of them for Hextile/ZRLE/Tight specifically,
|
||||||
|
including a ZRLE zlib-stream-persistence test across two separate
|
||||||
|
`FramebufferUpdate` messages and a Tight stream-reset-flag test proving
|
||||||
|
the low 4 control-byte bits actually tear down and reinitialize the
|
||||||
|
targeted stream rather than erroring out on stale state
|
||||||
|
|
||||||
Known gaps (explicit scope decisions, not oversights -- see issue #3 for
|
Known gaps (explicit scope decisions, not oversights -- see issue #3 for
|
||||||
follow-up tracking):
|
follow-up tracking):
|
||||||
- Apple's Screen Sharing authentication (Diffie-Hellman + AES, security
|
- Apple's Screen Sharing authentication (Diffie-Hellman + AES, security
|
||||||
type 30) isn't implemented, so this can't yet reach macOS's built-in VNC
|
type 30) isn't implemented, so this can't yet reach macOS's built-in VNC
|
||||||
server -- only standard VNC Authentication (type 2) and no-auth (type 1)
|
server -- only standard VNC Authentication (type 2) and no-auth (type 1)
|
||||||
- Tight compression isn't implemented yet (Raw + CopyRect + Hextile + ZRLE
|
- Tight's Basic compression mode always assumes zlib-compressed payloads;
|
||||||
are); Tight typically compresses best of all of these, so bandwidth over
|
the real protocol permits the server to skip compression for very small
|
||||||
very slow links is still not as good as a full VNC client would achieve
|
(filtered byte count under ~12) payloads, which this decoder doesn't
|
||||||
|
special-case (the exact trigger/wire-signaling for that couldn't be
|
||||||
|
verified with confidence against the RFC text alone). In practice this
|
||||||
|
only affects rare, tiny rectangles -- solid or near-solid tiny areas are
|
||||||
|
virtually always sent as Fill instead -- and fails that one rectangle's
|
||||||
|
decode cleanly (disconnects with a clear error) rather than silently
|
||||||
|
misinterpreting it
|
||||||
|
- Tight's Gradient filter is implemented from RFC 6143's description but
|
||||||
|
is the least exercised/confirmed of the three filters against a real
|
||||||
|
server in this pass (most real-world Tight traffic uses Copy or
|
||||||
|
Palette)
|
||||||
- No dynamic resize (connects at the server's native resolution; the
|
- No dynamic resize (connects at the server's native resolution; the
|
||||||
`Scale to Fit`/`Actual Size` toggle changes how that fixed resolution is
|
`Scale to Fit`/`Actual Size` toggle changes how that fixed resolution is
|
||||||
displayed locally, not what resolution is requested from the guest --
|
displayed locally, not what resolution is requested from the guest --
|
||||||
|
|||||||
@@ -251,4 +251,119 @@ int decodeZrleTile(const QByteArray& data, int offset, int tileWidth, int tileHe
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVector<QRgb> decodeTightCopyFilter(const QByteArray& data, int width, int height)
|
||||||
|
{
|
||||||
|
const int pixelCount = width * height;
|
||||||
|
QVector<QRgb> pixels;
|
||||||
|
if (pixelCount <= 0 || data.size() < pixelCount * 3) {
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
pixels.reserve(pixelCount);
|
||||||
|
const auto* bytes = reinterpret_cast<const uchar*>(data.constData());
|
||||||
|
for (int i = 0; i < pixelCount; ++i) {
|
||||||
|
pixels.append(rgbFromPixelBytes(bytes + (i * 3)));
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QRgb> decodeTightPaletteFilter(const QByteArray& data, int width, int height)
|
||||||
|
{
|
||||||
|
QVector<QRgb> pixels;
|
||||||
|
const int pixelCount = width * height;
|
||||||
|
if (pixelCount <= 0 || data.isEmpty()) {
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* bytes = reinterpret_cast<const uchar*>(data.constData());
|
||||||
|
const int size = data.size();
|
||||||
|
int pos = 0;
|
||||||
|
const int paletteSize = static_cast<int>(bytes[pos]) + 1; // 1-256 colors
|
||||||
|
++pos;
|
||||||
|
if (pos + (paletteSize * 3) > size) {
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QRgb> palette;
|
||||||
|
palette.reserve(paletteSize);
|
||||||
|
for (int i = 0; i < paletteSize; ++i) {
|
||||||
|
palette.append(rgbFromPixelBytes(bytes + pos));
|
||||||
|
pos += 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bitsPerPixel = 8;
|
||||||
|
if (paletteSize <= 2) {
|
||||||
|
bitsPerPixel = 1;
|
||||||
|
} else if (paletteSize <= 4) {
|
||||||
|
bitsPerPixel = 2;
|
||||||
|
} else if (paletteSize <= 16) {
|
||||||
|
bitsPerPixel = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
pixels.reserve(pixelCount);
|
||||||
|
int bitPos = 0;
|
||||||
|
for (int i = 0; i < pixelCount; ++i) {
|
||||||
|
const int byteIndex = pos + (bitPos / 8);
|
||||||
|
if (byteIndex >= size) {
|
||||||
|
return QVector<QRgb>();
|
||||||
|
}
|
||||||
|
const int shift = 8 - (bitPos % 8) - bitsPerPixel;
|
||||||
|
const int mask = (1 << bitsPerPixel) - 1;
|
||||||
|
const int index = (bytes[byteIndex] >> shift) & mask;
|
||||||
|
if (index >= palette.size()) {
|
||||||
|
return QVector<QRgb>();
|
||||||
|
}
|
||||||
|
pixels.append(palette.at(index));
|
||||||
|
bitPos += bitsPerPixel;
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QRgb> decodeTightGradientFilter(const QByteArray& data, int width, int height)
|
||||||
|
{
|
||||||
|
const int pixelCount = width * height;
|
||||||
|
QVector<QRgb> pixels;
|
||||||
|
if (pixelCount <= 0 || data.size() < pixelCount * 3) {
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* bytes = reinterpret_cast<const uchar*>(data.constData());
|
||||||
|
auto predict = [](int left, int up, int upLeft) {
|
||||||
|
return qBound(0, left + up - upLeft, 255);
|
||||||
|
};
|
||||||
|
|
||||||
|
QVector<int> rChan(pixelCount);
|
||||||
|
QVector<int> gChan(pixelCount);
|
||||||
|
QVector<int> bChan(pixelCount);
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const int idx = (y * width) + x;
|
||||||
|
const uchar* px = bytes + (idx * 3);
|
||||||
|
// TPIXEL order matches rgbFromPixelBytes: B,G,R.
|
||||||
|
const int deltaB = px[0];
|
||||||
|
const int deltaG = px[1];
|
||||||
|
const int deltaR = px[2];
|
||||||
|
|
||||||
|
const int leftR = (x > 0) ? rChan[idx - 1] : 0;
|
||||||
|
const int leftG = (x > 0) ? gChan[idx - 1] : 0;
|
||||||
|
const int leftB = (x > 0) ? bChan[idx - 1] : 0;
|
||||||
|
const int upR = (y > 0) ? rChan[idx - width] : 0;
|
||||||
|
const int upG = (y > 0) ? gChan[idx - width] : 0;
|
||||||
|
const int upB = (y > 0) ? bChan[idx - width] : 0;
|
||||||
|
const int upLeftR = (x > 0 && y > 0) ? rChan[idx - width - 1] : 0;
|
||||||
|
const int upLeftG = (x > 0 && y > 0) ? gChan[idx - width - 1] : 0;
|
||||||
|
const int upLeftB = (x > 0 && y > 0) ? bChan[idx - width - 1] : 0;
|
||||||
|
|
||||||
|
rChan[idx] = (predict(leftR, upR, upLeftR) + deltaR) & 0xFF;
|
||||||
|
gChan[idx] = (predict(leftG, upG, upLeftG) + deltaG) & 0xFF;
|
||||||
|
bChan[idx] = (predict(leftB, upB, upLeftB) + deltaB) & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pixels.reserve(pixelCount);
|
||||||
|
for (int i = 0; i < pixelCount; ++i) {
|
||||||
|
pixels.append(qRgb(rChan.at(i), gChan.at(i), bChan.at(i)));
|
||||||
|
}
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,36 @@ QVector<HextileSubrect> decodeHextileSubrects(bool coloured, int subrectCount,
|
|||||||
int decodeZrleTile(const QByteArray& data, int offset, int tileWidth, int tileHeight,
|
int decodeZrleTile(const QByteArray& data, int offset, int tileWidth, int tileHeight,
|
||||||
QVector<QRgb>& pixels);
|
QVector<QRgb>& pixels);
|
||||||
|
|
||||||
|
// Tight encoding (RFC 6143 SS7.7.4) filters. Unlike Hextile/ZRLE, a Tight
|
||||||
|
// rectangle is never internally tiled -- these operate on the whole
|
||||||
|
// rectangle's already-decompressed (or, for very small payloads the real
|
||||||
|
// protocol allows to skip compression entirely, raw -- NOT handled by this
|
||||||
|
// implementation, see VncSessionBackend's class comment) filtered byte
|
||||||
|
// stream at once. Each returns exactly width*height pixels on success; a
|
||||||
|
// short/malformed result (any size other than width*height, including an
|
||||||
|
// empty vector) signals truncated/invalid input to the caller.
|
||||||
|
|
||||||
|
// "Copy" filter: `data` is exactly width*height TPIXELs (3 bytes each,
|
||||||
|
// row-major, same B,G,R order as rgbFromPixelBytes/ZRLE's CPIXEL).
|
||||||
|
QVector<QRgb> decodeTightCopyFilter(const QByteArray& data, int width, int height);
|
||||||
|
|
||||||
|
// "Palette" filter: `data` is a 1-byte (paletteSize-1) count, then
|
||||||
|
// paletteSize TPIXELs, then a *continuous* (not row-padded, unlike ZRLE's
|
||||||
|
// packed palette) MSB-first bit-packed index stream covering width*height
|
||||||
|
// pixels, with bits-per-pixel derived from paletteSize the same way ZRLE's
|
||||||
|
// packed palette does (<=2 colors: 1 bit; <=4: 2 bits; <=16: 4 bits;
|
||||||
|
// otherwise 8 bits/1 byte per index, up to 256 colors).
|
||||||
|
QVector<QRgb> decodeTightPaletteFilter(const QByteArray& data, int width, int height);
|
||||||
|
|
||||||
|
// "Gradient" filter: `data` is exactly width*height TPIXELs, each channel
|
||||||
|
// (R,G,B independently) carrying a delta from a predicted value computed
|
||||||
|
// from already-decoded neighbors (predicted = clamp(left + up - upleft,
|
||||||
|
// 0, 255); treated as 0 past the first row/column). This is the least
|
||||||
|
// commonly exercised of the three Tight filters in real-world traffic and
|
||||||
|
// the one this implementation has the lowest confidence in byte-for-byte
|
||||||
|
// -- flagged for extra scrutiny/testing.
|
||||||
|
QVector<QRgb> decodeTightGradientFilter(const QByteArray& data, int width, int height);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+321
-3
@@ -12,6 +12,10 @@
|
|||||||
|
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <jpeglib.h>
|
||||||
|
}
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -38,14 +42,26 @@ constexpr qint32 kEncRaw = 0;
|
|||||||
constexpr qint32 kEncCopyRect = 1;
|
constexpr qint32 kEncCopyRect = 1;
|
||||||
constexpr qint32 kEncHextile = 5;
|
constexpr qint32 kEncHextile = 5;
|
||||||
constexpr qint32 kEncZRLE = 16;
|
constexpr qint32 kEncZRLE = 16;
|
||||||
|
constexpr qint32 kEncTight = 7;
|
||||||
// RFC 6143 SS7.8.2 "Cursor pseudo-encoding": not a real screen-content
|
// RFC 6143 SS7.8.2 "Cursor pseudo-encoding": not a real screen-content
|
||||||
// encoding -- a rectangle with this type carries a cursor shape update
|
// encoding -- a rectangle with this type carries a cursor shape update
|
||||||
// instead (hotspot in x/y, image dims in width/height), never painted into
|
// instead (hotspot in x/y, image dims in width/height), never painted into
|
||||||
// the framebuffer.
|
// the framebuffer.
|
||||||
constexpr qint32 kEncCursor = -239;
|
constexpr qint32 kEncCursor = -239;
|
||||||
|
|
||||||
constexpr std::array<qint32, 5> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncHextile,
|
constexpr std::array<qint32, 6> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncHextile,
|
||||||
kEncZRLE, kEncCursor };
|
kEncZRLE, kEncTight, kEncCursor };
|
||||||
|
|
||||||
|
// Tight compression-control byte, high nibble (after the low 4 bits'
|
||||||
|
// stream-reset flags are stripped off).
|
||||||
|
constexpr quint8 kTightModeFill = 0x08;
|
||||||
|
constexpr quint8 kTightModeJpeg = 0x09;
|
||||||
|
// Modes 0-7 are "Basic" compression; bits 0-1 of the mode select which of
|
||||||
|
// the 4 persistent zlib streams to use, bit 2 flags that an explicit
|
||||||
|
// filter-id byte follows (Copy is implied when it's clear).
|
||||||
|
constexpr quint8 kTightBasicModeMax = 0x07;
|
||||||
|
constexpr quint8 kTightBasicExplicitFilterFlag = 0x04;
|
||||||
|
constexpr quint8 kTightBasicStreamIndexMask = 0x03;
|
||||||
|
|
||||||
constexpr int kZrleTileSize = 64;
|
constexpr int kZrleTileSize = 64;
|
||||||
|
|
||||||
@@ -81,6 +97,62 @@ void appendU32BE(QByteArray& buf, quint32 value)
|
|||||||
buf.append(static_cast<char>((value >> 8) & 0xFF));
|
buf.append(static_cast<char>((value >> 8) & 0xFF));
|
||||||
buf.append(static_cast<char>(value & 0xFF));
|
buf.append(static_cast<char>(value & 0xFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decodes a Tight "JPEG" rectangle (a full baseline JPEG image covering
|
||||||
|
// the whole rectangle, no tiling) via libjpeg-turbo directly -- chosen
|
||||||
|
// over QImage::fromData(..., "JPEG") specifically to avoid depending on
|
||||||
|
// Qt's own JPEG plugin being present at runtime across every packaging
|
||||||
|
// target. Returns false on any decode failure or dimension mismatch
|
||||||
|
// rather than risk painting a misinterpreted image.
|
||||||
|
bool decodeJpegRectangle(const QByteArray& jpegBytes, int width, int height, QImage* outImage)
|
||||||
|
{
|
||||||
|
if (jpegBytes.isEmpty() || width <= 0 || height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
jpeg_decompress_struct cinfo;
|
||||||
|
jpeg_error_mgr jerr;
|
||||||
|
cinfo.err = jpeg_std_error(&jerr);
|
||||||
|
jpeg_create_decompress(&cinfo);
|
||||||
|
|
||||||
|
jpeg_mem_src(&cinfo, reinterpret_cast<const unsigned char*>(jpegBytes.constData()),
|
||||||
|
static_cast<unsigned long>(jpegBytes.size()));
|
||||||
|
|
||||||
|
if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
|
||||||
|
jpeg_destroy_decompress(&cinfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cinfo.out_color_space = JCS_RGB;
|
||||||
|
jpeg_start_decompress(&cinfo);
|
||||||
|
|
||||||
|
if (static_cast<int>(cinfo.output_width) != width
|
||||||
|
|| static_cast<int>(cinfo.output_height) != height) {
|
||||||
|
jpeg_finish_decompress(&cinfo);
|
||||||
|
jpeg_destroy_decompress(&cinfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image(width, height, QImage::Format_RGB32);
|
||||||
|
const int rowStride = static_cast<int>(cinfo.output_width) * cinfo.output_components;
|
||||||
|
QVector<unsigned char> rowBuffer(rowStride);
|
||||||
|
unsigned char* rowPointer[1];
|
||||||
|
while (cinfo.output_scanline < cinfo.output_height) {
|
||||||
|
rowPointer[0] = rowBuffer.data();
|
||||||
|
jpeg_read_scanlines(&cinfo, rowPointer, 1);
|
||||||
|
const int y = static_cast<int>(cinfo.output_scanline) - 1;
|
||||||
|
auto* dst = reinterpret_cast<QRgb*>(image.scanLine(y));
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const unsigned char* p = rowBuffer.constData() + (x * 3);
|
||||||
|
dst[x] = qRgb(p[0], p[1], p[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jpeg_finish_decompress(&cinfo);
|
||||||
|
jpeg_destroy_decompress(&cinfo);
|
||||||
|
*outImage = image;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VncSessionBackend::VncSessionBackend(const Profile& profile, QObject* parent)
|
VncSessionBackend::VncSessionBackend(const Profile& profile, QObject* parent)
|
||||||
@@ -106,9 +178,18 @@ VncSessionBackend::VncSessionBackend(const Profile& profile, QObject* parent)
|
|||||||
m_hextileSubrectsRemaining(0),
|
m_hextileSubrectsRemaining(0),
|
||||||
m_hextileSubrectsColoured(false),
|
m_hextileSubrectsColoured(false),
|
||||||
m_zrleInflateStream(new z_stream_s()),
|
m_zrleInflateStream(new z_stream_s()),
|
||||||
m_zrleInflateInitialized(false)
|
m_zrleInflateInitialized(false),
|
||||||
|
m_tightInflateStreams{ new z_stream_s(), new z_stream_s(), new z_stream_s(),
|
||||||
|
new z_stream_s() },
|
||||||
|
m_tightInflateInitialized{ false, false, false, false },
|
||||||
|
m_tightCompressionMode(0),
|
||||||
|
m_tightFilterId(0),
|
||||||
|
m_tightLengthByteIndex(0)
|
||||||
{
|
{
|
||||||
std::memset(m_zrleInflateStream, 0, sizeof(z_stream_s));
|
std::memset(m_zrleInflateStream, 0, sizeof(z_stream_s));
|
||||||
|
for (z_stream_s* stream : m_tightInflateStreams) {
|
||||||
|
std::memset(stream, 0, sizeof(z_stream_s));
|
||||||
|
}
|
||||||
connect(m_socket, &QTcpSocket::connected, this, &VncSessionBackend::onSocketConnected);
|
connect(m_socket, &QTcpSocket::connected, this, &VncSessionBackend::onSocketConnected);
|
||||||
connect(m_socket, &QTcpSocket::readyRead, this, &VncSessionBackend::onSocketReadyRead);
|
connect(m_socket, &QTcpSocket::readyRead, this, &VncSessionBackend::onSocketReadyRead);
|
||||||
connect(m_socket, &QTcpSocket::disconnected, this, &VncSessionBackend::onSocketDisconnected);
|
connect(m_socket, &QTcpSocket::disconnected, this, &VncSessionBackend::onSocketDisconnected);
|
||||||
@@ -124,6 +205,12 @@ VncSessionBackend::~VncSessionBackend()
|
|||||||
inflateEnd(m_zrleInflateStream);
|
inflateEnd(m_zrleInflateStream);
|
||||||
}
|
}
|
||||||
delete m_zrleInflateStream;
|
delete m_zrleInflateStream;
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
if (m_tightInflateInitialized[i]) {
|
||||||
|
inflateEnd(m_tightInflateStreams[i]);
|
||||||
|
}
|
||||||
|
delete m_tightInflateStreams[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VncSessionBackend::connectSession(const SessionConnectOptions& options)
|
void VncSessionBackend::connectSession(const SessionConnectOptions& options)
|
||||||
@@ -361,6 +448,11 @@ void VncSessionBackend::onSocketDisconnected()
|
|||||||
&& m_rfbState != RfbState::WaitingHextileRawTileData
|
&& m_rfbState != RfbState::WaitingHextileRawTileData
|
||||||
&& m_rfbState != RfbState::WaitingZrleCompressedLength
|
&& m_rfbState != RfbState::WaitingZrleCompressedLength
|
||||||
&& m_rfbState != RfbState::WaitingZrleCompressedData
|
&& m_rfbState != RfbState::WaitingZrleCompressedData
|
||||||
|
&& m_rfbState != RfbState::WaitingTightCompressionControl
|
||||||
|
&& m_rfbState != RfbState::WaitingTightFillColor
|
||||||
|
&& m_rfbState != RfbState::WaitingTightFilterId
|
||||||
|
&& m_rfbState != RfbState::WaitingTightLengthByte
|
||||||
|
&& m_rfbState != RfbState::WaitingTightPayload
|
||||||
&& m_rfbState != RfbState::WaitingSetColourMapHeader
|
&& m_rfbState != RfbState::WaitingSetColourMapHeader
|
||||||
&& m_rfbState != RfbState::WaitingSetColourMapData
|
&& m_rfbState != RfbState::WaitingSetColourMapData
|
||||||
&& m_rfbState != RfbState::WaitingServerCutTextHeader
|
&& m_rfbState != RfbState::WaitingServerCutTextHeader
|
||||||
@@ -419,6 +511,12 @@ void VncSessionBackend::resetProtocolState()
|
|||||||
inflateEnd(m_zrleInflateStream);
|
inflateEnd(m_zrleInflateStream);
|
||||||
m_zrleInflateInitialized = false;
|
m_zrleInflateInitialized = false;
|
||||||
}
|
}
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
if (m_tightInflateInitialized[i]) {
|
||||||
|
inflateEnd(m_tightInflateStreams[i]);
|
||||||
|
m_tightInflateInitialized[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VncSessionBackend::haveBytes(int count) const
|
bool VncSessionBackend::haveBytes(int count) const
|
||||||
@@ -588,6 +686,38 @@ void VncSessionBackend::advanceHextileTile()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VncSessionBackend::inflateTightStream(int streamIndex, const QByteArray& compressed,
|
||||||
|
QByteArray* decompressed)
|
||||||
|
{
|
||||||
|
z_stream_s* stream = m_tightInflateStreams[streamIndex];
|
||||||
|
if (!m_tightInflateInitialized[streamIndex]) {
|
||||||
|
std::memset(stream, 0, sizeof(z_stream_s));
|
||||||
|
if (inflateInit(stream) != Z_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_tightInflateInitialized[streamIndex] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int kChunkSize = 65536;
|
||||||
|
char outBuffer[kChunkSize];
|
||||||
|
stream->next_in = reinterpret_cast<Bytef*>(const_cast<char*>(compressed.constData()));
|
||||||
|
stream->avail_in = static_cast<uInt>(compressed.size());
|
||||||
|
int result = Z_OK;
|
||||||
|
while (stream->avail_in > 0) {
|
||||||
|
stream->next_out = reinterpret_cast<Bytef*>(outBuffer);
|
||||||
|
stream->avail_out = kChunkSize;
|
||||||
|
result = inflate(stream, Z_NO_FLUSH);
|
||||||
|
const int produced = kChunkSize - static_cast<int>(stream->avail_out);
|
||||||
|
if (produced > 0) {
|
||||||
|
decompressed->append(outBuffer, produced);
|
||||||
|
}
|
||||||
|
if (result != Z_OK) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result == Z_OK || result == Z_STREAM_END;
|
||||||
|
}
|
||||||
|
|
||||||
void VncSessionBackend::processReceiveBuffer()
|
void VncSessionBackend::processReceiveBuffer()
|
||||||
{
|
{
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -906,6 +1036,9 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
case kEncZRLE:
|
case kEncZRLE:
|
||||||
m_rfbState = RfbState::WaitingZrleCompressedLength;
|
m_rfbState = RfbState::WaitingZrleCompressedLength;
|
||||||
break;
|
break;
|
||||||
|
case kEncTight:
|
||||||
|
m_rfbState = RfbState::WaitingTightCompressionControl;
|
||||||
|
break;
|
||||||
default: {
|
default: {
|
||||||
// SetEncodings (see kAnnouncedEncodings) is entirely
|
// SetEncodings (see kAnnouncedEncodings) is entirely
|
||||||
// client-controlled, so a spec-compliant server will never
|
// client-controlled, so a spec-compliant server will never
|
||||||
@@ -1231,6 +1364,191 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingTightCompressionControl: {
|
||||||
|
if (!haveBytes(1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const quint8 controlByte = static_cast<quint8>(m_recvBuffer.at(0));
|
||||||
|
m_recvBuffer.remove(0, 1);
|
||||||
|
|
||||||
|
// Low 4 bits: reset flags for the 4 persistent Basic-mode zlib
|
||||||
|
// streams, independent of whatever mode this particular
|
||||||
|
// rectangle itself uses.
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
if ((controlByte & (1 << i)) != 0 && m_tightInflateInitialized[i]) {
|
||||||
|
inflateEnd(m_tightInflateStreams[i]);
|
||||||
|
m_tightInflateInitialized[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const quint8 mode = controlByte >> 4;
|
||||||
|
m_tightCompressionMode = mode;
|
||||||
|
if (mode == kTightModeFill) {
|
||||||
|
m_rfbState = RfbState::WaitingTightFillColor;
|
||||||
|
} else if (mode == kTightModeJpeg) {
|
||||||
|
m_tightLengthByteIndex = 0;
|
||||||
|
m_pendingLength = 0;
|
||||||
|
m_rfbState = RfbState::WaitingTightLengthByte;
|
||||||
|
} else if (mode <= kTightBasicModeMax) {
|
||||||
|
if ((mode & kTightBasicExplicitFilterFlag) != 0) {
|
||||||
|
m_rfbState = RfbState::WaitingTightFilterId;
|
||||||
|
} else {
|
||||||
|
m_tightFilterId = 0; // Copy implied
|
||||||
|
m_tightLengthByteIndex = 0;
|
||||||
|
m_pendingLength = 0;
|
||||||
|
m_rfbState = RfbState::WaitingTightLengthByte;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failConnection(
|
||||||
|
QStringLiteral("The VNC server sent an invalid Tight compression-control byte."),
|
||||||
|
QStringLiteral("Control byte 0x%1").arg(controlByte, 2, 16, QChar('0')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingTightFillColor: {
|
||||||
|
if (!haveBytes(3)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QRgb color = VncPixelCodecs::rgbFromPixelBytes(
|
||||||
|
reinterpret_cast<const uchar*>(m_recvBuffer.constData()));
|
||||||
|
m_recvBuffer.remove(0, 3);
|
||||||
|
|
||||||
|
if (m_currentRectangle.width > 0 && m_currentRectangle.height > 0) {
|
||||||
|
QPainter painter(&m_framebuffer);
|
||||||
|
painter.fillRect(QRect(m_currentRectangle.x, m_currentRectangle.y,
|
||||||
|
m_currentRectangle.width, m_currentRectangle.height),
|
||||||
|
QColor::fromRgb(color));
|
||||||
|
}
|
||||||
|
onRectangleFinished();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingTightFilterId: {
|
||||||
|
if (!haveBytes(1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_tightFilterId = static_cast<quint8>(m_recvBuffer.at(0));
|
||||||
|
m_recvBuffer.remove(0, 1);
|
||||||
|
if (m_tightFilterId > 2) {
|
||||||
|
failConnection(QStringLiteral("The VNC server sent an unrecognized Tight filter id."),
|
||||||
|
QStringLiteral("Filter id %1").arg(m_tightFilterId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_tightLengthByteIndex = 0;
|
||||||
|
m_pendingLength = 0;
|
||||||
|
m_rfbState = RfbState::WaitingTightLengthByte;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingTightLengthByte: {
|
||||||
|
if (!haveBytes(1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const quint8 b = static_cast<quint8>(m_recvBuffer.at(0));
|
||||||
|
m_recvBuffer.remove(0, 1);
|
||||||
|
|
||||||
|
if (m_tightLengthByteIndex == 0) {
|
||||||
|
m_pendingLength = (b & 0x7F);
|
||||||
|
} else if (m_tightLengthByteIndex == 1) {
|
||||||
|
m_pendingLength |= static_cast<quint32>(b & 0x7F) << 7;
|
||||||
|
} else {
|
||||||
|
m_pendingLength |= static_cast<quint32>(b) << 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact-length encoding: up to 3 bytes, continuation-bit
|
||||||
|
// style (only the first two bytes' high bit can request
|
||||||
|
// another byte; the third byte's full 8 bits are always used
|
||||||
|
// as the final contribution, capping the value at ~4MB).
|
||||||
|
const bool continuation = (m_tightLengthByteIndex < 2) && ((b & 0x80) != 0);
|
||||||
|
++m_tightLengthByteIndex;
|
||||||
|
if (continuation) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_rfbState = RfbState::WaitingTightPayload;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingTightPayload: {
|
||||||
|
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray payload = m_recvBuffer.left(static_cast<int>(m_pendingLength));
|
||||||
|
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
||||||
|
|
||||||
|
const int rectWidth = m_currentRectangle.width;
|
||||||
|
const int rectHeight = m_currentRectangle.height;
|
||||||
|
if (rectWidth <= 0 || rectHeight <= 0) {
|
||||||
|
onRectangleFinished();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_tightCompressionMode == kTightModeJpeg) {
|
||||||
|
QImage jpegImage;
|
||||||
|
if (!decodeJpegRectangle(payload, rectWidth, rectHeight, &jpegImage)) {
|
||||||
|
failConnection(QStringLiteral("The VNC server sent malformed Tight JPEG data."),
|
||||||
|
QStringLiteral("JPEG payload length %1").arg(payload.size()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QPainter painter(&m_framebuffer);
|
||||||
|
painter.drawImage(m_currentRectangle.x, m_currentRectangle.y, jpegImage);
|
||||||
|
}
|
||||||
|
onRectangleFinished();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic compression: always zlib-compressed by this decoder
|
||||||
|
// (the real protocol allows very small payloads to skip
|
||||||
|
// compression entirely; not handled here -- see class comment).
|
||||||
|
const int streamIndex = m_tightCompressionMode & kTightBasicStreamIndexMask;
|
||||||
|
QByteArray filtered;
|
||||||
|
if (!inflateTightStream(streamIndex, payload, &filtered)) {
|
||||||
|
failConnection(
|
||||||
|
QStringLiteral("The VNC server sent malformed Tight-compressed data."),
|
||||||
|
QStringLiteral("Stream %1, payload length %2").arg(streamIndex).arg(payload.size()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QRgb> pixels;
|
||||||
|
switch (m_tightFilterId) {
|
||||||
|
case 0:
|
||||||
|
pixels = VncPixelCodecs::decodeTightCopyFilter(filtered, rectWidth, rectHeight);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
pixels = VncPixelCodecs::decodeTightPaletteFilter(filtered, rectWidth, rectHeight);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
pixels = VncPixelCodecs::decodeTightGradientFilter(filtered, rectWidth, rectHeight);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (pixels.size() != rectWidth * rectHeight) {
|
||||||
|
failConnection(
|
||||||
|
QStringLiteral("The VNC server sent a malformed Tight rectangle."),
|
||||||
|
QStringLiteral("Filter %1 produced %2 pixels, expected %3")
|
||||||
|
.arg(m_tightFilterId)
|
||||||
|
.arg(pixels.size())
|
||||||
|
.arg(rectWidth * rectHeight));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QImage rectImage(rectWidth, rectHeight, QImage::Format_RGB32);
|
||||||
|
for (int y = 0; y < rectHeight; ++y) {
|
||||||
|
for (int x = 0; x < rectWidth; ++x) {
|
||||||
|
rectImage.setPixel(x, y, pixels.at((y * rectWidth) + x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QPainter painter(&m_framebuffer);
|
||||||
|
painter.drawImage(m_currentRectangle.x, m_currentRectangle.y, rectImage);
|
||||||
|
}
|
||||||
|
onRectangleFinished();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case RfbState::WaitingSetColourMapHeader: {
|
case RfbState::WaitingSetColourMapHeader: {
|
||||||
if (!haveBytes(5)) {
|
if (!haveBytes(5)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QRgb>
|
#include <QRgb>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
class QTcpSocket;
|
class QTcpSocket;
|
||||||
struct z_stream_s;
|
struct z_stream_s;
|
||||||
|
|
||||||
@@ -24,9 +26,19 @@ struct z_stream_s;
|
|||||||
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
||||||
// Authentication (security type 2) and no-auth (type 1) only -- not
|
// Authentication (security type 2) and no-auth (type 1) only -- not
|
||||||
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect + Hextile + ZRLE
|
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect + Hextile + ZRLE
|
||||||
// encodings (Tight compression not yet implemented). No dynamic resize.
|
// + Tight encodings. No dynamic resize. Clipboard sync (Latin-1 only, per
|
||||||
// Clipboard sync (Latin-1 only, per RFB's ServerCutText/ClientCutText) and
|
// RFB's ServerCutText/ClientCutText) and remote cursor shape sync (the
|
||||||
// remote cursor shape sync (the Cursor pseudo-encoding) are supported.
|
// Cursor pseudo-encoding) are supported.
|
||||||
|
//
|
||||||
|
// Tight decoding gap: the real protocol allows the server to skip zlib
|
||||||
|
// compression entirely for very small Basic-mode payloads; this decoder
|
||||||
|
// always attempts to zlib-inflate them, so a server that takes that
|
||||||
|
// shortcut on a given rectangle would have that one rectangle fail rather
|
||||||
|
// than decode. This is intentionally not special-cased (the exact trigger
|
||||||
|
// condition/wire signaling for it could not be verified with confidence
|
||||||
|
// against the RFC text alone, and it only affects rare, tiny rectangles --
|
||||||
|
// solid or near-solid tiny areas are virtually always sent as Fill instead
|
||||||
|
// in practice) -- see docs/PROGRESS.md.
|
||||||
class VncSessionBackend : public SessionBackend
|
class VncSessionBackend : public SessionBackend
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -92,6 +104,11 @@ private:
|
|||||||
WaitingHextileRawTileData,
|
WaitingHextileRawTileData,
|
||||||
WaitingZrleCompressedLength,
|
WaitingZrleCompressedLength,
|
||||||
WaitingZrleCompressedData,
|
WaitingZrleCompressedData,
|
||||||
|
WaitingTightCompressionControl,
|
||||||
|
WaitingTightFillColor,
|
||||||
|
WaitingTightFilterId,
|
||||||
|
WaitingTightLengthByte,
|
||||||
|
WaitingTightPayload,
|
||||||
WaitingSetColourMapHeader,
|
WaitingSetColourMapHeader,
|
||||||
WaitingSetColourMapData,
|
WaitingSetColourMapData,
|
||||||
WaitingServerCutTextHeader,
|
WaitingServerCutTextHeader,
|
||||||
@@ -150,6 +167,17 @@ private:
|
|||||||
z_stream_s* m_zrleInflateStream;
|
z_stream_s* m_zrleInflateStream;
|
||||||
bool m_zrleInflateInitialized;
|
bool m_zrleInflateInitialized;
|
||||||
|
|
||||||
|
// Tight decode state (RFC 6143 SS7.7.4). Unlike ZRLE, Tight's "Basic"
|
||||||
|
// compression mode has 4 independent persistent zlib streams (chosen
|
||||||
|
// per-rectangle by 2 bits of the compression-control byte), each with
|
||||||
|
// its own lifecycle -- reset individually via the control byte's low 4
|
||||||
|
// bits, otherwise persisting like ZRLE's single stream.
|
||||||
|
std::array<z_stream_s*, 4> m_tightInflateStreams;
|
||||||
|
std::array<bool, 4> m_tightInflateInitialized;
|
||||||
|
quint8 m_tightCompressionMode; // compression-control byte >> 4
|
||||||
|
quint8 m_tightFilterId;
|
||||||
|
int m_tightLengthByteIndex;
|
||||||
|
|
||||||
void setState(SessionState state, const QString& message);
|
void setState(SessionState state, const QString& message);
|
||||||
void resetProtocolState();
|
void resetProtocolState();
|
||||||
void processReceiveBuffer();
|
void processReceiveBuffer();
|
||||||
@@ -166,6 +194,7 @@ private:
|
|||||||
void sendClientCutText(const QString& text);
|
void sendClientCutText(const QString& text);
|
||||||
QRect currentHextileTileRect() const;
|
QRect currentHextileTileRect() const;
|
||||||
void advanceHextileTile();
|
void advanceHextileTile();
|
||||||
|
bool inflateTightStream(int streamIndex, const QByteArray& compressed, QByteArray* decompressed);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ add_executable(test_vnc_session_backend
|
|||||||
)
|
)
|
||||||
target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||||
target_link_libraries(test_vnc_session_backend PRIVATE
|
target_link_libraries(test_vnc_session_backend PRIVATE
|
||||||
Qt6::Core Qt6::Gui Qt6::Network Qt6::Test OpenSSL::Crypto ZLIB::ZLIB
|
Qt6::Core Qt6::Gui Qt6::Network Qt6::Test OpenSSL::Crypto ZLIB::ZLIB JPEG::JPEG
|
||||||
)
|
)
|
||||||
add_test(NAME test_vnc_session_backend COMMAND test_vnc_session_backend)
|
add_test(NAME test_vnc_session_backend COMMAND test_vnc_session_backend)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
|
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <jpeglib.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Independently documented bit-reversal example for VNC Authentication's
|
// Independently documented bit-reversal example for VNC Authentication's
|
||||||
// DES key prep (password "COW"): 'C'=0x43, 'O'=0x4F, 'W'=0x57, each
|
// DES key prep (password "COW"): 'C'=0x43, 'O'=0x4F, 'W'=0x57, each
|
||||||
@@ -209,6 +215,88 @@ QByteArray zlibCompressChunk(z_stream& stream, const QByteArray& input, bool fin
|
|||||||
output.resize(totalOut);
|
output.resize(totalOut);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirrors VncSessionBackend's compact-length decode in reverse, for
|
||||||
|
// building test fixtures.
|
||||||
|
void appendTightCompactLength(QByteArray& buf, quint32 length)
|
||||||
|
{
|
||||||
|
buf.append(static_cast<char>((length & 0x7F) | ((length > 0x7F) ? 0x80 : 0)));
|
||||||
|
if (length <= 0x7F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
length >>= 7;
|
||||||
|
buf.append(static_cast<char>((length & 0x7F) | ((length > 0x7F) ? 0x80 : 0)));
|
||||||
|
if (length <= 0x7F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
length >>= 7;
|
||||||
|
buf.append(static_cast<char>(length & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A FramebufferUpdate message with exactly one Tight-encoded rectangle
|
||||||
|
// (RFC 6143 encoding type 7), given the already fully-formed byte stream
|
||||||
|
// to follow the rectangle header (starting with the compression-control
|
||||||
|
// byte).
|
||||||
|
QByteArray tightFramebufferUpdate(int x, int y, int width, int height, const QByteArray& tightData)
|
||||||
|
{
|
||||||
|
QByteArray update;
|
||||||
|
update.append(char(0)); update.append(char(0));
|
||||||
|
update.append(char(0)); update.append(char(1)); // 1 rectangle
|
||||||
|
appendU16(update, static_cast<quint16>(x));
|
||||||
|
appendU16(update, static_cast<quint16>(y));
|
||||||
|
appendU16(update, static_cast<quint16>(width));
|
||||||
|
appendU16(update, static_cast<quint16>(height));
|
||||||
|
update.append(char(0)); update.append(char(0)); update.append(char(0));
|
||||||
|
update.append(char(7)); // encoding = Tight
|
||||||
|
update.append(tightData);
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encodes a solid-colored width x height baseline JPEG using libjpeg-turbo
|
||||||
|
// directly (mirroring the decode side's use of the same library), for
|
||||||
|
// exercising Tight's JPEG sub-mode against real, valid JPEG bytes.
|
||||||
|
QByteArray encodeJpegForTest(int width, int height, QRgb color)
|
||||||
|
{
|
||||||
|
jpeg_compress_struct cinfo;
|
||||||
|
jpeg_error_mgr jerr;
|
||||||
|
cinfo.err = jpeg_std_error(&jerr);
|
||||||
|
jpeg_create_compress(&cinfo);
|
||||||
|
|
||||||
|
unsigned char* buffer = nullptr;
|
||||||
|
unsigned long bufferSize = 0;
|
||||||
|
jpeg_mem_dest(&cinfo, &buffer, &bufferSize);
|
||||||
|
|
||||||
|
cinfo.image_width = static_cast<JDIMENSION>(width);
|
||||||
|
cinfo.image_height = static_cast<JDIMENSION>(height);
|
||||||
|
cinfo.input_components = 3;
|
||||||
|
cinfo.in_color_space = JCS_RGB;
|
||||||
|
jpeg_set_defaults(&cinfo);
|
||||||
|
jpeg_set_quality(&cinfo, 100, TRUE);
|
||||||
|
// Disable chroma subsampling so a solid color round-trips as exactly
|
||||||
|
// as JPEG's DCT quantization allows, for a tighter test tolerance.
|
||||||
|
for (int i = 0; i < cinfo.num_components; ++i) {
|
||||||
|
cinfo.comp_info[i].h_samp_factor = 1;
|
||||||
|
cinfo.comp_info[i].v_samp_factor = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
jpeg_start_compress(&cinfo, TRUE);
|
||||||
|
QVector<unsigned char> row(width * 3);
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
row[(x * 3) + 0] = static_cast<unsigned char>(qRed(color));
|
||||||
|
row[(x * 3) + 1] = static_cast<unsigned char>(qGreen(color));
|
||||||
|
row[(x * 3) + 2] = static_cast<unsigned char>(qBlue(color));
|
||||||
|
}
|
||||||
|
JSAMPROW rowPointer[1] = { row.data() };
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
jpeg_write_scanlines(&cinfo, rowPointer, 1);
|
||||||
|
}
|
||||||
|
jpeg_finish_compress(&cinfo);
|
||||||
|
|
||||||
|
const QByteArray result(reinterpret_cast<const char*>(buffer), static_cast<int>(bufferSize));
|
||||||
|
jpeg_destroy_compress(&cinfo);
|
||||||
|
std::free(buffer);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestVncSessionBackend : public QObject
|
class TestVncSessionBackend : public QObject
|
||||||
@@ -255,6 +343,11 @@ private slots:
|
|||||||
void zrlePlainRleTileProducesExpectedPixels();
|
void zrlePlainRleTileProducesExpectedPixels();
|
||||||
void zrlePaletteRleTileProducesExpectedPixels();
|
void zrlePaletteRleTileProducesExpectedPixels();
|
||||||
void zrleStreamPersistsAcrossTwoFramebufferUpdates();
|
void zrleStreamPersistsAcrossTwoFramebufferUpdates();
|
||||||
|
void tightFillProducesExpectedPixels();
|
||||||
|
void tightBasicCopyFilterProducesExpectedPixels();
|
||||||
|
void tightBasicPaletteFilterProducesExpectedPixels();
|
||||||
|
void tightJpegProducesExpectedPixels();
|
||||||
|
void tightStreamResetFlagAllowsIndependentDecoding();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<FakeVncServer> m_server;
|
std::unique_ptr<FakeVncServer> m_server;
|
||||||
@@ -1506,5 +1599,268 @@ void TestVncSessionBackend::zrleStreamPersistsAcrossTwoFramebufferUpdates()
|
|||||||
QCOMPARE(frames.at(1).pixelColor(1, 0), QColor(10, 20, 30));
|
QCOMPARE(frames.at(1).pixelColor(1, 0), QColor(10, 20, 30));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::tightFillProducesExpectedPixels()
|
||||||
|
{
|
||||||
|
QByteArray tight;
|
||||||
|
tight.append(char(0x80)); // mode = Fill, no stream-reset bits
|
||||||
|
tight += cpixelBytes(10, 200, 30);
|
||||||
|
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(3, 3));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 3, 3, tight));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
|
||||||
|
for (int y = 0; y < 3; ++y) {
|
||||||
|
for (int x = 0; x < 3; ++x) {
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(x, y), QColor(10, 200, 30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::tightBasicCopyFilterProducesExpectedPixels()
|
||||||
|
{
|
||||||
|
QByteArray filtered;
|
||||||
|
filtered += cpixelBytes(255, 0, 0);
|
||||||
|
filtered += cpixelBytes(0, 255, 0);
|
||||||
|
filtered += cpixelBytes(0, 0, 255);
|
||||||
|
filtered += cpixelBytes(255, 255, 255);
|
||||||
|
const QByteArray compressed = zlibCompressWhole(filtered);
|
||||||
|
|
||||||
|
QByteArray tight;
|
||||||
|
tight.append(char(0x00)); // mode = Basic, stream 0, Copy implied
|
||||||
|
appendTightCompactLength(tight, static_cast<quint32>(compressed.size()));
|
||||||
|
tight += compressed;
|
||||||
|
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(2, 2));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tight));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(2, 2));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 1), QColor(0, 0, 255));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 255, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::tightBasicPaletteFilterProducesExpectedPixels()
|
||||||
|
{
|
||||||
|
// 4 pixels, 2-color palette, 1 bit/pixel, packed MSB-first: indices
|
||||||
|
// [0,1,1,0] -> byte 0b0110_0000 (same packing convention as ZRLE's
|
||||||
|
// packed palette; with only one row here, continuous vs row-padded
|
||||||
|
// bit-packing happen to coincide).
|
||||||
|
QByteArray filtered;
|
||||||
|
filtered.append(char(0x01)); // paletteSize - 1 = 1 -> paletteSize = 2
|
||||||
|
filtered += cpixelBytes(10, 20, 30); // palette[0]
|
||||||
|
filtered += cpixelBytes(200, 210, 220); // palette[1]
|
||||||
|
filtered.append(char(0x60));
|
||||||
|
const QByteArray compressed = zlibCompressWhole(filtered);
|
||||||
|
|
||||||
|
QByteArray tight;
|
||||||
|
tight.append(char(0x40)); // mode = Basic, stream 0, explicit filter follows
|
||||||
|
tight.append(char(1)); // filter id = Palette
|
||||||
|
appendTightCompactLength(tight, static_cast<quint32>(compressed.size()));
|
||||||
|
tight += compressed;
|
||||||
|
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(4, 1));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 4, 1, tight));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(4, 1));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(10, 20, 30));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(200, 210, 220));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(2, 0), QColor(200, 210, 220));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(3, 0), QColor(10, 20, 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::tightJpegProducesExpectedPixels()
|
||||||
|
{
|
||||||
|
// 8x8 (one full JPEG MCU at 1x1 chroma sampling) solid red.
|
||||||
|
const QByteArray jpegBytes = encodeJpegForTest(8, 8, qRgb(220, 20, 20));
|
||||||
|
|
||||||
|
QByteArray tight;
|
||||||
|
tight.append(char(0x90)); // mode = JPEG
|
||||||
|
appendTightCompactLength(tight, static_cast<quint32>(jpegBytes.size()));
|
||||||
|
tight += jpegBytes;
|
||||||
|
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(8, 8));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 8, 8, tight));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(8, 8));
|
||||||
|
// JPEG is lossy even at quality 100 -- assert closeness, not equality.
|
||||||
|
const QColor decoded = m_lastFrame.pixelColor(4, 4);
|
||||||
|
QVERIFY(qAbs(decoded.red() - 220) <= 8);
|
||||||
|
QVERIFY(qAbs(decoded.green() - 20) <= 8);
|
||||||
|
QVERIFY(qAbs(decoded.blue() - 20) <= 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::tightStreamResetFlagAllowsIndependentDecoding()
|
||||||
|
{
|
||||||
|
QByteArray filteredA;
|
||||||
|
filteredA += cpixelBytes(10, 20, 30);
|
||||||
|
filteredA += cpixelBytes(10, 20, 30);
|
||||||
|
filteredA += cpixelBytes(10, 20, 30);
|
||||||
|
filteredA += cpixelBytes(10, 20, 30);
|
||||||
|
QByteArray filteredB;
|
||||||
|
filteredB += cpixelBytes(200, 210, 220);
|
||||||
|
filteredB += cpixelBytes(200, 210, 220);
|
||||||
|
filteredB += cpixelBytes(200, 210, 220);
|
||||||
|
filteredB += cpixelBytes(200, 210, 220);
|
||||||
|
|
||||||
|
// Each independently a complete, self-contained zlib stream (its own
|
||||||
|
// deflateInit + Z_FINISH) -- decoding the second one correctly after
|
||||||
|
// the first, on the same persistent stream slot, requires that
|
||||||
|
// stream-reset bit 0 in rectangle B's control byte actually tore down
|
||||||
|
// and re-initialized stream 0 rather than trying to keep feeding an
|
||||||
|
// already-finished stream more data.
|
||||||
|
const QByteArray compressedA = zlibCompressWhole(filteredA);
|
||||||
|
const QByteArray compressedB = zlibCompressWhole(filteredB);
|
||||||
|
|
||||||
|
QByteArray tightA;
|
||||||
|
tightA.append(char(0x00)); // mode = Basic, stream 0, Copy, no reset
|
||||||
|
appendTightCompactLength(tightA, static_cast<quint32>(compressedA.size()));
|
||||||
|
tightA += compressedA;
|
||||||
|
|
||||||
|
QByteArray tightB;
|
||||||
|
tightB.append(char(0x01)); // mode = Basic, stream 0, Copy, reset stream 0
|
||||||
|
appendTightCompactLength(tightB, static_cast<quint32>(compressedB.size()));
|
||||||
|
tightB += compressedB;
|
||||||
|
|
||||||
|
QVector<QImage> frames;
|
||||||
|
connect(m_backend.get(), &SessionBackend::frameUpdated, this,
|
||||||
|
[&frames](const QImage& frame) { frames.append(frame.copy()); });
|
||||||
|
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tightA, tightB]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(2, 2));
|
||||||
|
break;
|
||||||
|
case 3: // first (non-incremental) FramebufferUpdateRequest
|
||||||
|
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tightA));
|
||||||
|
break;
|
||||||
|
case 4: // next (incremental) FramebufferUpdateRequest
|
||||||
|
m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tightB));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(frames.size() >= 2);
|
||||||
|
QCOMPARE(m_lastState, SessionState::Connected);
|
||||||
|
QCOMPARE(frames.at(0).pixelColor(0, 0), QColor(10, 20, 30));
|
||||||
|
QCOMPARE(frames.at(1).pixelColor(0, 0), QColor(200, 210, 220));
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
||||||
#include "test_vnc_session_backend.moc"
|
#include "test_vnc_session_backend.moc"
|
||||||
|
|||||||
Reference in New Issue
Block a user