Internal
Public Access
Add VNC Hextile decoding
Implements RFC 6143 SS7.7.4: rectangles announced as Hextile (type 5) tile the update into 16x16 blocks, each either raw pixels or a background fill plus an optional list of foreground/individually- colored subrects, with background/foreground persisting across tiles within one rectangle when not re-specified. The pure byte-decode logic (tile metadata, subrect list) lives in new src/vnc_pixel_codecs.h/.cpp, kept separate from VncSessionBackend's wire-sequencing state machine so it's unit-testable without a socket -- the pattern the plan calls for continuing into the ZRLE/Tight work still ahead. Adds 5 fake-server tests covering a raw tile, a background-only solid fill, uncoloured and individually-colored subrects, and a 4-tile rectangle proving background persistence and correct tile-cursor wraparound. Verified against the live TightVNC test server (connect, frame, cursor, clipboard all still work); that particular server always chose Raw for the actual framebuffer content during this session, so Hextile's real-world path isn't independently confirmed live -- the unit tests are the primary correctness evidence for this phase. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,8 @@ set(ORBITHUB_SOURCES
|
|||||||
src/ssh_session_backend.h
|
src/ssh_session_backend.h
|
||||||
src/vnc_session_backend.cpp
|
src/vnc_session_backend.cpp
|
||||||
src/vnc_session_backend.h
|
src/vnc_session_backend.h
|
||||||
|
src/vnc_pixel_codecs.cpp
|
||||||
|
src/vnc_pixel_codecs.h
|
||||||
src/unsupported_session_backend.cpp
|
src/unsupported_session_backend.cpp
|
||||||
src/unsupported_session_backend.h
|
src/unsupported_session_backend.h
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#include "vnc_pixel_codecs.h"
|
||||||
|
|
||||||
|
namespace VncPixelCodecs {
|
||||||
|
|
||||||
|
QRgb rgbFromPixelBytes(const uchar* bytes)
|
||||||
|
{
|
||||||
|
// Byte order B,G,R,pad -- matches VncSessionBackend's negotiated
|
||||||
|
// SetPixelFormat (32bpp little-endian, R at shift 16 / G at 8 / B at 0).
|
||||||
|
return qRgb(bytes[2], bytes[1], bytes[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int hextileFixedMetaByteCount(quint8 subencoding)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
if ((subencoding & HextileFlags::kBackgroundSpecified) != 0) {
|
||||||
|
count += 4;
|
||||||
|
}
|
||||||
|
if ((subencoding & HextileFlags::kForegroundSpecified) != 0) {
|
||||||
|
count += 4;
|
||||||
|
}
|
||||||
|
if ((subencoding & HextileFlags::kAnySubrects) != 0) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int decodeHextileFixedMeta(quint8 subencoding, const QByteArray& data, QRgb* background,
|
||||||
|
QRgb* foreground)
|
||||||
|
{
|
||||||
|
int offset = 0;
|
||||||
|
const auto* bytes = reinterpret_cast<const uchar*>(data.constData());
|
||||||
|
|
||||||
|
if ((subencoding & HextileFlags::kBackgroundSpecified) != 0) {
|
||||||
|
*background = rgbFromPixelBytes(bytes + offset);
|
||||||
|
offset += 4;
|
||||||
|
}
|
||||||
|
if ((subencoding & HextileFlags::kForegroundSpecified) != 0) {
|
||||||
|
*foreground = rgbFromPixelBytes(bytes + offset);
|
||||||
|
offset += 4;
|
||||||
|
}
|
||||||
|
if ((subencoding & HextileFlags::kAnySubrects) != 0) {
|
||||||
|
return static_cast<int>(static_cast<quint8>(data.at(offset)));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hextileSubrectByteCount(bool coloured, int subrectCount)
|
||||||
|
{
|
||||||
|
return subrectCount * (coloured ? 6 : 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<HextileSubrect> decodeHextileSubrects(bool coloured, int subrectCount,
|
||||||
|
const QByteArray& data, QRgb foreground)
|
||||||
|
{
|
||||||
|
QVector<HextileSubrect> subrects;
|
||||||
|
subrects.reserve(subrectCount);
|
||||||
|
|
||||||
|
const auto* bytes = reinterpret_cast<const uchar*>(data.constData());
|
||||||
|
const int stride = coloured ? 6 : 2;
|
||||||
|
for (int i = 0; i < subrectCount; ++i) {
|
||||||
|
const uchar* entry = bytes + (i * stride);
|
||||||
|
QRgb color = foreground;
|
||||||
|
int fieldOffset = 0;
|
||||||
|
if (coloured) {
|
||||||
|
color = rgbFromPixelBytes(entry);
|
||||||
|
fieldOffset = 4;
|
||||||
|
}
|
||||||
|
const uchar xy = entry[fieldOffset];
|
||||||
|
const uchar wh = entry[fieldOffset + 1];
|
||||||
|
// High nibble = x (or width-1), low nibble = y (or height-1).
|
||||||
|
const int x = (xy >> 4) & 0x0F;
|
||||||
|
const int y = xy & 0x0F;
|
||||||
|
const int width = ((wh >> 4) & 0x0F) + 1;
|
||||||
|
const int height = (wh & 0x0F) + 1;
|
||||||
|
subrects.append(HextileSubrect{QRect(x, y, width, height), color});
|
||||||
|
}
|
||||||
|
return subrects;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#ifndef ORBITHUB_VNC_PIXEL_CODECS_H
|
||||||
|
#define ORBITHUB_VNC_PIXEL_CODECS_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QRgb>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
// Pure, state-free decode helpers for VNC/RFB pixel encodings, kept out of
|
||||||
|
// VncSessionBackend so the wire-sequencing (when to read what off the
|
||||||
|
// socket) and wire-decoding (how to interpret already-buffered bytes) stay
|
||||||
|
// separate and the latter is unit-testable without a live connection.
|
||||||
|
namespace VncPixelCodecs {
|
||||||
|
|
||||||
|
// Converts one pixel's worth of raw bytes as sent under
|
||||||
|
// VncSessionBackend's negotiated SetPixelFormat (32bpp, little-endian,
|
||||||
|
// byte order B,G,R,pad) into a QRgb.
|
||||||
|
QRgb rgbFromPixelBytes(const uchar* bytes);
|
||||||
|
|
||||||
|
// A single filled rectangle, in tile-local coordinates (0,0 = the tile's
|
||||||
|
// own top-left corner, not the enclosing FramebufferUpdate rectangle's).
|
||||||
|
struct HextileSubrect {
|
||||||
|
QRect rect;
|
||||||
|
QRgb color = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// RFC 6143 SS7.7.4 Hextile tile subencoding byte flags.
|
||||||
|
namespace HextileFlags {
|
||||||
|
constexpr quint8 kRaw = 0x01;
|
||||||
|
constexpr quint8 kBackgroundSpecified = 0x02;
|
||||||
|
constexpr quint8 kForegroundSpecified = 0x04;
|
||||||
|
constexpr quint8 kAnySubrects = 0x08;
|
||||||
|
constexpr quint8 kSubrectsColoured = 0x10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte length of a Hextile tile's "fixed" metadata -- the optional
|
||||||
|
// background/foreground color updates plus the optional subrect count --
|
||||||
|
// derivable from the subencoding byte alone, before any of those bytes are
|
||||||
|
// available. Only meaningful when HextileFlags::kRaw is *not* set (a Raw
|
||||||
|
// tile has no metadata at all, just tileWidth*tileHeight raw pixels).
|
||||||
|
int hextileFixedMetaByteCount(quint8 subencoding);
|
||||||
|
|
||||||
|
// Parses the `hextileFixedMetaByteCount(subencoding)` bytes described
|
||||||
|
// above. Updates *background/*foreground in place only when the
|
||||||
|
// corresponding flag is set in `subencoding` -- callers should persist
|
||||||
|
// their previous values across tiles in the same rectangle and pass them
|
||||||
|
// in here unchanged when a color isn't re-specified, since RFC 6143 has
|
||||||
|
// each tile inherit the last-specified colors. Returns the subrect count
|
||||||
|
// (0 if HextileFlags::kAnySubrects isn't set).
|
||||||
|
int decodeHextileFixedMeta(quint8 subencoding, const QByteArray& data, QRgb* background,
|
||||||
|
QRgb* foreground);
|
||||||
|
|
||||||
|
// Byte length of `subrectCount` subrects' worth of data, given whether
|
||||||
|
// they're individually colored (HextileFlags::kSubrectsColoured).
|
||||||
|
int hextileSubrectByteCount(bool coloured, int subrectCount);
|
||||||
|
|
||||||
|
// Parses `subrectCount` subrects (xy + wh bytes, plus a per-subrect color
|
||||||
|
// when `coloured`) out of `data`, substituting `foreground` in for any
|
||||||
|
// that aren't individually colored.
|
||||||
|
QVector<HextileSubrect> decodeHextileSubrects(bool coloured, int subrectCount,
|
||||||
|
const QByteArray& data, QRgb foreground);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+144
-2
@@ -1,5 +1,7 @@
|
|||||||
#include "vnc_session_backend.h"
|
#include "vnc_session_backend.h"
|
||||||
|
|
||||||
|
#include "vnc_pixel_codecs.h"
|
||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QTcpSocket>
|
#include <QTcpSocket>
|
||||||
@@ -32,13 +34,15 @@ constexpr quint8 kServerMsgServerCutText = 3;
|
|||||||
// of truth for what we tell the server we can decode via SetEncodings.
|
// of truth for what we tell the server we can decode via SetEncodings.
|
||||||
constexpr qint32 kEncRaw = 0;
|
constexpr qint32 kEncRaw = 0;
|
||||||
constexpr qint32 kEncCopyRect = 1;
|
constexpr qint32 kEncCopyRect = 1;
|
||||||
|
constexpr qint32 kEncHextile = 5;
|
||||||
// RFC 6143 SS7.8.2 "Cursor pseudo-encoding": not a real screen-content
|
// RFC 6143 SS7.8.2 "Cursor pseudo-encoding": not a real screen-content
|
||||||
// encoding -- a rectangle with this type carries a cursor shape update
|
// encoding -- a rectangle with this type carries a cursor shape update
|
||||||
// instead (hotspot in x/y, image dims in width/height), never painted into
|
// instead (hotspot in x/y, image dims in width/height), never painted into
|
||||||
// the framebuffer.
|
// the framebuffer.
|
||||||
constexpr qint32 kEncCursor = -239;
|
constexpr qint32 kEncCursor = -239;
|
||||||
|
|
||||||
constexpr std::array<qint32, 3> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncCursor };
|
constexpr std::array<qint32, 4> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncHextile,
|
||||||
|
kEncCursor };
|
||||||
|
|
||||||
quint16 readU16BE(const QByteArray& buf, int offset)
|
quint16 readU16BE(const QByteArray& buf, int offset)
|
||||||
{
|
{
|
||||||
@@ -82,7 +86,14 @@ VncSessionBackend::VncSessionBackend(const Profile& profile, QObject* parent)
|
|||||||
m_pendingRectanglesRemaining(0),
|
m_pendingRectanglesRemaining(0),
|
||||||
m_pointerButtonMask(0),
|
m_pointerButtonMask(0),
|
||||||
m_lastPointerX(0),
|
m_lastPointerX(0),
|
||||||
m_lastPointerY(0)
|
m_lastPointerY(0),
|
||||||
|
m_hextileTileX(0),
|
||||||
|
m_hextileTileY(0),
|
||||||
|
m_hextileBackground(qRgb(0, 0, 0)),
|
||||||
|
m_hextileForeground(qRgb(0, 0, 0)),
|
||||||
|
m_hextileSubencoding(0),
|
||||||
|
m_hextileSubrectsRemaining(0),
|
||||||
|
m_hextileSubrectsColoured(false)
|
||||||
{
|
{
|
||||||
connect(m_socket, &QTcpSocket::connected, this, &VncSessionBackend::onSocketConnected);
|
connect(m_socket, &QTcpSocket::connected, this, &VncSessionBackend::onSocketConnected);
|
||||||
connect(m_socket, &QTcpSocket::readyRead, this, &VncSessionBackend::onSocketReadyRead);
|
connect(m_socket, &QTcpSocket::readyRead, this, &VncSessionBackend::onSocketReadyRead);
|
||||||
@@ -326,6 +337,10 @@ void VncSessionBackend::onSocketDisconnected()
|
|||||||
&& m_rfbState != RfbState::WaitingRawPixelData
|
&& m_rfbState != RfbState::WaitingRawPixelData
|
||||||
&& m_rfbState != RfbState::WaitingCopyRectSource
|
&& m_rfbState != RfbState::WaitingCopyRectSource
|
||||||
&& m_rfbState != RfbState::WaitingCursorPixelData
|
&& m_rfbState != RfbState::WaitingCursorPixelData
|
||||||
|
&& m_rfbState != RfbState::WaitingHextileTileSubencoding
|
||||||
|
&& m_rfbState != RfbState::WaitingHextileTileMeta
|
||||||
|
&& m_rfbState != RfbState::WaitingHextileSubrectData
|
||||||
|
&& m_rfbState != RfbState::WaitingHextileRawTileData
|
||||||
&& m_rfbState != RfbState::WaitingSetColourMapHeader
|
&& m_rfbState != RfbState::WaitingSetColourMapHeader
|
||||||
&& m_rfbState != RfbState::WaitingSetColourMapData
|
&& m_rfbState != RfbState::WaitingSetColourMapData
|
||||||
&& m_rfbState != RfbState::WaitingServerCutTextHeader
|
&& m_rfbState != RfbState::WaitingServerCutTextHeader
|
||||||
@@ -370,6 +385,10 @@ void VncSessionBackend::resetProtocolState()
|
|||||||
m_framebuffer = QImage();
|
m_framebuffer = QImage();
|
||||||
m_pendingRectanglesRemaining = 0;
|
m_pendingRectanglesRemaining = 0;
|
||||||
m_pointerButtonMask = 0;
|
m_pointerButtonMask = 0;
|
||||||
|
m_hextileTileX = 0;
|
||||||
|
m_hextileTileY = 0;
|
||||||
|
m_hextileBackground = qRgb(0, 0, 0);
|
||||||
|
m_hextileForeground = qRgb(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VncSessionBackend::haveBytes(int count) const
|
bool VncSessionBackend::haveBytes(int count) const
|
||||||
@@ -516,6 +535,29 @@ void VncSessionBackend::onRectangleFinished()
|
|||||||
requestFramebufferUpdate(true);
|
requestFramebufferUpdate(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QRect VncSessionBackend::currentHextileTileRect() const
|
||||||
|
{
|
||||||
|
const int tileWidth = qMin(16, m_currentRectangle.width - m_hextileTileX);
|
||||||
|
const int tileHeight = qMin(16, m_currentRectangle.height - m_hextileTileY);
|
||||||
|
return QRect(m_currentRectangle.x + m_hextileTileX, m_currentRectangle.y + m_hextileTileY,
|
||||||
|
tileWidth, tileHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VncSessionBackend::advanceHextileTile()
|
||||||
|
{
|
||||||
|
m_hextileTileX += 16;
|
||||||
|
if (m_hextileTileX >= m_currentRectangle.width) {
|
||||||
|
m_hextileTileX = 0;
|
||||||
|
m_hextileTileY += 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_hextileTileY >= m_currentRectangle.height) {
|
||||||
|
onRectangleFinished();
|
||||||
|
} else {
|
||||||
|
m_rfbState = RfbState::WaitingHextileTileSubencoding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void VncSessionBackend::processReceiveBuffer()
|
void VncSessionBackend::processReceiveBuffer()
|
||||||
{
|
{
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -819,6 +861,18 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
case kEncCursor:
|
case kEncCursor:
|
||||||
m_rfbState = RfbState::WaitingCursorPixelData;
|
m_rfbState = RfbState::WaitingCursorPixelData;
|
||||||
break;
|
break;
|
||||||
|
case kEncHextile:
|
||||||
|
m_hextileTileX = 0;
|
||||||
|
m_hextileTileY = 0;
|
||||||
|
m_hextileBackground = qRgb(0, 0, 0);
|
||||||
|
m_hextileForeground = qRgb(0, 0, 0);
|
||||||
|
if (m_currentRectangle.width <= 0 || m_currentRectangle.height <= 0) {
|
||||||
|
// No tiles to decode at all.
|
||||||
|
onRectangleFinished();
|
||||||
|
} else {
|
||||||
|
m_rfbState = RfbState::WaitingHextileTileSubencoding;
|
||||||
|
}
|
||||||
|
break;
|
||||||
default: {
|
default: {
|
||||||
// SetEncodings (see kAnnouncedEncodings) is entirely
|
// SetEncodings (see kAnnouncedEncodings) is entirely
|
||||||
// client-controlled, so a spec-compliant server will never
|
// client-controlled, so a spec-compliant server will never
|
||||||
@@ -941,6 +995,94 @@ void VncSessionBackend::processReceiveBuffer()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingHextileTileSubencoding: {
|
||||||
|
if (!haveBytes(1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_hextileSubencoding = static_cast<quint8>(m_recvBuffer.at(0));
|
||||||
|
m_recvBuffer.remove(0, 1);
|
||||||
|
|
||||||
|
if ((m_hextileSubencoding & VncPixelCodecs::HextileFlags::kRaw) != 0) {
|
||||||
|
m_rfbState = RfbState::WaitingHextileRawTileData;
|
||||||
|
} else {
|
||||||
|
m_pendingLength = static_cast<quint32>(
|
||||||
|
VncPixelCodecs::hextileFixedMetaByteCount(m_hextileSubencoding));
|
||||||
|
m_rfbState = RfbState::WaitingHextileTileMeta;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingHextileTileMeta: {
|
||||||
|
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray data = m_recvBuffer.left(static_cast<int>(m_pendingLength));
|
||||||
|
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
||||||
|
|
||||||
|
const int subrectCount = VncPixelCodecs::decodeHextileFixedMeta(
|
||||||
|
m_hextileSubencoding, data, &m_hextileBackground, &m_hextileForeground);
|
||||||
|
|
||||||
|
// Every tile is filled with the (possibly just-updated,
|
||||||
|
// possibly inherited) background colour first, regardless of
|
||||||
|
// whether this tile re-specified it.
|
||||||
|
QPainter painter(&m_framebuffer);
|
||||||
|
painter.fillRect(currentHextileTileRect(), QColor::fromRgb(m_hextileBackground));
|
||||||
|
|
||||||
|
m_hextileSubrectsColoured =
|
||||||
|
(m_hextileSubencoding & VncPixelCodecs::HextileFlags::kSubrectsColoured) != 0;
|
||||||
|
m_hextileSubrectsRemaining = subrectCount;
|
||||||
|
if (subrectCount == 0) {
|
||||||
|
advanceHextileTile();
|
||||||
|
} else {
|
||||||
|
m_pendingLength = static_cast<quint32>(VncPixelCodecs::hextileSubrectByteCount(
|
||||||
|
m_hextileSubrectsColoured, subrectCount));
|
||||||
|
m_rfbState = RfbState::WaitingHextileSubrectData;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingHextileSubrectData: {
|
||||||
|
if (!haveBytes(static_cast<int>(m_pendingLength))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray data = m_recvBuffer.left(static_cast<int>(m_pendingLength));
|
||||||
|
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
|
||||||
|
|
||||||
|
const QVector<VncPixelCodecs::HextileSubrect> subrects =
|
||||||
|
VncPixelCodecs::decodeHextileSubrects(m_hextileSubrectsColoured,
|
||||||
|
m_hextileSubrectsRemaining, data,
|
||||||
|
m_hextileForeground);
|
||||||
|
const QRect tileRect = currentHextileTileRect();
|
||||||
|
QPainter painter(&m_framebuffer);
|
||||||
|
for (const VncPixelCodecs::HextileSubrect& subrect : subrects) {
|
||||||
|
painter.fillRect(subrect.rect.translated(tileRect.topLeft()),
|
||||||
|
QColor::fromRgb(subrect.color));
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceHextileTile();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RfbState::WaitingHextileRawTileData: {
|
||||||
|
const QRect tileRect = currentHextileTileRect();
|
||||||
|
const qint64 byteCount =
|
||||||
|
static_cast<qint64>(tileRect.width()) * tileRect.height() * 4;
|
||||||
|
if (!haveBytes(static_cast<int>(byteCount))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteCount > 0) {
|
||||||
|
const QImage tileImage(reinterpret_cast<const uchar*>(m_recvBuffer.constData()),
|
||||||
|
tileRect.width(), tileRect.height(),
|
||||||
|
tileRect.width() * 4, QImage::Format_RGB32);
|
||||||
|
QPainter painter(&m_framebuffer);
|
||||||
|
painter.drawImage(tileRect.topLeft(), tileImage);
|
||||||
|
}
|
||||||
|
m_recvBuffer.remove(0, static_cast<int>(byteCount));
|
||||||
|
advanceHextileTile();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case RfbState::WaitingSetColourMapHeader: {
|
case RfbState::WaitingSetColourMapHeader: {
|
||||||
if (!haveBytes(5)) {
|
if (!haveBytes(5)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <QAbstractSocket>
|
#include <QAbstractSocket>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QRgb>
|
||||||
|
|
||||||
class QTcpSocket;
|
class QTcpSocket;
|
||||||
|
|
||||||
@@ -20,10 +22,11 @@ class QTcpSocket;
|
|||||||
//
|
//
|
||||||
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
||||||
// Authentication (security type 2) and no-auth (type 1) only -- not
|
// Authentication (security type 2) and no-auth (type 1) only -- not
|
||||||
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect encodings only.
|
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect + Hextile
|
||||||
// No dynamic resize. Clipboard sync (Latin-1 only, per RFB's
|
// encodings (ZRLE/Tight compression not yet implemented). No dynamic
|
||||||
// ServerCutText/ClientCutText) and remote cursor shape sync (the Cursor
|
// resize. Clipboard sync (Latin-1 only, per RFB's ServerCutText/
|
||||||
// pseudo-encoding) are supported.
|
// ClientCutText) and remote cursor shape sync (the Cursor pseudo-encoding)
|
||||||
|
// are supported.
|
||||||
class VncSessionBackend : public SessionBackend
|
class VncSessionBackend : public SessionBackend
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -83,6 +86,10 @@ private:
|
|||||||
WaitingRawPixelData,
|
WaitingRawPixelData,
|
||||||
WaitingCopyRectSource,
|
WaitingCopyRectSource,
|
||||||
WaitingCursorPixelData,
|
WaitingCursorPixelData,
|
||||||
|
WaitingHextileTileSubencoding,
|
||||||
|
WaitingHextileTileMeta,
|
||||||
|
WaitingHextileSubrectData,
|
||||||
|
WaitingHextileRawTileData,
|
||||||
WaitingSetColourMapHeader,
|
WaitingSetColourMapHeader,
|
||||||
WaitingSetColourMapData,
|
WaitingSetColourMapData,
|
||||||
WaitingServerCutTextHeader,
|
WaitingServerCutTextHeader,
|
||||||
@@ -120,6 +127,18 @@ private:
|
|||||||
int m_lastPointerX;
|
int m_lastPointerX;
|
||||||
int m_lastPointerY;
|
int m_lastPointerY;
|
||||||
|
|
||||||
|
// Hextile decode state (RFC 6143 SS7.7.4): tile-cursor position relative
|
||||||
|
// to the current rectangle's origin, plus the background/foreground
|
||||||
|
// colors, which persist across tiles within one rectangle whenever a
|
||||||
|
// tile doesn't re-specify them.
|
||||||
|
int m_hextileTileX;
|
||||||
|
int m_hextileTileY;
|
||||||
|
QRgb m_hextileBackground;
|
||||||
|
QRgb m_hextileForeground;
|
||||||
|
quint8 m_hextileSubencoding;
|
||||||
|
int m_hextileSubrectsRemaining;
|
||||||
|
bool m_hextileSubrectsColoured;
|
||||||
|
|
||||||
void setState(SessionState state, const QString& message);
|
void setState(SessionState state, const QString& message);
|
||||||
void resetProtocolState();
|
void resetProtocolState();
|
||||||
void processReceiveBuffer();
|
void processReceiveBuffer();
|
||||||
@@ -134,6 +153,8 @@ private:
|
|||||||
void sendPointerEvent();
|
void sendPointerEvent();
|
||||||
void sendWheelClick(quint8 wheelBit);
|
void sendWheelClick(quint8 wheelBit);
|
||||||
void sendClientCutText(const QString& text);
|
void sendClientCutText(const QString& text);
|
||||||
|
QRect currentHextileTileRect() const;
|
||||||
|
void advanceHextileTile();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ add_test(NAME test_ssh_session_backend COMMAND test_ssh_session_backend)
|
|||||||
add_executable(test_vnc_session_backend
|
add_executable(test_vnc_session_backend
|
||||||
test_vnc_session_backend.cpp
|
test_vnc_session_backend.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/vnc_session_backend.cpp
|
${CMAKE_SOURCE_DIR}/src/vnc_session_backend.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/src/vnc_pixel_codecs.cpp
|
||||||
${CMAKE_SOURCE_DIR}/src/session_backend.h
|
${CMAKE_SOURCE_DIR}/src/session_backend.h
|
||||||
)
|
)
|
||||||
target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||||
|
|||||||
@@ -79,6 +79,52 @@ SessionConnectOptions makeOptions(const QString& password = QString())
|
|||||||
options.password = password;
|
options.password = password;
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void appendU16(QByteArray& buf, quint16 value)
|
||||||
|
{
|
||||||
|
buf.append(static_cast<char>((value >> 8) & 0xFF));
|
||||||
|
buf.append(static_cast<char>(value & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pixel bytes as expected under our negotiated SetPixelFormat: 32bpp
|
||||||
|
// little-endian, byte order B,G,R,pad.
|
||||||
|
QByteArray pixelBytes(int r, int g, int b)
|
||||||
|
{
|
||||||
|
QByteArray bytes;
|
||||||
|
bytes.append(static_cast<char>(b));
|
||||||
|
bytes.append(static_cast<char>(g));
|
||||||
|
bytes.append(static_cast<char>(r));
|
||||||
|
bytes.append(char(0));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray serverInitBytes(int width, int height)
|
||||||
|
{
|
||||||
|
QByteArray serverInit;
|
||||||
|
appendU16(serverInit, static_cast<quint16>(width));
|
||||||
|
appendU16(serverInit, static_cast<quint16>(height));
|
||||||
|
serverInit.append(QByteArray(16, char(0)));
|
||||||
|
serverInit.append(QByteArray(4, char(0)));
|
||||||
|
return serverInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A FramebufferUpdate message with exactly one Hextile-encoded rectangle
|
||||||
|
// (RFC 6143 encoding type 5), given the already fully-formed tile byte
|
||||||
|
// stream to follow the rectangle header.
|
||||||
|
QByteArray hextileFramebufferUpdate(int x, int y, int width, int height, const QByteArray& tileData)
|
||||||
|
{
|
||||||
|
QByteArray update;
|
||||||
|
update.append(char(0)); update.append(char(0));
|
||||||
|
update.append(char(0)); update.append(char(1)); // 1 rectangle
|
||||||
|
appendU16(update, static_cast<quint16>(x));
|
||||||
|
appendU16(update, static_cast<quint16>(y));
|
||||||
|
appendU16(update, static_cast<quint16>(width));
|
||||||
|
appendU16(update, static_cast<quint16>(height));
|
||||||
|
update.append(char(0)); update.append(char(0)); update.append(char(0));
|
||||||
|
update.append(char(5)); // encoding = Hextile
|
||||||
|
update.append(tileData);
|
||||||
|
return update;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestVncSessionBackend : public QObject
|
class TestVncSessionBackend : public QObject
|
||||||
@@ -114,6 +160,11 @@ private slots:
|
|||||||
void setClipboardTextSendsClientCutText();
|
void setClipboardTextSendsClientCutText();
|
||||||
void cursorPseudoEncodingProducesExpectedImageAndHotspot();
|
void cursorPseudoEncodingProducesExpectedImageAndHotspot();
|
||||||
void zeroSizeCursorPseudoEncodingHidesCursor();
|
void zeroSizeCursorPseudoEncodingHidesCursor();
|
||||||
|
void hextileRawTileProducesExpectedPixels();
|
||||||
|
void hextileBackgroundOnlyTileFillsSolidColor();
|
||||||
|
void hextileUncolouredSubrectUsesForeground();
|
||||||
|
void hextileColouredSubrectUsesOwnColor();
|
||||||
|
void hextileMultiTilePersistsBackgroundAcrossTiles();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<FakeVncServer> m_server;
|
std::unique_ptr<FakeVncServer> m_server;
|
||||||
@@ -843,5 +894,235 @@ void TestVncSessionBackend::zeroSizeCursorPseudoEncodingHidesCursor()
|
|||||||
QTRY_VERIFY(gotHidden);
|
QTRY_VERIFY(gotHidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::hextileRawTileProducesExpectedPixels()
|
||||||
|
{
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(2, 2));
|
||||||
|
break;
|
||||||
|
case 3: { // a single Raw-subencoding Hextile tile covering the whole 2x2 rectangle
|
||||||
|
QByteArray tile;
|
||||||
|
tile.append(char(0x01)); // HextileFlags::kRaw
|
||||||
|
tile += pixelBytes(255, 0, 0);
|
||||||
|
tile += pixelBytes(0, 255, 0);
|
||||||
|
tile += pixelBytes(0, 0, 255);
|
||||||
|
tile += pixelBytes(255, 255, 255);
|
||||||
|
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 2, 2, tile));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(2, 2));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 1), QColor(0, 0, 255));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 255, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::hextileBackgroundOnlyTileFillsSolidColor()
|
||||||
|
{
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(3, 3));
|
||||||
|
break;
|
||||||
|
case 3: { // BackgroundSpecified only, no subrects -- solid fill
|
||||||
|
QByteArray tile;
|
||||||
|
tile.append(char(0x02)); // HextileFlags::kBackgroundSpecified
|
||||||
|
tile += pixelBytes(0, 128, 255);
|
||||||
|
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 3, 3, tile));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
|
||||||
|
for (int y = 0; y < 3; ++y) {
|
||||||
|
for (int x = 0; x < 3; ++x) {
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(x, y), QColor(0, 128, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::hextileUncolouredSubrectUsesForeground()
|
||||||
|
{
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(4, 4));
|
||||||
|
break;
|
||||||
|
case 3: {
|
||||||
|
// Background(black) + Foreground(red) + one uncoloured 2x2
|
||||||
|
// subrect at tile-local (1,1) -- must render in the foreground
|
||||||
|
// color, everywhere else in the background color.
|
||||||
|
QByteArray tile;
|
||||||
|
tile.append(char(0x02 | 0x04 | 0x08)); // Background|Foreground|AnySubrects
|
||||||
|
tile += pixelBytes(0, 0, 0); // background
|
||||||
|
tile += pixelBytes(255, 0, 0); // foreground
|
||||||
|
tile.append(char(1)); // subrect count
|
||||||
|
tile.append(char(0x11)); // xy: x=1, y=1
|
||||||
|
tile.append(char(0x11)); // wh: width=2, height=2
|
||||||
|
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 4, 4, tile));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(4, 4));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 0, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 0, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(2, 2), QColor(255, 0, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(3, 3), QColor(0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::hextileColouredSubrectUsesOwnColor()
|
||||||
|
{
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(3, 3));
|
||||||
|
break;
|
||||||
|
case 3: {
|
||||||
|
// No background/foreground specified this tile -- both stay at
|
||||||
|
// the rectangle-start default (black). One individually-
|
||||||
|
// coloured 1x1 subrect at tile-local (0,0).
|
||||||
|
QByteArray tile;
|
||||||
|
tile.append(char(0x08 | 0x10)); // AnySubrects|SubrectsColoured
|
||||||
|
tile.append(char(1)); // subrect count
|
||||||
|
tile += pixelBytes(0, 255, 0); // this subrect's own color
|
||||||
|
tile.append(char(0x00)); // xy: x=0, y=0
|
||||||
|
tile.append(char(0x00)); // wh: width=1, height=1
|
||||||
|
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 3, 3, tile));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 255, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(0, 0, 0));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(2, 2), QColor(0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestVncSessionBackend::hextileMultiTilePersistsBackgroundAcrossTiles()
|
||||||
|
{
|
||||||
|
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
|
||||||
|
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
|
||||||
|
});
|
||||||
|
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
|
||||||
|
switch (m_server->nextStep()) {
|
||||||
|
case 0: {
|
||||||
|
QByteArray securityTypes;
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
securityTypes.append(char(1));
|
||||||
|
m_server->sendWhenConnected(securityTypes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
m_server->sendWhenConnected(serverInitBytes(20, 20));
|
||||||
|
break;
|
||||||
|
case 3: {
|
||||||
|
// A 20x20 rectangle tiles as (0,0) 16x16, (16,0) 4x16,
|
||||||
|
// (0,16) 16x4, (16,16) 4x4 -- only the first tile specifies a
|
||||||
|
// background color; the other three specify nothing at all
|
||||||
|
// (subencoding 0x00) and must inherit it.
|
||||||
|
QByteArray tile1;
|
||||||
|
tile1.append(char(0x02)); // BackgroundSpecified
|
||||||
|
tile1 += pixelBytes(0, 0, 255);
|
||||||
|
QByteArray tile2(1, char(0x00));
|
||||||
|
QByteArray tile3(1, char(0x00));
|
||||||
|
QByteArray tile4(1, char(0x00));
|
||||||
|
m_server->sendWhenConnected(
|
||||||
|
hextileFramebufferUpdate(0, 0, 20, 20, tile1 + tile2 + tile3 + tile4));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_backend->connectSession(makeOptions());
|
||||||
|
QTRY_VERIFY(m_gotFrame);
|
||||||
|
QCOMPARE(m_lastFrame.size(), QSize(20, 20));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 0, 255));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(19, 0), QColor(0, 0, 255));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(0, 19), QColor(0, 0, 255));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(19, 19), QColor(0, 0, 255));
|
||||||
|
QCOMPARE(m_lastFrame.pixelColor(10, 10), QColor(0, 0, 255));
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
QTEST_GUILESS_MAIN(TestVncSessionBackend)
|
||||||
#include "test_vnc_session_backend.moc"
|
#include "test_vnc_session_backend.moc"
|
||||||
|
|||||||
Reference in New Issue
Block a user