Internal
Public Access
Implements RFC 6143 SS7.7.6: a ZRLE rectangle is a 4-byte compressed length followed by that many zlib-compressed bytes, decompressing to 64x64 tiles each using one of five subencodings (Raw, Solid, packed palette, Plain RLE, Palette RLE). The zlib stream persists for the whole connection rather than being reset per-rectangle or per-update, so VncSessionBackend now owns a lazily-initialized, persistent z_stream torn down only in resetProtocolState() on a fresh connect/reconnect. Since the entire rectangle's compressed data decompresses into memory in one shot, tile parsing is a plain synchronous loop rather than needing its own RfbState values -- only the compressed-length and compressed-data reads are actual protocol states. Tile decoding (the five subencodings, including the continuation-byte run-length encoding shared by two of them) lives in vnc_pixel_codecs.h/.cpp alongside the Hextile decoder, unit-tested with 6 new tests covering each subencoding plus a persistence test that splits one continuous deflate stream across two separate FramebufferUpdate messages -- it only decodes correctly if the connection's inflate stream is retained between them. Adds a top-level find_package(ZLIB REQUIRED) + ZLIB::ZLIB link (previously only pulled in transitively via vendored FreeRDP's own smartcard-emulation feature, which happened to have it enabled but shouldn't be relied on for that). Live-verified against the TightVNC test server that nothing regressed (connect, cursor, clipboard); that server consistently sends Raw for actual framebuffer content regardless of announced encodings, so Hextile/ZRLE's live decode path isn't independently confirmed against a real server -- the unit tests are the primary evidence here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1511 lines
60 KiB
C++
1511 lines
60 KiB
C++
#include "vnc_session_backend.h"
|
|
|
|
#include <QTcpServer>
|
|
#include <QTcpSocket>
|
|
#include <QTest>
|
|
|
|
#include <zlib.h>
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
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));
|
|
}
|
|
|
|
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
|
#include "test_vnc_session_backend.moc"
|