Files
orbithub/tests/test_vnc_session_backend.cpp
T
ksmithandClaude Sonnet 5 4fca8fce41 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>
2026-09-15 21:13:17 -06:00

1867 lines
73 KiB
C++

#include "vnc_session_backend.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <QTest>
#include <zlib.h>
extern "C" {
#include <jpeglib.h>
}
#include <cstdlib>
namespace {
// Independently documented bit-reversal example for VNC Authentication's
// DES key prep (password "COW"): 'C'=0x43, 'O'=0x4F, 'W'=0x57, each
// reversed bit-by-bit -> 0xC2, 0xF2, 0xEA, padded with zeros to 8 bytes.
// Cross-checked against multiple independent VNC client implementations'
// documented behavior, not just re-derived from this code's own logic.
QByteArray expectedCowKey()
{
return QByteArray::fromHex("c2f2ea0000000000");
}
// A tiny scripted RFB "server" for state-machine tests: accepts exactly one
// connection on 127.0.0.1 and lets the test drive the byte sequence it
// sends, while capturing whatever the real VncSessionBackend under test
// writes back.
class FakeVncServer : public QObject
{
Q_OBJECT
public:
FakeVncServer()
{
server.listen(QHostAddress::LocalHost);
connect(&server, &QTcpServer::newConnection, this, [this]() {
connection = server.nextPendingConnection();
connect(connection, &QTcpSocket::readyRead, this, [this]() {
received.append(connection->readAll());
emit dataReceived();
});
emit clientConnected();
});
}
quint16 port() const { return server.serverPort(); }
void sendWhenConnected(const QByteArray& bytes)
{
if (connection != nullptr) {
connection->write(bytes);
}
}
// Scripted steps are matched by ordinal position, not by pattern-
// matching received byte content: several distinct RFB messages (e.g.
// the 1-byte security-type selection and the 1-byte ClientInit
// shared-flag) are indistinguishable by content alone, so content
// matching is genuinely ambiguous here.
int nextStep() { return step++; }
QTcpServer server;
QTcpSocket* connection = nullptr;
QByteArray received;
int step = 0;
signals:
void clientConnected();
void dataReceived();
};
Profile makeVncProfile(quint16 port)
{
Profile profile;
profile.name = QStringLiteral("Test VNC");
profile.host = QStringLiteral("127.0.0.1");
profile.port = port;
profile.protocol = QStringLiteral("VNC");
return profile;
}
SessionConnectOptions makeOptions(const QString& password = QString())
{
SessionConnectOptions options;
options.password = password;
return options;
}
void appendU16(QByteArray& buf, quint16 value)
{
buf.append(static_cast<char>((value >> 8) & 0xFF));
buf.append(static_cast<char>(value & 0xFF));
}
// Pixel bytes as expected under our negotiated SetPixelFormat: 32bpp
// little-endian, byte order B,G,R,pad.
QByteArray pixelBytes(int r, int g, int b)
{
QByteArray bytes;
bytes.append(static_cast<char>(b));
bytes.append(static_cast<char>(g));
bytes.append(static_cast<char>(r));
bytes.append(char(0));
return bytes;
}
QByteArray serverInitBytes(int width, int height)
{
QByteArray serverInit;
appendU16(serverInit, static_cast<quint16>(width));
appendU16(serverInit, static_cast<quint16>(height));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
return serverInit;
}
// A FramebufferUpdate message with exactly one Hextile-encoded rectangle
// (RFC 6143 encoding type 5), given the already fully-formed tile byte
// stream to follow the rectangle header.
QByteArray hextileFramebufferUpdate(int x, int y, int width, int height, const QByteArray& tileData)
{
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(5)); // encoding = Hextile
update.append(tileData);
return update;
}
void appendU32(QByteArray& buf, quint32 value)
{
buf.append(static_cast<char>((value >> 24) & 0xFF));
buf.append(static_cast<char>((value >> 16) & 0xFF));
buf.append(static_cast<char>((value >> 8) & 0xFF));
buf.append(static_cast<char>(value & 0xFF));
}
// ZRLE's CPIXEL: 3 bytes, B,G,R order (the pad byte a full 32bpp pixel
// would have is simply omitted) -- matches VncPixelCodecs::rgbFromPixelBytes.
QByteArray cpixelBytes(int r, int g, int b)
{
QByteArray bytes;
bytes.append(static_cast<char>(b));
bytes.append(static_cast<char>(g));
bytes.append(static_cast<char>(r));
return bytes;
}
// A FramebufferUpdate message with exactly one ZRLE-encoded rectangle
// (RFC 6143 encoding type 16): a 4-byte compressed length followed by that
// many already-zlib-compressed bytes.
QByteArray zrleFramebufferUpdate(int x, int y, int width, int height, const QByteArray& compressed)
{
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(16)); // encoding = ZRLE
appendU32(update, static_cast<quint32>(compressed.size()));
update.append(compressed);
return update;
}
// Compresses `input` as a single, complete, self-contained zlib stream
// (its own header + Z_FINISH) -- correct for tests where each
// VncSessionBackend under test lazily starts its own fresh inflate stream
// on the first ZRLE rectangle it ever sees.
QByteArray zlibCompressWhole(const QByteArray& input)
{
z_stream stream{};
deflateInit(&stream, Z_DEFAULT_COMPRESSION);
QByteArray output(static_cast<int>(deflateBound(&stream, static_cast<uLong>(input.size()))),
char(0));
stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(input.constData()));
stream.avail_in = static_cast<uInt>(input.size());
stream.next_out = reinterpret_cast<Bytef*>(output.data());
stream.avail_out = static_cast<uInt>(output.size());
deflate(&stream, Z_FINISH);
output.resize(output.size() - static_cast<int>(stream.avail_out));
deflateEnd(&stream);
return output;
}
// Compresses `input` as one chunk of an ongoing (not yet finished, unless
// `finish` is true) zlib stream carried in `stream` across calls -- used to
// build two separately-decodable compressed chunks that only both decode
// correctly against a single persistent inflate stream, for testing that
// ZRLE's zlib stream is retained across rectangles/updates rather than
// reset each time.
QByteArray zlibCompressChunk(z_stream& stream, const QByteArray& input, bool finish)
{
QByteArray output(static_cast<int>(deflateBound(&stream, static_cast<uLong>(input.size()))) + 64,
char(0));
stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(input.constData()));
stream.avail_in = static_cast<uInt>(input.size());
const int flushMode = finish ? Z_FINISH : Z_SYNC_FLUSH;
int totalOut = 0;
do {
stream.next_out = reinterpret_cast<Bytef*>(output.data() + totalOut);
stream.avail_out = static_cast<uInt>(output.size() - totalOut);
deflate(&stream, flushMode);
totalOut = output.size() - static_cast<int>(stream.avail_out);
} while (stream.avail_out == 0);
output.resize(totalOut);
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
{
Q_OBJECT
private slots:
// Pure-function coverage.
void desKeyFromPasswordMatchesKnownVector();
void desKeyFromPasswordPadsShortPasswords();
void desKeyFromPasswordTruncatesLongPasswords();
void vncAuthResponseIsSixteenBytesAndDeterministic();
void vncAuthResponseRejectsWrongChallengeSize();
void keysymForQtKeyMapsNamedKeys();
void keysymForQtKeyMapsFunctionKeys();
void keysymForQtKeyPassesThroughPrintableText();
void keysymForQtKeyUsesUnicodeConventionBeyondLatin1();
void keysymForQtKeyReturnsZeroForUnmapped();
void mapSocketErrorCoversCommonCases();
// State-machine coverage against a scripted in-process fake server.
void init();
void cleanup();
void connectsWithNoAuthRfb38();
void connectsWithVncAuthenticationRfb38();
void authFailureRfb38ReachesFailedState();
void unsupportedSecurityTypeReachesFailedState();
void rfb33ServerWithNoAuthConnectsWithoutSecurityResult();
void rawFramebufferUpdateProducesExpectedPixels();
void copyRectEncodingDoesNotFailConnection();
void unannouncedEncodingFailsConnectionWithClearMessage();
void serverCutTextEmitsRemoteClipboardTextChanged();
void setClipboardTextSendsClientCutText();
void cursorPseudoEncodingProducesExpectedImageAndHotspot();
void zeroSizeCursorPseudoEncodingHidesCursor();
void hextileRawTileProducesExpectedPixels();
void hextileBackgroundOnlyTileFillsSolidColor();
void hextileUncolouredSubrectUsesForeground();
void hextileColouredSubrectUsesOwnColor();
void hextileMultiTilePersistsBackgroundAcrossTiles();
void zrleRawTileProducesExpectedPixels();
void zrleSolidTileFillsColor();
void zrlePackedPaletteTileProducesExpectedPixels();
void zrlePlainRleTileProducesExpectedPixels();
void zrlePaletteRleTileProducesExpectedPixels();
void zrleStreamPersistsAcrossTwoFramebufferUpdates();
void tightFillProducesExpectedPixels();
void tightBasicCopyFilterProducesExpectedPixels();
void tightBasicPaletteFilterProducesExpectedPixels();
void tightJpegProducesExpectedPixels();
void tightStreamResetFlagAllowsIndependentDecoding();
private:
std::unique_ptr<FakeVncServer> m_server;
std::unique_ptr<VncSessionBackend> m_backend;
SessionState m_lastState = SessionState::Disconnected;
QString m_lastErrorDisplay;
QString m_lastErrorRaw;
QImage m_lastFrame;
bool m_gotFrame = false;
};
void TestVncSessionBackend::desKeyFromPasswordMatchesKnownVector()
{
QCOMPARE(VncSessionBackend::desKeyFromPassword(QStringLiteral("COW")), expectedCowKey());
}
void TestVncSessionBackend::desKeyFromPasswordPadsShortPasswords()
{
const QByteArray key = VncSessionBackend::desKeyFromPassword(QStringLiteral(""));
QCOMPARE(key, QByteArray(8, char(0)));
}
void TestVncSessionBackend::desKeyFromPasswordTruncatesLongPasswords()
{
// Only the first 8 characters are ever used as the DES key.
const QByteArray key1 = VncSessionBackend::desKeyFromPassword(QStringLiteral("12345678"));
const QByteArray key2 = VncSessionBackend::desKeyFromPassword(QStringLiteral("12345678ignored"));
QCOMPARE(key1, key2);
QCOMPARE(key1.size(), 8);
}
void TestVncSessionBackend::vncAuthResponseIsSixteenBytesAndDeterministic()
{
const QByteArray challenge(16, char(0x42));
const QByteArray response1 = VncSessionBackend::vncAuthResponse(challenge, QStringLiteral("secret"));
const QByteArray response2 = VncSessionBackend::vncAuthResponse(challenge, QStringLiteral("secret"));
QCOMPARE(response1.size(), 16);
QCOMPARE(response1, response2);
const QByteArray differentPassword =
VncSessionBackend::vncAuthResponse(challenge, QStringLiteral("other"));
QVERIFY(response1 != differentPassword);
}
void TestVncSessionBackend::vncAuthResponseRejectsWrongChallengeSize()
{
QVERIFY(VncSessionBackend::vncAuthResponse(QByteArray(15, char(0)), QStringLiteral("x")).isEmpty());
QVERIFY(VncSessionBackend::vncAuthResponse(QByteArray(), QStringLiteral("x")).isEmpty());
}
void TestVncSessionBackend::keysymForQtKeyMapsNamedKeys()
{
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Backspace, QString()), quint32(0xff08));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Tab, QString()), quint32(0xff09));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Return, QString()), quint32(0xff0d));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Escape, QString()), quint32(0xff1b));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Delete, QString()), quint32(0xffff));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Left, QString()), quint32(0xff51));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Up, QString()), quint32(0xff52));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Right, QString()), quint32(0xff53));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Down, QString()), quint32(0xff54));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Shift, QString()), quint32(0xffe1));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Control, QString()), quint32(0xffe3));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Alt, QString()), quint32(0xffe9));
}
void TestVncSessionBackend::keysymForQtKeyMapsFunctionKeys()
{
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_F1, QString()), quint32(0xffbe));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_F12, QString()), quint32(0xffc9));
}
void TestVncSessionBackend::keysymForQtKeyPassesThroughPrintableText()
{
// Printable ASCII/Latin-1 keysyms are just the codepoint itself.
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_A, QStringLiteral("a")), quint32('a'));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_5, QStringLiteral("5")), quint32('5'));
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_Space, QStringLiteral(" ")), quint32(' '));
}
void TestVncSessionBackend::keysymForQtKeyUsesUnicodeConventionBeyondLatin1()
{
// X11's convention for Unicode codepoints beyond Latin-1: keysym =
// 0x01000000 + codepoint. Euro sign U+20AC as an example.
const QString euro = QString::fromUtf8("\xE2\x82\xAC");
QCOMPARE(VncSessionBackend::keysymForQtKey(0, euro), quint32(0x01000000u + 0x20ACu));
}
void TestVncSessionBackend::keysymForQtKeyReturnsZeroForUnmapped()
{
QCOMPARE(VncSessionBackend::keysymForQtKey(Qt::Key_MediaPlay, QString()), quint32(0));
}
void TestVncSessionBackend::mapSocketErrorCoversCommonCases()
{
QCOMPARE(VncSessionBackend::mapSocketError(QAbstractSocket::ConnectionRefusedError, QString()),
QStringLiteral("Connection refused by remote host."));
QCOMPARE(VncSessionBackend::mapSocketError(QAbstractSocket::HostNotFoundError, QString()),
QStringLiteral("Host could not be resolved."));
QCOMPARE(VncSessionBackend::mapSocketError(QAbstractSocket::SocketTimeoutError, QString()),
QStringLiteral("Connection timed out."));
QVERIFY(!VncSessionBackend::mapSocketError(QAbstractSocket::UnknownSocketError,
QStringLiteral("raw detail"))
.isEmpty());
}
void TestVncSessionBackend::init()
{
m_server = std::make_unique<FakeVncServer>();
m_backend =
std::make_unique<VncSessionBackend>(makeVncProfile(m_server->port()), nullptr);
m_lastState = SessionState::Disconnected;
m_lastErrorDisplay.clear();
m_lastErrorRaw.clear();
m_lastFrame = QImage();
m_gotFrame = false;
connect(m_backend.get(), &SessionBackend::stateChanged, this,
[this](SessionState state, const QString&) { m_lastState = state; });
connect(m_backend.get(), &SessionBackend::connectionError, this,
[this](const QString& display, const QString& raw) {
m_lastErrorDisplay = display;
m_lastErrorRaw = raw;
});
connect(m_backend.get(), &SessionBackend::frameUpdated, this, [this](const QImage& frame) {
m_lastFrame = frame;
m_gotFrame = true;
});
}
void TestVncSessionBackend::cleanup()
{
if (m_backend) {
m_backend->disconnectSession();
}
m_backend.reset();
m_server.reset();
}
void TestVncSessionBackend::connectsWithNoAuthRfb38()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: { // client's version reply
QByteArray securityTypes;
securityTypes.append(char(1)); // count = 1
securityTypes.append(char(1)); // type 1 = None
m_server->sendWhenConnected(securityTypes);
break;
}
case 1: // client's security-type selection (byte value 1)
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2: { // ClientInit (shared-flag byte)
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(4)); // width = 4
serverInit.append(char(0)); serverInit.append(char(2)); // height = 2
serverInit.append(QByteArray(16, char(0))); // pixel format (ignored by client)
serverInit.append(QByteArray(4, char(0))); // name length = 0
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Connected);
}
void TestVncSessionBackend::connectsWithVncAuthenticationRfb38()
{
const QString password = QStringLiteral("secret1");
const QByteArray challenge(16, char(0x11));
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, &challenge]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: { // version reply
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(2)); // type 2 = VNC Authentication
m_server->sendWhenConnected(securityTypes);
break;
}
case 1: // security-type selection
m_server->sendWhenConnected(challenge);
break;
case 2: // 16-byte DES response (content itself checked separately below)
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 3: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(2));
serverInit.append(char(0)); serverInit.append(char(2));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions(password));
QTRY_COMPARE(m_lastState, SessionState::Connected);
// Independently verify the response the fake server actually received
// at step 2 was the correct DES-encrypted challenge -- can't capture it
// mid-script above without complicating the dispatch, so just replay
// the expected computation here for comparison purposes is redundant;
// instead this is implicitly proven by reaching Connected at all, since
// a real VncSessionBackend only proceeds past WaitingSecurityResult
// (here, an explicit `OK`) after sending *some* response and the
// server unconditionally accepts it in this script. The actual byte
// correctness of vncAuthResponse() is covered directly by
// vncAuthResponseIsSixteenBytesAndDeterministic() and the "COW" key
// vector above.
}
void TestVncSessionBackend::authFailureRfb38ReachesFailedState()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(2));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(16, char(0x22))); // challenge
break;
case 2: {
QByteArray result;
result.append(char(0)); result.append(char(0)); result.append(char(0));
result.append(char(1)); // SecurityResult: failed
const QByteArray reason = QByteArray("bad password");
result.append(char(0)); result.append(char(0)); result.append(char(0));
result.append(static_cast<char>(reason.size()));
result.append(reason);
m_server->sendWhenConnected(result);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions(QStringLiteral("wrong")));
QTRY_COMPARE(m_lastState, SessionState::Failed);
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("bad password")));
}
void TestVncSessionBackend::unsupportedSecurityTypeReachesFailedState()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
if (m_server->nextStep() == 0) {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(30)); // Apple's scheme -- unsupported here
m_server->sendWhenConnected(securityTypes);
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Failed);
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("doesn't support")));
}
void TestVncSessionBackend::rfb33ServerWithNoAuthConnectsWithoutSecurityResult()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.003\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
switch (m_server->nextStep()) {
case 0: { // version reply
QByteArray securityType(4, char(0));
securityType[3] = char(1); // type 1 = None, sent directly (3.3 style)
m_server->sendWhenConnected(securityType);
break;
}
case 1: { // ClientInit arrives directly -- 3.3 has no SecurityResult at all
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Connected);
}
void TestVncSessionBackend::rawFramebufferUpdateProducesExpectedPixels()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
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: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(2)); // width = 2
serverInit.append(char(0)); serverInit.append(char(1)); // height = 1
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: {
// First FramebufferUpdateRequest -- reply with a single Raw
// rectangle covering the whole 2x1 framebuffer: one red pixel,
// one green pixel (as bytes matching the requested 32bpp
// little-endian R@16/G@8/B@0 format: B,G,R,pad per pixel).
QByteArray update;
update.append(char(0)); // message-type: FramebufferUpdate
update.append(char(0)); // padding
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x = 0
update.append(char(0)); update.append(char(0)); // y = 0
update.append(char(0)); update.append(char(2)); // width = 2
update.append(char(0)); update.append(char(1)); // height = 1
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(0)); // encoding = 0 (Raw)
// pixel 0: red (B=0,G=0,R=255,pad=0)
update.append(char(0)); update.append(char(0)); update.append(char(0xff));
update.append(char(0));
// pixel 1: green (B=0,G=255,R=0,pad=0)
update.append(char(0)); update.append(char(0xff)); update.append(char(0));
update.append(char(0));
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 1));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
}
// Regression guard for the WaitingRectangleHeader dispatch: every encoding
// this backend announces via SetEncodings must have a working decode path.
// Raw is already exercised by rawFramebufferUpdateProducesExpectedPixels();
// this covers CopyRect. Whenever a new encoding is added to
// kAnnouncedEncodings in vnc_session_backend.cpp, a matching test belongs
// here (or nearby) so the announced list and the dispatch switch can never
// silently drift apart.
void TestVncSessionBackend::copyRectEncodingDoesNotFailConnection()
{
int frameCount = 0;
connect(m_backend.get(), &SessionBackend::frameUpdated, this,
[&frameCount](const QImage&) { ++frameCount; });
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
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: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(2)); // width = 2
serverInit.append(char(0)); serverInit.append(char(2)); // height = 2
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: { // initial non-incremental request -> seed the framebuffer with Raw
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x
update.append(char(0)); update.append(char(0)); // y
update.append(char(0)); update.append(char(2)); // width
update.append(char(0)); update.append(char(2)); // height
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(0)); // encoding = Raw
update.append(QByteArray(2 * 2 * 4, char(0x11)));
m_server->sendWhenConnected(update);
break;
}
case 4: { // next incremental request -> a CopyRect rectangle
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x
update.append(char(0)); update.append(char(0)); // y
update.append(char(0)); update.append(char(2)); // width
update.append(char(0)); update.append(char(2)); // height
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(1)); // encoding = CopyRect
update.append(char(0)); update.append(char(0)); // src x = 0
update.append(char(0)); update.append(char(0)); // src y = 0
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(frameCount >= 2);
QCOMPARE(m_lastState, SessionState::Connected);
}
void TestVncSessionBackend::unannouncedEncodingFailsConnectionWithClearMessage()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
m_server->received.clear();
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: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1)); // width = 1
serverInit.append(char(0)); serverInit.append(char(1)); // height = 1
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: { // a rectangle whose encoding was never announced via SetEncodings
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x
update.append(char(0)); update.append(char(0)); // y
update.append(char(0)); update.append(char(1)); // width
update.append(char(0)); update.append(char(1)); // height
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(99)); // encoding = 99, never announced
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Failed);
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("non-compliant")));
}
void TestVncSessionBackend::serverCutTextEmitsRemoteClipboardTextChanged()
{
QString lastClipboardText;
connect(m_backend.get(), &SessionBackend::remoteClipboardTextChanged, this,
[&lastClipboardText](const QString& text) { lastClipboardText = text; });
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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: { // ClientInit -- reply with ServerInit, then a ServerCutText message
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
QByteArray cutText;
cutText.append(char(3)); // message-type: ServerCutText
cutText.append(3, char(0)); // padding
const QByteArray text = QByteArray("hello clipboard");
cutText.append(char(0)); cutText.append(char(0)); cutText.append(char(0));
cutText.append(static_cast<char>(text.size())); // length (BE, fits one byte here)
m_server->sendWhenConnected(serverInit + cutText + text);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(lastClipboardText, QStringLiteral("hello clipboard"));
}
void TestVncSessionBackend::setClipboardTextSendsClientCutText()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_COMPARE(m_lastState, SessionState::Connected);
m_backend->setClipboardText(QStringLiteral("hi"));
// The exact ClientCutText bytes must eventually appear at the tail of
// whatever the fake server received -- checked with endsWith() rather
// than an exact match on the whole buffer, since exactly how the
// preceding handshake/SetEncodings/FramebufferUpdateRequest bytes get
// chunked into TCP reads isn't guaranteed and doesn't matter here.
QByteArray expected;
expected.append(char(6)); // message-type: ClientCutText
expected.append(3, char(0)); // padding
expected.append(char(0)); expected.append(char(0)); expected.append(char(0));
expected.append(char(2)); // length = 2
expected.append("hi");
QTRY_VERIFY(m_server->received.endsWith(expected));
}
void TestVncSessionBackend::cursorPseudoEncodingProducesExpectedImageAndHotspot()
{
QImage lastCursorImage;
QPoint lastHotspot;
bool gotCursor = false;
connect(m_backend.get(), &SessionBackend::cursorImageChanged, this,
[&](const QImage& image, const QPoint& hotspot) {
lastCursorImage = image;
lastHotspot = hotspot;
gotCursor = true;
});
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(4)); // width = 4
serverInit.append(char(0)); serverInit.append(char(4)); // height = 4
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: { // first FramebufferUpdateRequest -> a Cursor pseudo-encoding rectangle
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(1)); // x = hotspot x = 1
update.append(char(0)); update.append(char(1)); // y = hotspot y = 1
update.append(char(0)); update.append(char(2)); // width = 2
update.append(char(0)); update.append(char(1)); // height = 1
update.append(char(0xff)); update.append(char(0xff));
update.append(char(0xff)); update.append(char(0x11)); // encoding = -239 (Cursor)
// pixel 0: red, opaque (byte order B,G,R,pad, per our negotiated format)
update.append(char(0)); update.append(char(0)); update.append(char(0xff));
update.append(char(0));
// pixel 1: blue, will be masked transparent
update.append(char(0xff)); update.append(char(0)); update.append(char(0));
update.append(char(0));
// mask: ceil(2/8)=1 byte/row * height 1 -- bit7=pixel0 opaque, bit6=pixel1 transparent
update.append(char(0x80));
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(gotCursor);
QCOMPARE(lastCursorImage.size(), QSize(2, 1));
QCOMPARE(lastHotspot, QPoint(1, 1));
QCOMPARE(lastCursorImage.pixelColor(0, 0), QColor(255, 0, 0, 255));
QCOMPARE(lastCursorImage.pixelColor(1, 0).alpha(), 0);
}
void TestVncSessionBackend::zeroSizeCursorPseudoEncodingHidesCursor()
{
bool gotHidden = false;
connect(m_backend.get(), &SessionBackend::cursorHidden, this,
[&gotHidden]() { gotHidden = true; });
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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: { // ClientInit
QByteArray serverInit;
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(char(0)); serverInit.append(char(1));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
m_server->sendWhenConnected(serverInit);
break;
}
case 3: { // a 0x0 Cursor rectangle means "hide the cursor"
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
update.append(char(0)); update.append(char(0)); // x (hotspot, unused here)
update.append(char(0)); update.append(char(0)); // y
update.append(char(0)); update.append(char(0)); // width = 0
update.append(char(0)); update.append(char(0)); // height = 0
update.append(char(0xff)); update.append(char(0xff));
update.append(char(0xff)); update.append(char(0x11)); // encoding = -239 (Cursor)
m_server->sendWhenConnected(update);
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(gotHidden);
}
void TestVncSessionBackend::hextileRawTileProducesExpectedPixels()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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: { // a single Raw-subencoding Hextile tile covering the whole 2x2 rectangle
QByteArray tile;
tile.append(char(0x01)); // HextileFlags::kRaw
tile += pixelBytes(255, 0, 0);
tile += pixelBytes(0, 255, 0);
tile += pixelBytes(0, 0, 255);
tile += pixelBytes(255, 255, 255);
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 2, 2, tile));
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::hextileBackgroundOnlyTileFillsSolidColor()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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: { // BackgroundSpecified only, no subrects -- solid fill
QByteArray tile;
tile.append(char(0x02)); // HextileFlags::kBackgroundSpecified
tile += pixelBytes(0, 128, 255);
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 3, 3, tile));
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(0, 128, 255));
}
}
}
void TestVncSessionBackend::hextileUncolouredSubrectUsesForeground()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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, 4));
break;
case 3: {
// Background(black) + Foreground(red) + one uncoloured 2x2
// subrect at tile-local (1,1) -- must render in the foreground
// color, everywhere else in the background color.
QByteArray tile;
tile.append(char(0x02 | 0x04 | 0x08)); // Background|Foreground|AnySubrects
tile += pixelBytes(0, 0, 0); // background
tile += pixelBytes(255, 0, 0); // foreground
tile.append(char(1)); // subrect count
tile.append(char(0x11)); // xy: x=1, y=1
tile.append(char(0x11)); // wh: width=2, height=2
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 4, 4, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(4, 4));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(2, 2), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(3, 3), QColor(0, 0, 0));
}
void TestVncSessionBackend::hextileColouredSubrectUsesOwnColor()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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: {
// No background/foreground specified this tile -- both stay at
// the rectangle-start default (black). One individually-
// coloured 1x1 subrect at tile-local (0,0).
QByteArray tile;
tile.append(char(0x08 | 0x10)); // AnySubrects|SubrectsColoured
tile.append(char(1)); // subrect count
tile += pixelBytes(0, 255, 0); // this subrect's own color
tile.append(char(0x00)); // xy: x=0, y=0
tile.append(char(0x00)); // wh: width=1, height=1
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 3, 3, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 255, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(0, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(2, 2), QColor(0, 0, 0));
}
void TestVncSessionBackend::hextileMultiTilePersistsBackgroundAcrossTiles()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
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(20, 20));
break;
case 3: {
// A 20x20 rectangle tiles as (0,0) 16x16, (16,0) 4x16,
// (0,16) 16x4, (16,16) 4x4 -- only the first tile specifies a
// background color; the other three specify nothing at all
// (subencoding 0x00) and must inherit it.
QByteArray tile1;
tile1.append(char(0x02)); // BackgroundSpecified
tile1 += pixelBytes(0, 0, 255);
QByteArray tile2(1, char(0x00));
QByteArray tile3(1, char(0x00));
QByteArray tile4(1, char(0x00));
m_server->sendWhenConnected(
hextileFramebufferUpdate(0, 0, 20, 20, tile1 + tile2 + tile3 + tile4));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(20, 20));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(19, 0), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(0, 19), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(19, 19), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(10, 10), QColor(0, 0, 255));
}
void TestVncSessionBackend::zrleRawTileProducesExpectedPixels()
{
QByteArray tile;
tile.append(char(0)); // Raw
tile += cpixelBytes(255, 0, 0);
tile += cpixelBytes(0, 255, 0);
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
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, 1));
break;
case 3:
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 1, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 1));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
}
void TestVncSessionBackend::zrleSolidTileFillsColor()
{
QByteArray tile;
tile.append(char(1)); // Solid
tile += cpixelBytes(0, 128, 255);
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
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(zrleFramebufferUpdate(0, 0, 2, 2, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 2));
for (int y = 0; y < 2; ++y) {
for (int x = 0; x < 2; ++x) {
QCOMPARE(m_lastFrame.pixelColor(x, y), QColor(0, 128, 255));
}
}
}
void TestVncSessionBackend::zrlePackedPaletteTileProducesExpectedPixels()
{
// 4 pixels, 2-color palette, 1 bit/pixel, packed MSB-first: indices
// [0,1,1,0] -> byte 0b0110_0000.
QByteArray tile;
tile.append(char(2)); // packed palette, paletteSize = 2
tile += cpixelBytes(10, 20, 30); // palette[0]
tile += cpixelBytes(200, 210, 220); // palette[1]
tile.append(char(0x60));
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
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(zrleFramebufferUpdate(0, 0, 4, 1, compressed));
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::zrlePlainRleTileProducesExpectedPixels()
{
// One run of length 3 (encoded as total=2, single continuation byte
// since 2 != 255) covering the whole 3x1 tile.
QByteArray tile;
tile.append(char(128)); // Plain RLE
tile += cpixelBytes(50, 60, 70);
tile.append(char(2));
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
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, 1));
break;
case 3:
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 3, 1, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 1));
for (int x = 0; x < 3; ++x) {
QCOMPARE(m_lastFrame.pixelColor(x, 0), QColor(50, 60, 70));
}
}
void TestVncSessionBackend::zrlePaletteRleTileProducesExpectedPixels()
{
// palette = [colorA, colorB]. Entries: index 0, run length 1 (index
// byte < 128, no run-length byte follows); then index 1, run length 2
// (index byte 0x81 = 128 + 1, followed by a run-length byte encoding
// total=1 -> actual length 2).
QByteArray tile;
tile.append(char(130)); // Palette RLE, paletteSize = 2
tile += cpixelBytes(1, 2, 3); // palette[0] = colorA
tile += cpixelBytes(4, 5, 6); // palette[1] = colorB
tile.append(char(0x00));
tile.append(char(0x81));
tile.append(char(0x01));
const QByteArray compressed = zlibCompressWhole(tile);
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed]() {
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, 1));
break;
case 3:
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 3, 1, compressed));
break;
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 1));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(1, 2, 3));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(4, 5, 6));
QCOMPARE(m_lastFrame.pixelColor(2, 0), QColor(4, 5, 6));
}
void TestVncSessionBackend::zrleStreamPersistsAcrossTwoFramebufferUpdates()
{
QByteArray tile1;
tile1.append(char(0)); // Raw
tile1 += cpixelBytes(255, 0, 0);
tile1 += cpixelBytes(0, 255, 0);
QByteArray tile2;
tile2.append(char(1)); // Solid
tile2 += cpixelBytes(10, 20, 30);
// One continuous deflate stream split into two chunks -- the second
// chunk is only decodable by an inflate stream that already consumed
// the first, which is exactly what should happen if
// VncSessionBackend's ZRLE stream persists across separate
// FramebufferUpdate messages instead of being reset each time.
z_stream deflateStream{};
deflateInit(&deflateStream, Z_DEFAULT_COMPRESSION);
const QByteArray compressed1 = zlibCompressChunk(deflateStream, tile1, false);
const QByteArray compressed2 = zlibCompressChunk(deflateStream, tile2, true);
deflateEnd(&deflateStream);
// Capture each frame as it arrives, rather than polling m_lastFrame
// between separately-timed QTRY_VERIFY checks -- the two updates can
// both land before the first check ever gets scheduled to run, so only
// recording state exactly when each signal fires is race-free here.
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, compressed1, compressed2]() {
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, 1));
break;
case 3: // first (non-incremental) FramebufferUpdateRequest
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 1, compressed1));
break;
case 4: // next (incremental) FramebufferUpdateRequest
m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 1, compressed2));
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(255, 0, 0));
QCOMPARE(frames.at(0).pixelColor(1, 0), QColor(0, 255, 0));
QCOMPARE(frames.at(1).pixelColor(0, 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)
#include "test_vnc_session_backend.moc"