#include "vnc_session_backend.h" #include #include #include #include extern "C" { #include } #include 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((value >> 8) & 0xFF)); buf.append(static_cast(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(b)); bytes.append(static_cast(g)); bytes.append(static_cast(r)); bytes.append(char(0)); return bytes; } QByteArray serverInitBytes(int width, int height) { QByteArray serverInit; appendU16(serverInit, static_cast(width)); appendU16(serverInit, static_cast(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(x)); appendU16(update, static_cast(y)); appendU16(update, static_cast(width)); appendU16(update, static_cast(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((value >> 24) & 0xFF)); buf.append(static_cast((value >> 16) & 0xFF)); buf.append(static_cast((value >> 8) & 0xFF)); buf.append(static_cast(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(b)); bytes.append(static_cast(g)); bytes.append(static_cast(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(x)); appendU16(update, static_cast(y)); appendU16(update, static_cast(width)); appendU16(update, static_cast(height)); update.append(char(0)); update.append(char(0)); update.append(char(0)); update.append(char(16)); // encoding = ZRLE appendU32(update, static_cast(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(deflateBound(&stream, static_cast(input.size()))), char(0)); stream.next_in = reinterpret_cast(const_cast(input.constData())); stream.avail_in = static_cast(input.size()); stream.next_out = reinterpret_cast(output.data()); stream.avail_out = static_cast(output.size()); deflate(&stream, Z_FINISH); output.resize(output.size() - static_cast(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(deflateBound(&stream, static_cast(input.size()))) + 64, char(0)); stream.next_in = reinterpret_cast(const_cast(input.constData())); stream.avail_in = static_cast(input.size()); const int flushMode = finish ? Z_FINISH : Z_SYNC_FLUSH; int totalOut = 0; do { stream.next_out = reinterpret_cast(output.data() + totalOut); stream.avail_out = static_cast(output.size() - totalOut); deflate(&stream, flushMode); totalOut = output.size() - static_cast(stream.avail_out); } while (stream.avail_out == 0); output.resize(totalOut); return output; } // Mirrors VncSessionBackend's compact-length decode in reverse, for // building test fixtures. void appendTightCompactLength(QByteArray& buf, quint32 length) { buf.append(static_cast((length & 0x7F) | ((length > 0x7F) ? 0x80 : 0))); if (length <= 0x7F) { return; } length >>= 7; buf.append(static_cast((length & 0x7F) | ((length > 0x7F) ? 0x80 : 0))); if (length <= 0x7F) { return; } length >>= 7; buf.append(static_cast(length & 0xFF)); } // A FramebufferUpdate message with exactly one Tight-encoded rectangle // (RFC 6143 encoding type 7), given the already fully-formed byte stream // to follow the rectangle header (starting with the compression-control // byte). QByteArray tightFramebufferUpdate(int x, int y, int width, int height, const QByteArray& tightData) { QByteArray update; update.append(char(0)); update.append(char(0)); update.append(char(0)); update.append(char(1)); // 1 rectangle appendU16(update, static_cast(x)); appendU16(update, static_cast(y)); appendU16(update, static_cast(width)); appendU16(update, static_cast(height)); update.append(char(0)); update.append(char(0)); update.append(char(0)); update.append(char(7)); // encoding = Tight update.append(tightData); return update; } // Encodes a solid-colored width x height baseline JPEG using libjpeg-turbo // directly (mirroring the decode side's use of the same library), for // exercising Tight's JPEG sub-mode against real, valid JPEG bytes. QByteArray encodeJpegForTest(int width, int height, QRgb color) { jpeg_compress_struct cinfo; jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); unsigned char* buffer = nullptr; unsigned long bufferSize = 0; jpeg_mem_dest(&cinfo, &buffer, &bufferSize); cinfo.image_width = static_cast(width); cinfo.image_height = static_cast(height); cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, 100, TRUE); // Disable chroma subsampling so a solid color round-trips as exactly // as JPEG's DCT quantization allows, for a tighter test tolerance. for (int i = 0; i < cinfo.num_components; ++i) { cinfo.comp_info[i].h_samp_factor = 1; cinfo.comp_info[i].v_samp_factor = 1; } jpeg_start_compress(&cinfo, TRUE); QVector row(width * 3); for (int x = 0; x < width; ++x) { row[(x * 3) + 0] = static_cast(qRed(color)); row[(x * 3) + 1] = static_cast(qGreen(color)); row[(x * 3) + 2] = static_cast(qBlue(color)); } JSAMPROW rowPointer[1] = { row.data() }; for (int y = 0; y < height; ++y) { jpeg_write_scanlines(&cinfo, rowPointer, 1); } jpeg_finish_compress(&cinfo); const QByteArray result(reinterpret_cast(buffer), static_cast(bufferSize)); jpeg_destroy_compress(&cinfo); std::free(buffer); return result; } } class TestVncSessionBackend : public QObject { Q_OBJECT private slots: // Pure-function coverage. void desKeyFromPasswordMatchesKnownVector(); void desKeyFromPasswordPadsShortPasswords(); void desKeyFromPasswordTruncatesLongPasswords(); void vncAuthResponseIsSixteenBytesAndDeterministic(); void vncAuthResponseRejectsWrongChallengeSize(); void keysymForQtKeyMapsNamedKeys(); void keysymForQtKeyMapsFunctionKeys(); void keysymForQtKeyPassesThroughPrintableText(); void keysymForQtKeyUsesUnicodeConventionBeyondLatin1(); void keysymForQtKeyReturnsZeroForUnmapped(); void mapSocketErrorCoversCommonCases(); // State-machine coverage against a scripted in-process fake server. void init(); void cleanup(); void connectsWithNoAuthRfb38(); void connectsWithVncAuthenticationRfb38(); void authFailureRfb38ReachesFailedState(); void unsupportedSecurityTypeReachesFailedState(); void rfb33ServerWithNoAuthConnectsWithoutSecurityResult(); void rawFramebufferUpdateProducesExpectedPixels(); void copyRectEncodingDoesNotFailConnection(); void unannouncedEncodingFailsConnectionWithClearMessage(); void serverCutTextEmitsRemoteClipboardTextChanged(); void setClipboardTextSendsClientCutText(); void cursorPseudoEncodingProducesExpectedImageAndHotspot(); void zeroSizeCursorPseudoEncodingHidesCursor(); void hextileRawTileProducesExpectedPixels(); void hextileBackgroundOnlyTileFillsSolidColor(); void hextileUncolouredSubrectUsesForeground(); void hextileColouredSubrectUsesOwnColor(); void hextileMultiTilePersistsBackgroundAcrossTiles(); void zrleRawTileProducesExpectedPixels(); void zrleSolidTileFillsColor(); void zrlePackedPaletteTileProducesExpectedPixels(); void zrlePlainRleTileProducesExpectedPixels(); void zrlePaletteRleTileProducesExpectedPixels(); void zrleStreamPersistsAcrossTwoFramebufferUpdates(); void tightFillProducesExpectedPixels(); void tightBasicCopyFilterProducesExpectedPixels(); void tightBasicPaletteFilterProducesExpectedPixels(); void tightJpegProducesExpectedPixels(); void tightStreamResetFlagAllowsIndependentDecoding(); private: std::unique_ptr m_server; std::unique_ptr 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(); m_backend = std::make_unique(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(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(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 frames; connect(m_backend.get(), &SessionBackend::frameUpdated, this, [&frames](const QImage& frame) { frames.append(frame.copy()); }); connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() { m_server->sendWhenConnected(QByteArray("RFB 003.008\n")); }); connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, compressed1, compressed2]() { switch (m_server->nextStep()) { case 0: { QByteArray securityTypes; securityTypes.append(char(1)); securityTypes.append(char(1)); m_server->sendWhenConnected(securityTypes); break; } case 1: m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK break; case 2: m_server->sendWhenConnected(serverInitBytes(2, 1)); break; case 3: // first (non-incremental) FramebufferUpdateRequest m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 1, compressed1)); break; case 4: // next (incremental) FramebufferUpdateRequest m_server->sendWhenConnected(zrleFramebufferUpdate(0, 0, 2, 1, compressed2)); break; default: break; } }); m_backend->connectSession(makeOptions()); QTRY_VERIFY(frames.size() >= 2); QCOMPARE(m_lastState, SessionState::Connected); QCOMPARE(frames.at(0).pixelColor(0, 0), QColor(255, 0, 0)); QCOMPARE(frames.at(0).pixelColor(1, 0), QColor(0, 255, 0)); QCOMPARE(frames.at(1).pixelColor(0, 0), QColor(10, 20, 30)); QCOMPARE(frames.at(1).pixelColor(1, 0), QColor(10, 20, 30)); } void TestVncSessionBackend::tightFillProducesExpectedPixels() { QByteArray tight; tight.append(char(0x80)); // mode = Fill, no stream-reset bits tight += cpixelBytes(10, 200, 30); connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() { m_server->sendWhenConnected(QByteArray("RFB 003.008\n")); }); connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() { switch (m_server->nextStep()) { case 0: { QByteArray securityTypes; securityTypes.append(char(1)); securityTypes.append(char(1)); m_server->sendWhenConnected(securityTypes); break; } case 1: m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK break; case 2: m_server->sendWhenConnected(serverInitBytes(3, 3)); break; case 3: m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 3, 3, tight)); break; default: break; } }); m_backend->connectSession(makeOptions()); QTRY_VERIFY(m_gotFrame); QCOMPARE(m_lastFrame.size(), QSize(3, 3)); for (int y = 0; y < 3; ++y) { for (int x = 0; x < 3; ++x) { QCOMPARE(m_lastFrame.pixelColor(x, y), QColor(10, 200, 30)); } } } void TestVncSessionBackend::tightBasicCopyFilterProducesExpectedPixels() { QByteArray filtered; filtered += cpixelBytes(255, 0, 0); filtered += cpixelBytes(0, 255, 0); filtered += cpixelBytes(0, 0, 255); filtered += cpixelBytes(255, 255, 255); const QByteArray compressed = zlibCompressWhole(filtered); QByteArray tight; tight.append(char(0x00)); // mode = Basic, stream 0, Copy implied appendTightCompactLength(tight, static_cast(compressed.size())); tight += compressed; connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() { m_server->sendWhenConnected(QByteArray("RFB 003.008\n")); }); connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() { switch (m_server->nextStep()) { case 0: { QByteArray securityTypes; securityTypes.append(char(1)); securityTypes.append(char(1)); m_server->sendWhenConnected(securityTypes); break; } case 1: m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK break; case 2: m_server->sendWhenConnected(serverInitBytes(2, 2)); break; case 3: m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tight)); break; default: break; } }); m_backend->connectSession(makeOptions()); QTRY_VERIFY(m_gotFrame); QCOMPARE(m_lastFrame.size(), QSize(2, 2)); QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0)); QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0)); QCOMPARE(m_lastFrame.pixelColor(0, 1), QColor(0, 0, 255)); QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 255, 255)); } void TestVncSessionBackend::tightBasicPaletteFilterProducesExpectedPixels() { // 4 pixels, 2-color palette, 1 bit/pixel, packed MSB-first: indices // [0,1,1,0] -> byte 0b0110_0000 (same packing convention as ZRLE's // packed palette; with only one row here, continuous vs row-padded // bit-packing happen to coincide). QByteArray filtered; filtered.append(char(0x01)); // paletteSize - 1 = 1 -> paletteSize = 2 filtered += cpixelBytes(10, 20, 30); // palette[0] filtered += cpixelBytes(200, 210, 220); // palette[1] filtered.append(char(0x60)); const QByteArray compressed = zlibCompressWhole(filtered); QByteArray tight; tight.append(char(0x40)); // mode = Basic, stream 0, explicit filter follows tight.append(char(1)); // filter id = Palette appendTightCompactLength(tight, static_cast(compressed.size())); tight += compressed; connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() { m_server->sendWhenConnected(QByteArray("RFB 003.008\n")); }); connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() { switch (m_server->nextStep()) { case 0: { QByteArray securityTypes; securityTypes.append(char(1)); securityTypes.append(char(1)); m_server->sendWhenConnected(securityTypes); break; } case 1: m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK break; case 2: m_server->sendWhenConnected(serverInitBytes(4, 1)); break; case 3: m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 4, 1, tight)); break; default: break; } }); m_backend->connectSession(makeOptions()); QTRY_VERIFY(m_gotFrame); QCOMPARE(m_lastFrame.size(), QSize(4, 1)); QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(10, 20, 30)); QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(200, 210, 220)); QCOMPARE(m_lastFrame.pixelColor(2, 0), QColor(200, 210, 220)); QCOMPARE(m_lastFrame.pixelColor(3, 0), QColor(10, 20, 30)); } void TestVncSessionBackend::tightJpegProducesExpectedPixels() { // 8x8 (one full JPEG MCU at 1x1 chroma sampling) solid red. const QByteArray jpegBytes = encodeJpegForTest(8, 8, qRgb(220, 20, 20)); QByteArray tight; tight.append(char(0x90)); // mode = JPEG appendTightCompactLength(tight, static_cast(jpegBytes.size())); tight += jpegBytes; connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() { m_server->sendWhenConnected(QByteArray("RFB 003.008\n")); }); connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tight]() { switch (m_server->nextStep()) { case 0: { QByteArray securityTypes; securityTypes.append(char(1)); securityTypes.append(char(1)); m_server->sendWhenConnected(securityTypes); break; } case 1: m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK break; case 2: m_server->sendWhenConnected(serverInitBytes(8, 8)); break; case 3: m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 8, 8, tight)); break; default: break; } }); m_backend->connectSession(makeOptions()); QTRY_VERIFY(m_gotFrame); QCOMPARE(m_lastFrame.size(), QSize(8, 8)); // JPEG is lossy even at quality 100 -- assert closeness, not equality. const QColor decoded = m_lastFrame.pixelColor(4, 4); QVERIFY(qAbs(decoded.red() - 220) <= 8); QVERIFY(qAbs(decoded.green() - 20) <= 8); QVERIFY(qAbs(decoded.blue() - 20) <= 8); } void TestVncSessionBackend::tightStreamResetFlagAllowsIndependentDecoding() { QByteArray filteredA; filteredA += cpixelBytes(10, 20, 30); filteredA += cpixelBytes(10, 20, 30); filteredA += cpixelBytes(10, 20, 30); filteredA += cpixelBytes(10, 20, 30); QByteArray filteredB; filteredB += cpixelBytes(200, 210, 220); filteredB += cpixelBytes(200, 210, 220); filteredB += cpixelBytes(200, 210, 220); filteredB += cpixelBytes(200, 210, 220); // Each independently a complete, self-contained zlib stream (its own // deflateInit + Z_FINISH) -- decoding the second one correctly after // the first, on the same persistent stream slot, requires that // stream-reset bit 0 in rectangle B's control byte actually tore down // and re-initialized stream 0 rather than trying to keep feeding an // already-finished stream more data. const QByteArray compressedA = zlibCompressWhole(filteredA); const QByteArray compressedB = zlibCompressWhole(filteredB); QByteArray tightA; tightA.append(char(0x00)); // mode = Basic, stream 0, Copy, no reset appendTightCompactLength(tightA, static_cast(compressedA.size())); tightA += compressedA; QByteArray tightB; tightB.append(char(0x01)); // mode = Basic, stream 0, Copy, reset stream 0 appendTightCompactLength(tightB, static_cast(compressedB.size())); tightB += compressedB; QVector frames; connect(m_backend.get(), &SessionBackend::frameUpdated, this, [&frames](const QImage& frame) { frames.append(frame.copy()); }); connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() { m_server->sendWhenConnected(QByteArray("RFB 003.008\n")); }); connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, tightA, tightB]() { switch (m_server->nextStep()) { case 0: { QByteArray securityTypes; securityTypes.append(char(1)); securityTypes.append(char(1)); m_server->sendWhenConnected(securityTypes); break; } case 1: m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK break; case 2: m_server->sendWhenConnected(serverInitBytes(2, 2)); break; case 3: // first (non-incremental) FramebufferUpdateRequest m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tightA)); break; case 4: // next (incremental) FramebufferUpdateRequest m_server->sendWhenConnected(tightFramebufferUpdate(0, 0, 2, 2, tightB)); break; default: break; } }); m_backend->connectSession(makeOptions()); QTRY_VERIFY(frames.size() >= 2); QCOMPARE(m_lastState, SessionState::Connected); QCOMPARE(frames.at(0).pixelColor(0, 0), QColor(10, 20, 30)); QCOMPARE(frames.at(1).pixelColor(0, 0), QColor(200, 210, 220)); } QTEST_GUILESS_MAIN(TestVncSessionBackend) #include "test_vnc_session_backend.moc"