Files
orbithub/tests/test_vnc_session_backend.cpp
T
ksmithandClaude Sonnet 5 e49fa0cf26 Add VNC Hextile decoding
Implements RFC 6143 SS7.7.4: rectangles announced as Hextile (type 5)
tile the update into 16x16 blocks, each either raw pixels or a
background fill plus an optional list of foreground/individually-
colored subrects, with background/foreground persisting across tiles
within one rectangle when not re-specified.

The pure byte-decode logic (tile metadata, subrect list) lives in new
src/vnc_pixel_codecs.h/.cpp, kept separate from
VncSessionBackend's wire-sequencing state machine so it's unit-testable
without a socket -- the pattern the plan calls for continuing into the
ZRLE/Tight work still ahead. Adds 5 fake-server tests covering a raw
tile, a background-only solid fill, uncoloured and individually-colored
subrects, and a 4-tile rectangle proving background persistence and
correct tile-cursor wraparound.

Verified against the live TightVNC test server (connect, frame,
cursor, clipboard all still work); that particular server always
chose Raw for the actual framebuffer content during this session, so
Hextile's real-world path isn't independently confirmed live -- the
unit tests are the primary correctness evidence for this phase.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 20:49:08 -06:00

1129 lines
45 KiB
C++

#include "vnc_session_backend.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <QTest>
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;
}
}
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();
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));
}
QTEST_GUILESS_MAIN(TestVncSessionBackend)
#include "test_vnc_session_backend.moc"