Files
LookingGlass/client/tests/lgmp_clipboard_test.c
Geoffrey McRae cecd4b6f48 [client] tests: exercise mandatory LGMP streams
Replace legacy clipboard grant and queued input fixtures with bound SPSC
stream endpoints. Verify stream-only payload delivery, credit recovery,
release completion, and four 256 KiB records for a 1 MiB response.

Keep queue assertions for status, discovery, ownership, and control.
2026-08-15 14:51:24 +10:00

1987 lines
71 KiB
C

/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "clipboard.h"
#include "common/KVMFRClipboard.h"
#include "common/LGMPConfig.h"
#include "common/debug.h"
#include <lgmp/client.h>
#include <lgmp/host.h>
#include <lgmp/stream.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define TEST_WAIT_MS 2500U
#define TEST_QUIET_MS 50U
#define TEST_MEMORY_MAX 128U
#define SLOT_BYTES KVMFR_CLIPBOARD_STREAM_RECORD_BYTES
#define TEST_SHM_OVERHEAD (1024U * 1024U)
/* Match the production duplex streams and leave room for LGMP metadata. */
#define TEST_SHM_SIZE \
(2U * KVMFR_CLIPBOARD_STREAM_SLOT_COUNT * SLOT_BYTES + \
TEST_SHM_OVERHEAD)
#define CHECK(x) \
do \
{ \
if (!(x)) \
{ \
fprintf(stderr, "check failed at %s:%d: %s\n", \
__FILE__, __LINE__, #x); \
return false; \
} \
} \
while (0)
typedef struct EventTrace
{
atomic_uint notice;
atomic_uint release;
atomic_uint request;
atomic_uint requestCancel;
atomic_uint dataBegin;
atomic_uint dataChunk;
atomic_uint dataEnd;
atomic_uint dataCancel;
atomic_uint dataReady;
atomic_uint fileOffer;
atomic_uint fileAcquire;
atomic_uint fileAcquired;
atomic_uint fileRequest;
atomic_uint fileCancel;
atomic_bool acceptRequest;
atomic_bool blockChunkOnce;
atomic_bool attached;
atomic_bool fileOfferBeforeNotice;
LG_ClipboardData noticeTypes[LG_CLIPBOARD_DATA_NONE];
size_t noticeCount;
LG_ClipboardRequest requestID;
LG_ClipboardData requestType;
LG_ClipboardRequest streamID;
LG_ClipboardData streamType;
uint64_t sizeHint;
uint64_t chunkOffset;
size_t chunkSize;
uint8_t chunkData[64];
uint64_t finalSize;
LG_ClipboardRequest cancelID;
LG_ClipboardCancelReason cancelReason;
uint64_t fileDataset;
uint64_t fileAcquisition;
LG_ClipboardFileError fileError;
LG_ClipboardFileRequest fileRequestValue;
}
EventTrace;
typedef struct TestState
{
void * memory;
PLGMPHost host;
PLGMPHostQueue queue;
PLGMPClient client;
PLGMPHostStream hostToClientStream;
PLGMPHostStream clientToHostStream;
KVMFRStreamDescriptor hostToClientDescriptor;
KVMFRStreamDescriptor clientToHostDescriptor;
bool streamsBound;
LGMPClipboard * clipboard;
const LG_ClipboardOps * ops;
uint32_t clientID;
uint32_t hostSerial;
uint64_t localClipboardGeneration;
PLGMPMemory memories[TEST_MEMORY_MAX];
unsigned memoryCount;
atomic_uint statusCount;
atomic_bool statusAvailable;
atomic_uint statusGeneration;
atomic_bool autoAttach;
EventTrace events;
}
TestState;
typedef struct DisconnectTask
{
LGMPClipboard * clipboard;
atomic_bool done;
}
DisconnectTask;
static bool disconnectAndDrain(TestState * state,
uint32_t * releaseGeneration);
static void notice(void * opaque, const LG_ClipboardData types[],
size_t count)
{
EventTrace * trace = opaque;
if (count == 1 && types[0] == LG_CLIPBOARD_DATA_FILES)
atomic_store_explicit(&trace->fileOfferBeforeNotice,
atomic_load_explicit(&trace->fileOffer, memory_order_acquire) != 0,
memory_order_release);
trace->noticeCount = count;
if (count)
memcpy(trace->noticeTypes, types, count * sizeof(*types));
atomic_fetch_add_explicit(&trace->notice, 1, memory_order_release);
}
static void releaseNotice(void * opaque)
{
EventTrace * trace = opaque;
atomic_fetch_add_explicit(&trace->release, 1, memory_order_release);
}
static bool request(void * opaque, LG_ClipboardRequest id,
LG_ClipboardData type)
{
EventTrace * trace = opaque;
trace->requestID = id;
trace->requestType = type;
atomic_fetch_add_explicit(&trace->request, 1, memory_order_release);
return atomic_load_explicit(&trace->acceptRequest, memory_order_acquire);
}
static LG_ClipboardResult dataBegin(void * opaque,
LG_ClipboardRequest id, LG_ClipboardData type, uint64_t sizeHint)
{
EventTrace * trace = opaque;
trace->streamID = id;
trace->streamType = type;
trace->sizeHint = sizeHint;
atomic_fetch_add_explicit(&trace->dataBegin, 1, memory_order_release);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static LG_ClipboardResult dataChunk(void * opaque,
LG_ClipboardRequest id, uint64_t offset,
const void * data, size_t size)
{
EventTrace * trace = opaque;
trace->streamID = id;
trace->chunkOffset = offset;
trace->chunkSize = size;
if (size <= sizeof(trace->chunkData))
memcpy(trace->chunkData, data, size);
atomic_fetch_add_explicit(&trace->dataChunk, 1, memory_order_release);
if (atomic_exchange_explicit(&trace->blockChunkOnce, false,
memory_order_acq_rel))
return LG_CLIPBOARD_RESULT_BLOCKED;
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static LG_ClipboardResult dataEnd(void * opaque,
LG_ClipboardRequest id, uint64_t size)
{
EventTrace * trace = opaque;
trace->streamID = id;
trace->finalSize = size;
atomic_fetch_add_explicit(&trace->dataEnd, 1, memory_order_release);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static void dataCancelEvent(void * opaque, LG_ClipboardRequest id,
LG_ClipboardCancelReason reason)
{
EventTrace * trace = opaque;
trace->cancelID = id;
trace->cancelReason = reason;
atomic_fetch_add_explicit(&trace->dataCancel, 1, memory_order_release);
}
static void dataReadyEvent(void * opaque, LG_ClipboardRequest id)
{
EventTrace * trace = opaque;
trace->streamID = id;
atomic_fetch_add_explicit(&trace->dataReady, 1, memory_order_release);
}
static void requestCancelEvent(void * opaque, LG_ClipboardRequest id,
LG_ClipboardCancelReason reason)
{
EventTrace * trace = opaque;
trace->cancelID = id;
trace->cancelReason = reason;
atomic_fetch_add_explicit(
&trace->requestCancel, 1, memory_order_release);
}
static void fileOfferEvent(void * opaque, uint64_t dataset)
{
EventTrace * trace = opaque;
trace->fileDataset = dataset;
atomic_fetch_add_explicit(&trace->fileOffer, 1, memory_order_release);
}
static void fileAcquireEvent(void * opaque, uint64_t dataset,
uint64_t acquisition)
{
EventTrace * trace = opaque;
trace->fileDataset = dataset;
trace->fileAcquisition = acquisition;
atomic_fetch_add_explicit(&trace->fileAcquire, 1, memory_order_release);
}
static void fileAcquiredEvent(void * opaque, uint64_t dataset,
uint64_t acquisition, LG_ClipboardFileError error)
{
EventTrace * trace = opaque;
trace->fileDataset = dataset;
trace->fileAcquisition = acquisition;
trace->fileError = error;
atomic_fetch_add_explicit(&trace->fileAcquired, 1, memory_order_release);
}
static void fileRequestEvent(void * opaque,
const LG_ClipboardFileRequest * request)
{
EventTrace * trace = opaque;
trace->fileRequestValue = *request;
atomic_fetch_add_explicit(&trace->fileRequest, 1, memory_order_release);
}
static void fileCancelEvent(void * opaque, uint64_t dataset,
uint64_t request, LG_ClipboardFileError error)
{
EventTrace * trace = opaque;
trace->fileDataset = dataset;
trace->fileAcquisition = request;
trace->fileError = error;
atomic_fetch_add_explicit(&trace->fileCancel, 1, memory_order_release);
}
static const LG_ClipboardEventOps EVENT_OPS =
{
.notice = notice,
.dataBegin = dataBegin,
.dataChunk = dataChunk,
.dataEnd = dataEnd,
.dataCancel = dataCancelEvent,
.dataReady = dataReadyEvent,
.requestCancel = requestCancelEvent,
.release = releaseNotice,
.request = request,
.fileOffer = fileOfferEvent,
.fileAcquire = fileAcquireEvent,
.fileAcquired = fileAcquiredEvent,
.fileRequest = fileRequestEvent,
.fileCancel = fileCancelEvent,
};
static void statusChanged(void * opaque, const LG_ClipboardStatus * status)
{
TestState * state = opaque;
atomic_store_explicit(&state->statusAvailable,
status->available, memory_order_release);
atomic_store_explicit(&state->statusGeneration,
status->generation, memory_order_release);
atomic_fetch_add_explicit(&state->statusCount, 1, memory_order_release);
if (status->available &&
atomic_load_explicit(&state->autoAttach, memory_order_acquire) &&
!atomic_exchange_explicit(
&state->events.attached, true, memory_order_acq_rel))
{
const bool attached = state->ops->attach(
state->clipboard, &EVENT_OPS, &state->events);
atomic_store_explicit(
&state->events.attached, attached, memory_order_release);
}
}
static bool hostProcess(TestState * state)
{
CHECK(lgmpHostProcess(state->host) == LGMP_OK);
return true;
}
static KVMFRStreamDescriptor exportStreamDescriptor(
const struct LGMPStreamDescriptor * descriptor)
{
return (KVMFRStreamDescriptor)
{
.magic = descriptor->magic,
.version = descriptor->version,
.size = descriptor->size,
.offset = descriptor->offset,
.regionSize = descriptor->regionSize,
.direction = descriptor->direction,
.policy = descriptor->policy,
.slotCount = descriptor->slotCount,
.slotSize = descriptor->slotSize,
};
}
static bool waitAtomic(TestState * state, atomic_uint * value,
unsigned expected)
{
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
if (atomic_load_explicit(value, memory_order_acquire) >= expected)
return true;
usleep(1000);
}
return false;
}
static bool waitMemory(TestState * state, PLGMPMemory memory)
{
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
if (!lgmpHostQueuePayloadPending(state->queue, memory))
return true;
usleep(1000);
}
return false;
}
static bool allocMemory(TestState * state, size_t size,
PLGMPMemory * result)
{
CHECK(state->memoryCount < TEST_MEMORY_MAX);
CHECK(lgmpHostMemAlloc(state->host, (uint32_t)size, result) == LGMP_OK);
state->memories[state->memoryCount++] = *result;
memset(lgmpHostMemPtr(*result), 0, size);
return true;
}
static bool postMemory(TestState * state, PLGMPMemory memory,
KVMFRClipboardQueueType type, uint32_t serial)
{
const uint64_t udata = KVMFR_CLIPBOARD_QUEUE_UDATA(type, serial);
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
unsigned recipients = 0;
const LGMP_STATUS status = lgmpHostQueuePostForClients(state->queue,
udata, memory, &state->clientID, 1, &recipients);
if (status == LGMP_OK)
return recipients == 1;
CHECK(status == LGMP_ERR_QUEUE_FULL);
CHECK(hostProcess(state));
usleep(1000);
}
return false;
}
static uint32_t nextSerial(TestState * state)
{
if (++state->hostSerial == 0)
++state->hostSerial;
return state->hostSerial;
}
static bool unbindStream(TestState * state, PLGMPHostStream stream)
{
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
const LGMP_STATUS status = lgmpHostStreamUnbind(stream);
if (status == LGMP_OK)
return true;
CHECK(status == LGMP_ERR_STREAM_BUSY);
CHECK(hostProcess(state));
usleep(1000);
}
return false;
}
static bool postStatus(TestState * state, uint32_t endpointGeneration,
uint32_t ownerClientID, uint32_t ownerGeneration,
bool available, KVMFRClipboardFormatFlags formats)
{
if (!ownerClientID && state->streamsBound)
{
CHECK(unbindStream(state, state->clientToHostStream));
CHECK(unbindStream(state, state->hostToClientStream));
state->streamsBound = false;
}
PLGMPMemory memory;
CHECK(allocMemory(state, sizeof(KVMFRClipboardStatus), &memory));
KVMFRClipboardStatus * status = lgmpHostMemPtr(memory);
*status = (KVMFRClipboardStatus)
{
.version = KVMFR_CLIPBOARD_VERSION,
.flags = (available ? KVMFR_CLIPBOARD_STATUS_AVAILABLE : 0) |
(ownerClientID ? KVMFR_CLIPBOARD_STATUS_HAS_OWNER : 0),
.generation = endpointGeneration,
.ownerClientID = ownerClientID,
.ownerGeneration = ownerGeneration,
.lease = 1000,
.formats = available ? formats : 0,
.slotBytes = KVMFR_CLIPBOARD_STREAM_SLOT_BYTES,
.streamVersion = KVMFR_CLIPBOARD_STREAM_VERSION,
.streamSlotCount = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
.hostToClient = state->hostToClientDescriptor,
.clientToHost = state->clientToHostDescriptor,
};
CHECK(postMemory(state, memory, KVMFR_CLIPBOARD_QUEUE_STATUS,
nextSerial(state)));
CHECK(waitMemory(state, memory));
return true;
}
static bool postRecord(TestState * state,
const KVMFRClipboardMessage * record,
KVMFRClipboardQueueType type, const void * data)
{
CHECK(type == KVMFR_CLIPBOARD_QUEUE_MESSAGE);
CHECK(record->length == 0);
CHECK(data == NULL);
PLGMPMemory memory;
CHECK(allocMemory(state, sizeof(*record), &memory));
memcpy(lgmpHostMemPtr(memory), record, sizeof(*record));
CHECK(postMemory(state, memory, type, nextSerial(state)));
return true;
}
static bool postStreamRecord(TestState * state,
const KVMFRClipboardMessage * record, const void * data)
{
CHECK(state->streamsBound);
CHECK(record->length <= KVMFR_CLIPBOARD_STREAM_SLOT_BYTES);
CHECK(!record->length || data);
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
LGMPStreamBuffer buffer = { 0 };
const LGMP_STATUS status = lgmpHostStreamWriteAcquire(
state->hostToClientStream, &buffer);
if (status == LGMP_ERR_STREAM_FULL)
{
usleep(1000);
continue;
}
CHECK(status == LGMP_OK);
const uint32_t bytes = sizeof(*record) + record->length;
CHECK(buffer.capacity >= bytes);
memcpy(buffer.data, record, sizeof(*record));
if (record->length)
memcpy((uint8_t *)buffer.data + sizeof(*record), data,
record->length);
CHECK(lgmpHostStreamWriteCommit(state->hostToClientStream,
&buffer, bytes) == LGMP_OK);
return true;
}
return false;
}
static bool settleHostStream(TestState * state)
{
for (unsigned i = 0; i < TEST_QUIET_MS; ++i)
{
CHECK(hostProcess(state));
usleep(1000);
}
return true;
}
static bool expectPayloadQueueClean(TestState * state)
{
for (;;)
{
KVMFRClipboardMessage record;
size_t size = sizeof(record);
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
state->queue, &record, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
return true;
CHECK(status == LGMP_OK);
CHECK(size == sizeof(record));
CHECK(clientID == state->clientID);
CHECK(lgmpHostAckData(state->queue) == LGMP_OK);
CHECK(record.type == KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE);
}
}
static bool readClientStream(TestState * state,
KVMFRClipboardMessage * record, LGMPStreamBuffer * buffer,
const void ** data)
{
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
CHECK(expectPayloadQueueClean(state));
memset(buffer, 0, sizeof(*buffer));
const LGMP_STATUS status = lgmpHostStreamReadPeek(
state->clientToHostStream, buffer);
if (status == LGMP_ERR_STREAM_EMPTY)
{
usleep(1000);
continue;
}
CHECK(status == LGMP_OK);
CHECK(buffer->size >= sizeof(*record));
memcpy(record, buffer->data, sizeof(*record));
CHECK(buffer->size == sizeof(*record) + record->length);
*data = (const uint8_t *)buffer->data + sizeof(*record);
return true;
}
return false;
}
static bool releaseClientStream(TestState * state,
LGMPStreamBuffer * buffer)
{
CHECK(lgmpHostStreamReadRelease(
state->clientToHostStream, buffer) == LGMP_OK);
memset(buffer, 0, sizeof(*buffer));
return true;
}
static bool readClient(TestState * state, KVMFRClipboardMessage * record)
{
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
size_t size = sizeof(*record);
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
state->queue, record, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
{
usleep(1000);
continue;
}
CHECK(status == LGMP_OK);
CHECK(size == sizeof(*record));
CHECK(clientID == state->clientID);
CHECK(record->type != KVMFR_CLIPBOARD_MESSAGE_DATA);
CHECK(record->type != KVMFR_CLIPBOARD_MESSAGE_FILE_DATA);
CHECK(lgmpHostAckData(state->queue) == LGMP_OK);
return true;
}
return false;
}
static bool readType(TestState * state, KVMFRClipboardMessageType type,
KVMFRClipboardMessage * record)
{
for (unsigned i = 0; i < 32; ++i)
{
CHECK(readClient(state, record));
if (record->type == type)
return true;
}
return false;
}
static bool readNonKeepalive(TestState * state,
KVMFRClipboardMessage * record)
{
for (unsigned i = 0; i < 32; ++i)
{
CHECK(readClient(state, record));
if (record->type != KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE)
return true;
}
return false;
}
static bool noClientControl(TestState * state)
{
for (unsigned i = 0; i < TEST_QUIET_MS; ++i)
{
CHECK(hostProcess(state));
KVMFRClipboardMessage record;
size_t size = sizeof(record);
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
state->queue, &record, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
{
usleep(1000);
continue;
}
if (status == LGMP_OK)
lgmpHostAckData(state->queue);
CHECK(status == LGMP_ERR_QUEUE_EMPTY);
}
return true;
}
static bool claim(TestState * state, uint32_t endpointGeneration,
bool autoAttach, KVMFRClipboardMessage * result)
{
atomic_store(&state->autoAttach, autoAttach);
CHECK(postStatus(state, endpointGeneration, 0, 0, true, 0));
if (autoAttach)
{
for (unsigned i = 0; i < TEST_WAIT_MS &&
!atomic_load(&state->events.attached); ++i)
{
CHECK(hostProcess(state));
usleep(1000);
}
CHECK(atomic_load(&state->events.attached));
}
else
{
CHECK(state->ops->attach(
state->clipboard, &EVENT_OPS, &state->events));
atomic_store(&state->events.attached, true);
}
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, result));
CHECK(result->version == KVMFR_CLIPBOARD_VERSION);
CHECK(result->generation != 0);
CHECK(result->token == endpointGeneration);
CHECK(result->flags == 0);
return true;
}
static bool own(TestState * state, uint32_t endpointGeneration,
uint32_t claimGeneration)
{
if (!state->streamsBound)
{
CHECK(lgmpHostStreamBind(state->hostToClientStream,
state->clientID, NULL) == LGMP_OK);
if (lgmpHostStreamBind(state->clientToHostStream,
state->clientID, NULL) != LGMP_OK)
{
lgmpHostStreamUnbind(state->hostToClientStream);
return false;
}
state->streamsBound = true;
}
CHECK(postStatus(state, endpointGeneration, state->clientID,
claimGeneration, true, 0));
return true;
}
static KVMFRClipboardMessage hostRecord(
KVMFRClipboardMessageType type, uint32_t generation)
{
return (KVMFRClipboardMessage)
{
.version = KVMFR_CLIPBOARD_VERSION,
.type = type,
.generation = generation,
};
}
static bool offerAndRequest(TestState * state, uint32_t generation,
uint64_t clipboardGeneration, LG_ClipboardRequest appRequest,
KVMFRClipboardMessage * wireRequest)
{
KVMFRClipboardMessage offer =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, generation);
offer.clipboardGeneration = clipboardGeneration;
offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT |
KVMFR_CLIPBOARD_FORMAT_MASK_PNG;
const unsigned notices = atomic_load(&state->events.notice) + 1;
CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.notice, notices));
CHECK(state->events.noticeCount == 2);
CHECK(state->events.noticeTypes[0] == LG_CLIPBOARD_DATA_TEXT);
CHECK(state->events.noticeTypes[1] == LG_CLIPBOARD_DATA_PNG);
CHECK(!state->ops->request(state->clipboard,
appRequest + 1, LG_CLIPBOARD_DATA_JPEG));
CHECK(state->ops->request(state->clipboard,
appRequest, LG_CLIPBOARD_DATA_TEXT));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_REQUEST, wireRequest));
CHECK(kvmfrClipboardTransferFromClient(wireRequest->transfer));
CHECK(wireRequest->generation == generation);
CHECK(wireRequest->clipboardGeneration == clipboardGeneration);
CHECK(wireRequest->format == KVMFR_CLIPBOARD_FORMAT_TEXT);
return true;
}
static bool testClaim(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 10, true, &claimRecord));
CHECK(own(state, 10, claimRecord.generation));
const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
KVMFRClipboardMessage offer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer));
state->localClipboardGeneration = offer.clipboardGeneration;
CHECK(offer.generation == claimRecord.generation);
CHECK(offer.clipboardGeneration != 0);
CHECK(offer.token == KVMFR_CLIPBOARD_FORMAT_MASK_TEXT);
return true;
}
static bool testOfferRequest(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 20, false, &claimRecord));
CHECK(own(state, 20, claimRecord.generation));
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
200, 77, &requestRecord));
return true;
}
static bool testInboundStream(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 30, false, &claimRecord));
CHECK(own(state, 30, claimRecord.generation));
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
300, 88, &requestRecord));
const uint8_t bytes[] = { 1, 2, 3, 4, 5 };
KVMFRClipboardMessage data =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
data.clipboardGeneration = 300;
data.transfer = requestRecord.transfer;
data.size = 5;
data.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN;
CHECK(postStreamRecord(state, &data, NULL));
CHECK(waitAtomic(state, &state->events.dataBegin, 1));
CHECK(atomic_load(&state->events.dataBegin) == 1);
CHECK(atomic_load(&state->events.dataChunk) == 0);
CHECK(state->events.streamID == 88);
CHECK(state->events.sizeHint == 5);
data.sequence = 1;
data.size = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
data.flags = 0;
data.length = sizeof(bytes);
CHECK(postStreamRecord(state, &data, bytes));
CHECK(waitAtomic(state, &state->events.dataChunk, 1));
CHECK(state->events.chunkOffset == 0);
CHECK(state->events.chunkSize == sizeof(bytes));
CHECK(memcmp(state->events.chunkData, bytes, sizeof(bytes)) == 0);
data.sequence = 2;
data.offset = sizeof(bytes);
data.size = sizeof(bytes);
data.flags = KVMFR_CLIPBOARD_FLAG_END;
data.length = 0;
CHECK(postStreamRecord(state, &data, NULL));
CHECK(waitAtomic(state, &state->events.dataEnd, 1));
CHECK(atomic_load(&state->events.dataChunk) == 1);
CHECK(state->events.finalSize == 5);
KVMFRClipboardMessage request2;
CHECK(offerAndRequest(state, claimRecord.generation,
301, 89, &request2));
data = hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA,
claimRecord.generation);
data.clipboardGeneration = 301;
data.transfer = request2.transfer;
data.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
CHECK(postStreamRecord(state, &data, NULL));
CHECK(waitAtomic(state, &state->events.dataEnd, 2));
CHECK(atomic_load(&state->events.dataBegin) == 2);
CHECK(state->events.finalSize == 0);
return true;
}
static bool prepareOutbound(TestState * state, uint32_t endpoint,
KVMFRClipboardMessage * claimRecord, uint64_t transfer)
{
CHECK(claim(state, endpoint, false, claimRecord));
CHECK(own(state, endpoint, claimRecord->generation));
const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
KVMFRClipboardMessage offer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer));
state->localClipboardGeneration = offer.clipboardGeneration;
KVMFRClipboardMessage requestRecord =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
claimRecord->generation);
requestRecord.clipboardGeneration = offer.clipboardGeneration;
requestRecord.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | transfer;
requestRecord.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
const unsigned requests = atomic_load(&state->events.request) + 1;
CHECK(postRecord(state, &requestRecord,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.request, requests));
CHECK(state->events.requestID == requestRecord.transfer);
CHECK(state->events.requestType == LG_CLIPBOARD_DATA_TEXT);
return true;
}
static bool checkDataRecord(TestState * state,
KVMFRClipboardMessage * record, LGMPStreamBuffer * buffer,
const void ** data)
{
CHECK(readClientStream(state, record, buffer, data));
CHECK(record->type == KVMFR_CLIPBOARD_MESSAGE_DATA);
CHECK(record->version == KVMFR_CLIPBOARD_VERSION);
return true;
}
static bool checkFileDataRecord(TestState * state,
KVMFRClipboardFileOperation operation,
KVMFRClipboardMessage * record, LGMPStreamBuffer * buffer,
const void ** data)
{
CHECK(readClientStream(state, record, buffer, data));
CHECK(record->type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA);
CHECK(record->version == KVMFR_CLIPBOARD_VERSION);
CHECK(record->token == operation);
return true;
}
static bool testOutboundStream(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(prepareOutbound(state, 40, &claimRecord, 1));
const LG_ClipboardRequest transfer = state->events.requestID;
CHECK(state->ops->dataBegin(state->clipboard, transfer,
LG_CLIPBOARD_DATA_TEXT, 5) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(noClientControl(state));
const uint8_t first[] = { 1, 2, 3 };
CHECK(state->ops->dataChunk(state->clipboard, transfer,
0, first, sizeof(first)) == LG_CLIPBOARD_RESULT_ACCEPTED);
KVMFRClipboardMessage record;
LGMPStreamBuffer buffer;
const void * data = NULL;
CHECK(checkDataRecord(state, &record, &buffer, &data));
CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_BEGIN);
CHECK(record.sequence == 0 && record.offset == 0 && record.size == 5);
CHECK(memcmp(data, first, sizeof(first)) == 0);
CHECK(releaseClientStream(state, &buffer));
const uint8_t second[] = { 4, 5 };
CHECK(state->ops->dataChunk(state->clipboard, transfer,
3, second, sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(checkDataRecord(state, &record, &buffer, &data));
CHECK(record.flags == 0 && record.sequence == 1 && record.offset == 3);
CHECK(record.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN);
CHECK(memcmp(data, second, sizeof(second)) == 0);
CHECK(releaseClientStream(state, &buffer));
CHECK(state->ops->dataEnd(state->clipboard, transfer, 5) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(checkDataRecord(state, &record, &buffer, &data));
CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_END);
CHECK(record.sequence == 2 && record.offset == 5 && record.size == 5);
CHECK(record.length == 0);
CHECK(releaseClientStream(state, &buffer));
KVMFRClipboardMessage requestRecord =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
claimRecord.generation);
requestRecord.clipboardGeneration = record.clipboardGeneration;
requestRecord.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 2;
requestRecord.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
CHECK(postRecord(state, &requestRecord,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.request, 2));
CHECK(state->ops->dataBegin(state->clipboard, requestRecord.transfer,
LG_CLIPBOARD_DATA_TEXT, 0) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(state->ops->dataEnd(state->clipboard,
requestRecord.transfer, 0) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(checkDataRecord(state, &record, &buffer, &data));
CHECK(record.flags == (KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END));
CHECK(record.sequence == 0 && record.offset == 0 && record.size == 0);
CHECK(releaseClientStream(state, &buffer));
return true;
}
static bool testBlocked(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(prepareOutbound(state, 50, &claimRecord, 3));
const LG_ClipboardRequest transfer = state->events.requestID;
CHECK(state->ops->dataBegin(state->clipboard, transfer,
LG_CLIPBOARD_DATA_TEXT, 5) == LG_CLIPBOARD_RESULT_ACCEPTED);
const uint8_t bytes[] = { 8, 9, 10, 11, 12 };
for (unsigned i = 0; i < KVMFR_CLIPBOARD_STREAM_SLOT_COUNT; ++i)
CHECK(state->ops->dataChunk(state->clipboard, transfer,
i, &bytes[i], 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(state->ops->dataChunk(state->clipboard, transfer,
4, &bytes[4], 1) == LG_CLIPBOARD_RESULT_BLOCKED);
KVMFRClipboardMessage record;
LGMPStreamBuffer buffer;
const void * data = NULL;
CHECK(checkDataRecord(state, &record, &buffer, &data));
CHECK(record.sequence == 0 && record.offset == 0 && record.length == 1);
CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_BEGIN);
CHECK(*(const uint8_t *)data == bytes[0]);
CHECK(releaseClientStream(state, &buffer));
CHECK(waitAtomic(state, &state->events.dataReady, 1));
CHECK(state->events.streamID == transfer);
CHECK(state->ops->dataChunk(state->clipboard, transfer,
4, &bytes[4], 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
for (unsigned i = 1; i < 5; ++i)
{
CHECK(checkDataRecord(state, &record, &buffer, &data));
CHECK(record.sequence == i && record.offset == i &&
record.length == 1);
CHECK(*(const uint8_t *)data == bytes[i]);
CHECK(releaseClientStream(state, &buffer));
}
CHECK(state->ops->dataEnd(state->clipboard, transfer, 5) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(checkDataRecord(state, &record, &buffer, &data));
CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_END);
CHECK(record.sequence == 5 && record.offset == 5 && record.size == 5);
CHECK(releaseClientStream(state, &buffer));
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
501, 5001, &requestRecord));
atomic_store(&state->events.blockChunkOnce, true);
KVMFRClipboardMessage wire =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
wire.clipboardGeneration = 501;
wire.transfer = requestRecord.transfer;
wire.size = 1;
wire.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
wire.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
wire.length = 1;
CHECK(postStreamRecord(state, &wire, bytes));
CHECK(waitAtomic(state, &state->events.dataChunk, 1));
CHECK(atomic_load(&state->events.dataEnd) == 0);
CHECK(state->ops->dataReady(state->clipboard, 5001));
CHECK(waitAtomic(state, &state->events.dataEnd, 1));
CHECK(atomic_load(&state->events.dataChunk) == 2);
return true;
}
static bool testCancel(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(prepareOutbound(state, 60, &claimRecord, 4));
const LG_ClipboardRequest outbound = state->events.requestID;
KVMFRClipboardMessage cancel =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_CANCEL, claimRecord.generation);
cancel.clipboardGeneration = state->localClipboardGeneration;
cancel.transfer = outbound;
cancel.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
cancel.token = LG_CLIPBOARD_CANCEL_ABORTED;
CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.requestCancel, 1));
CHECK(state->events.cancelID == outbound);
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
601, 6001, &requestRecord));
cancel.clipboardGeneration = 601;
cancel.transfer = requestRecord.transfer;
cancel.token = LG_CLIPBOARD_CANCEL_REPLACED;
CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.dataCancel, 1));
CHECK(state->events.cancelID == 6001);
CHECK(state->events.cancelReason == LG_CLIPBOARD_CANCEL_REPLACED);
KVMFRClipboardMessage outboundRequest =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
claimRecord.generation);
outboundRequest.clipboardGeneration = state->localClipboardGeneration;
outboundRequest.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 5;
outboundRequest.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
CHECK(postRecord(state, &outboundRequest,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.request, 2));
CHECK(state->ops->dataCancel(state->clipboard,
outboundRequest.transfer, LG_CLIPBOARD_CANCEL_ABORTED));
KVMFRClipboardMessage wireCancel;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CANCEL, &wireCancel));
CHECK(wireCancel.transfer == outboundRequest.transfer);
CHECK(wireCancel.clipboardGeneration ==
state->localClipboardGeneration);
CHECK(wireCancel.format == KVMFR_CLIPBOARD_FORMAT_TEXT);
CHECK(wireCancel.token == LG_CLIPBOARD_CANCEL_ABORTED);
return true;
}
static bool testRestart(TestState * state)
{
KVMFRClipboardMessage oldClaim;
CHECK(claim(state, 70, false, &oldClaim));
CHECK(own(state, 70, oldClaim.generation));
const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
KVMFRClipboardMessage oldOffer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &oldOffer));
CHECK(postStatus(state, 71, 0, 0, true, 0));
KVMFRClipboardMessage newClaim;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, &newClaim));
CHECK(newClaim.generation != oldClaim.generation);
CHECK(newClaim.token == 71);
KVMFRClipboardMessage replay;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &replay));
CHECK(replay.generation == newClaim.generation);
CHECK(replay.clipboardGeneration == oldOffer.clipboardGeneration);
CHECK(own(state, 71, newClaim.generation));
CHECK(postStatus(state, 71, 0, 0, true, 0));
KVMFRClipboardMessage reclaimed;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, &reclaimed));
CHECK(reclaimed.generation != newClaim.generation);
CHECK(reclaimed.token == 71);
return true;
}
static bool testFileLimits(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 75, false, &claimRecord));
CHECK(own(state, 75, claimRecord.generation));
const uint64_t localDataset = UINT64_C(0x1234000012340001);
const uint64_t remoteDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x5678000056780001);
CHECK(state->ops->offerFiles(state->clipboard, localDataset));
KVMFRClipboardMessage wire;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &wire));
CHECK(wire.clipboardGeneration == localDataset);
KVMFRClipboardMessage offer =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation);
offer.clipboardGeneration = remoteDataset;
offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.notice, 1));
CHECK(atomic_load(&state->events.fileOffer) == 1);
CHECK(atomic_load(&state->events.fileOfferBeforeNotice));
CHECK(state->events.fileDataset == remoteDataset);
KVMFRClipboardMessage staleAcquire =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE,
claimRecord.generation);
staleAcquire.clipboardGeneration = localDataset + 1U;
staleAcquire.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x98);
staleAcquire.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &staleAcquire,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire));
CHECK(wire.clipboardGeneration == staleAcquire.clipboardGeneration);
CHECK(wire.transfer == staleAcquire.transfer);
CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_STALE);
CHECK(atomic_load(&state->events.fileAcquire) == 0);
KVMFRClipboardMessage staleRequest =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST,
claimRecord.generation);
staleRequest.clipboardGeneration = localDataset;
staleRequest.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x99);
staleRequest.format = KVMFR_CLIPBOARD_FORMAT_FILES;
staleRequest.token = KVMFR_CLIPBOARD_FILE_OP_LIST;
CHECK(postRecord(state, &staleRequest,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL, &wire));
CHECK(wire.clipboardGeneration == staleRequest.clipboardGeneration);
CHECK(wire.transfer == staleRequest.transfer);
CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_STALE);
CHECK(atomic_load(&state->events.fileRequest) == 0);
for (uint64_t i = 0; i < 4; ++i)
{
const uint64_t acquisition = UINT64_C(0x100) + i;
CHECK(state->ops->fileAcquire(
state->clipboard, remoteDataset, acquisition));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire));
CHECK(wire.clipboardGeneration == remoteDataset);
CHECK(wire.transfer == acquisition);
KVMFRClipboardMessage acquired =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
claimRecord.generation);
acquired.clipboardGeneration = remoteDataset;
acquired.transfer = acquisition;
acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &acquired,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileAcquired, (unsigned)i + 1));
CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_NONE);
}
for (uint64_t i = 0; i < 4; ++i)
{
const uint64_t acquisition = KVMFR_CLIPBOARD_TRANSFER_HELPER |
(UINT64_C(0x200) + i);
KVMFRClipboardMessage acquire =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE,
claimRecord.generation);
acquire.clipboardGeneration = localDataset;
acquire.transfer = acquisition;
acquire.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &acquire,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileAcquire, (unsigned)i + 1));
CHECK(state->events.fileDataset == localDataset);
CHECK(state->events.fileAcquisition == acquisition);
CHECK(state->ops->fileAcquired(state->clipboard,
localDataset, acquisition, LG_CLIPBOARD_FILE_ERROR_NONE));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire));
CHECK(wire.transfer == acquisition);
CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE);
}
CHECK(!state->ops->fileAcquire(
state->clipboard, remoteDataset, UINT64_C(0x999)));
KVMFRClipboardMessage overflowAcquire =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE,
claimRecord.generation);
overflowAcquire.clipboardGeneration = localDataset;
overflowAcquire.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x999);
overflowAcquire.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &overflowAcquire,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(readNonKeepalive(state, &wire));
CHECK(wire.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED);
CHECK(wire.transfer == overflowAcquire.transfer);
CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE);
CHECK(atomic_load(&state->events.fileAcquire) == 4);
for (uint64_t i = 0; i < 16; ++i)
{
const LG_ClipboardFileRequest request =
{
.dataset = remoteDataset,
.request = UINT64_C(0x1000) + i,
.operation = LG_CLIPBOARD_FILE_LIST,
};
CHECK(state->ops->fileRequest(state->clipboard, &request));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire));
CHECK(wire.clipboardGeneration == remoteDataset);
CHECK(wire.transfer == request.request);
CHECK(wire.token == KVMFR_CLIPBOARD_FILE_OP_LIST);
}
for (uint64_t i = 0; i < 16; ++i)
{
KVMFRClipboardMessage request =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST,
claimRecord.generation);
request.clipboardGeneration = localDataset;
request.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER |
(UINT64_C(0x2000) + i);
request.format = KVMFR_CLIPBOARD_FORMAT_FILES;
request.token = KVMFR_CLIPBOARD_FILE_OP_LIST;
CHECK(postRecord(state, &request,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileRequest,
(unsigned)i + 1));
CHECK(state->events.fileRequestValue.dataset == localDataset);
CHECK(state->events.fileRequestValue.request == request.transfer);
}
const LG_ClipboardFileRequest overflowRequest =
{
.dataset = remoteDataset,
.request = UINT64_C(0x9999),
.operation = LG_CLIPBOARD_FILE_LIST,
};
CHECK(!state->ops->fileRequest(state->clipboard, &overflowRequest));
KVMFRClipboardMessage inboundOverflow =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST,
claimRecord.generation);
inboundOverflow.clipboardGeneration = localDataset;
inboundOverflow.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x9999);
inboundOverflow.format = KVMFR_CLIPBOARD_FORMAT_FILES;
inboundOverflow.token = KVMFR_CLIPBOARD_FILE_OP_LIST;
CHECK(postRecord(state, &inboundOverflow,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(readNonKeepalive(state, &wire));
CHECK(wire.type == KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL);
CHECK(wire.transfer == inboundOverflow.transfer);
CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE);
CHECK(atomic_load(&state->events.fileRequest) == 16);
return true;
}
static bool testFileOwnerLoss(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 79, false, &claimRecord));
CHECK(own(state, 79, claimRecord.generation));
const uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x7900000179000001);
KVMFRClipboardMessage offer =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation);
offer.clipboardGeneration = dataset;
offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileOffer, 1));
const uint64_t acquisition = UINT64_C(0x7901);
CHECK(state->ops->fileAcquire(
state->clipboard, dataset, acquisition));
KVMFRClipboardMessage wire;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire));
KVMFRClipboardMessage acquired =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
claimRecord.generation);
acquired.clipboardGeneration = dataset;
acquired.transfer = acquisition;
acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &acquired,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileAcquired, 1));
const LG_ClipboardFileRequest request =
{
.dataset = dataset,
.request = UINT64_C(0x7902),
.operation = LG_CLIPBOARD_FILE_LIST,
};
CHECK(state->ops->fileRequest(state->clipboard, &request));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire));
const unsigned cancellations =
atomic_load(&state->events.fileCancel);
CHECK(postStatus(state, 80, 0, 0, true, 0));
CHECK(waitAtomic(state, &state->events.fileCancel,
cancellations + 2));
CHECK(state->events.fileDataset == dataset);
CHECK(state->events.fileAcquisition == acquisition);
CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_DISCONNECTED);
return true;
}
static bool testMalformedFileData(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 81, false, &claimRecord));
CHECK(own(state, 81, claimRecord.generation));
const uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x8100000181000001);
KVMFRClipboardMessage offer =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation);
offer.clipboardGeneration = dataset;
offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileOffer, 1));
const uint64_t acquisition = UINT64_C(0x8101);
CHECK(state->ops->fileAcquire(state->clipboard, dataset, acquisition));
KVMFRClipboardMessage wire;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &wire));
KVMFRClipboardMessage acquired =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
claimRecord.generation);
acquired.clipboardGeneration = dataset;
acquired.transfer = acquisition;
acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &acquired,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileAcquired, 1));
const LG_ClipboardFileRequest request =
{
.dataset = dataset,
.request = UINT64_C(0x8102),
.node = 2,
.length = 4,
.operation = LG_CLIPBOARD_FILE_READ,
};
CHECK(state->ops->fileRequest(state->clipboard, &request));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST, &wire));
KVMFRClipboardMessage malformed =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_DATA,
claimRecord.generation);
malformed.clipboardGeneration = dataset;
malformed.transfer = request.request;
malformed.offset = 1;
malformed.size = request.length;
malformed.format = KVMFR_CLIPBOARD_FORMAT_FILES;
malformed.flags = KVMFR_CLIPBOARD_FLAG_BEGIN;
malformed.length = 1;
malformed.token = KVMFR_CLIPBOARD_FILE_OP_READ;
const uint8_t byte = 0;
const unsigned cancellations =
atomic_load(&state->events.fileCancel);
CHECK(postStreamRecord(state, &malformed, &byte));
CHECK(waitAtomic(state, &state->events.fileCancel, cancellations + 1));
CHECK(state->events.fileDataset == dataset);
CHECK(state->events.fileAcquisition == request.request);
CHECK(state->events.fileError == LG_CLIPBOARD_FILE_ERROR_INVALID);
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL, &wire));
CHECK(wire.clipboardGeneration == dataset);
CHECK(wire.transfer == request.request);
CHECK(wire.token == KVMFR_CLIPBOARD_FILE_ERROR_INVALID);
CHECK(!state->ops->fileCancel(state->clipboard,
dataset, request.request, LG_CLIPBOARD_FILE_ERROR_CANCELLED));
malformed.transfer = request.request + 1U;
CHECK(postStreamRecord(state, &malformed, &byte));
CHECK(settleHostStream(state));
CHECK(atomic_load(&state->events.fileCancel) == cancellations + 1);
return true;
}
static bool testFileStream(TestState * state)
{
static uint8_t bytes[KVMFR_CLIPBOARD_FILE_READ_BYTES];
for (size_t i = 0; i < sizeof(bytes); ++i)
bytes[i] = (uint8_t)(i * 131U + 17U);
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 78, false, &claimRecord));
CHECK(own(state, 78, claimRecord.generation));
const uint64_t dataset = UINT64_C(0x7800000178000001);
CHECK(state->ops->offerFiles(state->clipboard, dataset));
KVMFRClipboardMessage wire;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &wire));
const uint64_t acquisition = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x7801);
KVMFRClipboardMessage acquire =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE,
claimRecord.generation);
acquire.clipboardGeneration = dataset;
acquire.transfer = acquisition;
acquire.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &acquire,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileAcquire, 1));
CHECK(state->ops->fileAcquired(state->clipboard,
dataset, acquisition, LG_CLIPBOARD_FILE_ERROR_NONE));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED, &wire));
KVMFRClipboardMessage request =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST,
claimRecord.generation);
request.clipboardGeneration = dataset;
request.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x7802);
request.offset = 123;
request.size = 42;
request.format = KVMFR_CLIPBOARD_FORMAT_FILES;
request.flags = sizeof(bytes);
request.token = KVMFR_CLIPBOARD_FILE_OP_READ;
CHECK(postRecord(state, &request,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileRequest, 1));
const LG_ClipboardFileRequest descriptor =
state->events.fileRequestValue;
CHECK(descriptor.dataset == dataset);
CHECK(descriptor.request == request.transfer);
CHECK(descriptor.node == 42);
CHECK(descriptor.offset == 123);
CHECK(descriptor.length == sizeof(bytes));
CHECK(descriptor.operation == LG_CLIPBOARD_FILE_READ);
CHECK(state->ops->fileDataBegin(state->clipboard,
&descriptor, sizeof(bytes)) == LG_CLIPBOARD_RESULT_ACCEPTED);
for (unsigned i = 0; i < KVMFR_CLIPBOARD_STREAM_SLOT_COUNT; ++i)
{
const uint64_t offset =
(uint64_t)i * KVMFR_CLIPBOARD_STREAM_SLOT_BYTES;
CHECK(state->ops->fileDataChunk(state->clipboard,
&descriptor, offset, bytes + offset,
KVMFR_CLIPBOARD_STREAM_SLOT_BYTES,
i + 1 == KVMFR_CLIPBOARD_STREAM_SLOT_COUNT) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
}
KVMFRClipboardMessage record;
LGMPStreamBuffer buffer;
const void * data = NULL;
for (unsigned i = 0; i < KVMFR_CLIPBOARD_STREAM_SLOT_COUNT; ++i)
{
const uint64_t offset =
(uint64_t)i * KVMFR_CLIPBOARD_STREAM_SLOT_BYTES;
CHECK(checkFileDataRecord(state, KVMFR_CLIPBOARD_FILE_OP_READ,
&record, &buffer, &data));
CHECK(record.offset == offset && record.sequence == i);
CHECK(record.length == KVMFR_CLIPBOARD_STREAM_SLOT_BYTES);
if (i == 0)
{
CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_BEGIN);
CHECK(record.size == sizeof(bytes));
}
else if (i + 1 == KVMFR_CLIPBOARD_STREAM_SLOT_COUNT)
{
CHECK(record.flags == KVMFR_CLIPBOARD_FLAG_END);
CHECK(record.size == sizeof(bytes));
}
else
{
CHECK(record.flags == 0);
CHECK(record.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN);
}
CHECK(memcmp(data, bytes + offset,
KVMFR_CLIPBOARD_STREAM_SLOT_BYTES) == 0);
CHECK(releaseClientStream(state, &buffer));
}
CHECK(state->ops->fileDataEnd(state->clipboard,
&descriptor, sizeof(bytes)) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(noClientControl(state));
const unsigned requests = atomic_load(&state->events.fileRequest);
request.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(0x7803);
request.offset = 0;
request.size = KVMFR_CLIPBOARD_FILE_ROOT_NODE;
request.flags = 0;
request.token = KVMFR_CLIPBOARD_FILE_OP_LIST;
CHECK(postRecord(state, &request,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileRequest, requests + 1));
const LG_ClipboardFileRequest empty = state->events.fileRequestValue;
CHECK(empty.operation == LG_CLIPBOARD_FILE_LIST);
CHECK(state->ops->fileDataBegin(state->clipboard,
&empty, KVMFR_CLIPBOARD_SIZE_UNKNOWN) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(state->ops->fileDataEnd(state->clipboard,
&empty, 0) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(checkFileDataRecord(state, KVMFR_CLIPBOARD_FILE_OP_LIST,
&record, &buffer, &data));
CHECK(record.flags == (KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END));
CHECK(record.offset == 0 && record.sequence == 0 && record.size == 0);
CHECK(record.length == 0);
CHECK(releaseClientStream(state, &buffer));
return true;
}
static void * disconnectThread(void * opaque)
{
DisconnectTask * task = opaque;
lgmpClipboard_disconnect(task->clipboard);
atomic_store(&task->done, true);
return NULL;
}
static bool disconnectAndDrain(TestState * state,
uint32_t * releaseGeneration)
{
DisconnectTask task = { .clipboard = state->clipboard };
atomic_init(&task.done, false);
pthread_t thread;
CHECK(pthread_create(&thread, NULL, disconnectThread, &task) == 0);
bool valid = true;
for (unsigned i = 0; i < TEST_WAIT_MS && !atomic_load(&task.done); ++i)
{
if (lgmpHostProcess(state->host) != LGMP_OK)
valid = false;
for (;;)
{
KVMFRClipboardMessage record;
size_t size = sizeof(record);
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
state->queue, &record, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
break;
if (status != LGMP_OK)
{
valid = false;
break;
}
valid &= size == sizeof(record);
valid &= clientID == state->clientID;
if (record.type == KVMFR_CLIPBOARD_MESSAGE_RELEASE &&
releaseGeneration)
*releaseGeneration = record.generation;
if (lgmpHostAckData(state->queue) != LGMP_OK)
valid = false;
}
usleep(1000);
}
valid &= pthread_join(thread, NULL) == 0;
valid &= atomic_load(&task.done);
return valid;
}
static bool recreateClipboard(TestState * state)
{
state->ops->setStatusListener(state->clipboard, NULL, NULL);
if (atomic_load(&state->events.attached))
{
state->ops->detach(state->clipboard);
atomic_store(&state->events.attached, false);
KVMFRClipboardMessage release;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_RELEASE, &release));
CHECK(release.generation != 0);
const uint32_t endpointGeneration =
atomic_load(&state->statusGeneration);
CHECK(endpointGeneration != 0);
CHECK(postStatus(state, endpointGeneration, 0, 0, true, 0));
}
CHECK(disconnectAndDrain(state, NULL));
lgmpClipboard_destroy(&state->clipboard);
CHECK(lgmpClipboard_create(state->client, &state->clipboard));
CHECK(lgmpClipboard_connect(state->clipboard, state->clientID));
state->ops = lgmpClipboard_getOps();
CHECK(state->ops);
state->ops->setStatusListener(state->clipboard, statusChanged, state);
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
if (lgmpHostQueueHasSubs(state->queue))
return true;
usleep(1000);
}
return false;
}
static bool testProcessRestart(TestState * state)
{
KVMFRClipboardMessage oldClaim;
CHECK(claim(state, 76, false, &oldClaim));
CHECK(own(state, 76, oldClaim.generation));
const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
KVMFRClipboardMessage oldLocalOffer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &oldLocalOffer));
KVMFRClipboardMessage oldRequest;
CHECK(offerAndRequest(state, oldClaim.generation,
760, 7600, &oldRequest));
const uint64_t oldFileDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x76000001);
const uint64_t oldFileAcquisition = UINT64_C(0x76010001);
KVMFRClipboardMessage fileOffer =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, oldClaim.generation);
fileOffer.clipboardGeneration = oldFileDataset;
fileOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
const unsigned oldFileOffers = atomic_load(&state->events.fileOffer);
CHECK(postRecord(state, &fileOffer,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileOffer, oldFileOffers + 1));
CHECK(state->ops->fileAcquire(state->clipboard,
oldFileDataset, oldFileAcquisition));
KVMFRClipboardMessage fileWire;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &fileWire));
CHECK(recreateClipboard(state));
KVMFRClipboardMessage newClaim;
CHECK(claim(state, 77, false, &newClaim));
CHECK(own(state, 77, newClaim.generation));
CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
KVMFRClipboardMessage newLocalOffer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &newLocalOffer));
CHECK(newLocalOffer.clipboardGeneration !=
oldLocalOffer.clipboardGeneration);
KVMFRClipboardMessage newRequest;
CHECK(offerAndRequest(state, newClaim.generation,
770, 7700, &newRequest));
CHECK(newRequest.transfer != oldRequest.transfer);
const unsigned cancelCount = atomic_load(&state->events.dataCancel);
KVMFRClipboardMessage staleCancel =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_CANCEL, newClaim.generation);
staleCancel.clipboardGeneration = 760;
staleCancel.transfer = oldRequest.transfer;
staleCancel.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
staleCancel.token = LG_CLIPBOARD_CANCEL_ABORTED;
CHECK(postRecord(state, &staleCancel,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
CHECK(atomic_load(&state->events.dataCancel) == cancelCount);
const unsigned beginCount = atomic_load(&state->events.dataBegin);
KVMFRClipboardMessage staleData =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, newClaim.generation);
staleData.clipboardGeneration = 760;
staleData.transfer = oldRequest.transfer;
staleData.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
staleData.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
CHECK(postStreamRecord(state, &staleData, NULL));
CHECK(settleHostStream(state));
CHECK(atomic_load(&state->events.dataBegin) == beginCount);
KVMFRClipboardMessage data =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, newClaim.generation);
data.clipboardGeneration = 770;
data.transfer = newRequest.transfer;
data.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
const unsigned endCount = atomic_load(&state->events.dataEnd);
CHECK(postStreamRecord(state, &data, NULL));
CHECK(waitAtomic(state, &state->events.dataEnd, endCount + 1));
CHECK(atomic_load(&state->events.dataBegin) == beginCount + 1);
const uint64_t newFileDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x77000001);
const uint64_t newFileAcquisition = UINT64_C(0x77010001);
fileOffer = hostRecord(
KVMFR_CLIPBOARD_MESSAGE_OFFER, newClaim.generation);
fileOffer.clipboardGeneration = newFileDataset;
fileOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
const unsigned newFileOffers = atomic_load(&state->events.fileOffer);
CHECK(postRecord(state, &fileOffer,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileOffer, newFileOffers + 1));
CHECK(state->ops->fileAcquire(state->clipboard,
newFileDataset, newFileAcquisition));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE, &fileWire));
const unsigned acquiredCount =
atomic_load(&state->events.fileAcquired);
KVMFRClipboardMessage staleAcquired =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
newClaim.generation);
staleAcquired.clipboardGeneration = oldFileDataset;
staleAcquired.transfer = oldFileAcquisition;
staleAcquired.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &staleAcquired,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
CHECK(atomic_load(&state->events.fileAcquired) == acquiredCount);
KVMFRClipboardMessage staleFileCancel =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL,
newClaim.generation);
staleFileCancel.clipboardGeneration = oldFileDataset;
staleFileCancel.transfer = oldFileAcquisition;
staleFileCancel.format = KVMFR_CLIPBOARD_FORMAT_FILES;
staleFileCancel.token = KVMFR_CLIPBOARD_FILE_ERROR_STALE;
CHECK(postRecord(state, &staleFileCancel,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
CHECK(atomic_load(&state->events.fileCancel) == 0);
KVMFRClipboardMessage acquired =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED,
newClaim.generation);
acquired.clipboardGeneration = newFileDataset;
acquired.transfer = newFileAcquisition;
acquired.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &acquired,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.fileAcquired,
acquiredCount + 1));
CHECK(state->events.fileDataset == newFileDataset);
CHECK(state->events.fileAcquisition == newFileAcquisition);
return true;
}
static bool testDisconnect(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 80, false, &claimRecord));
CHECK(own(state, 80, claimRecord.generation));
uint32_t releaseGeneration = 0;
CHECK(disconnectAndDrain(state, &releaseGeneration));
CHECK(releaseGeneration == claimRecord.generation);
return true;
}
static bool testMalformed(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 90, false, &claimRecord));
CHECK(own(state, 90, claimRecord.generation));
const unsigned statusCount = atomic_load(&state->statusCount);
PLGMPMemory badStatusMemory;
CHECK(allocMemory(state, sizeof(KVMFRClipboardStatus),
&badStatusMemory));
KVMFRClipboardStatus * badStatus = lgmpHostMemPtr(badStatusMemory);
*badStatus = (KVMFRClipboardStatus)
{
.version = KVMFR_CLIPBOARD_VERSION,
.flags = KVMFR_CLIPBOARD_STATUS_AVAILABLE,
.generation = 91,
.lease = 1000,
};
CHECK(postMemory(state, badStatusMemory,
KVMFR_CLIPBOARD_QUEUE_STATUS, nextSerial(state)));
CHECK(waitMemory(state, badStatusMemory));
CHECK(atomic_load(&state->statusCount) == statusCount);
KVMFRClipboardMessage malformed =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation + 1);
malformed.clipboardGeneration = 900;
malformed.token = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT;
CHECK(postRecord(state, &malformed,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
CHECK(atomic_load(&state->events.notice) == 0);
malformed.generation = claimRecord.generation;
CHECK(postStreamRecord(state, &malformed, NULL));
CHECK(settleHostStream(state));
CHECK(atomic_load(&state->events.notice) == 0);
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
901, 9001, &requestRecord));
KVMFRClipboardMessage data =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
data.clipboardGeneration = 901;
data.transfer = requestRecord.transfer;
data.offset = 1;
data.size = 1;
data.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
CHECK(postStreamRecord(state, &data, NULL));
CHECK(settleHostStream(state));
CHECK(atomic_load(&state->events.dataBegin) == 0);
const uint8_t byte = 42;
data.offset = 0;
data.length = 1;
CHECK(postStreamRecord(state, &data, &byte));
CHECK(waitAtomic(state, &state->events.dataEnd, 1));
CHECK(atomic_load(&state->events.dataBegin) == 1);
CHECK(atomic_load(&state->events.dataChunk) == 1);
CHECK(state->events.streamID == 9001);
CHECK(state->events.chunkData[0] == byte);
return true;
}
static bool testFileFormatIsolation(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 91, false, &claimRecord));
CHECK(own(state, 91, claimRecord.generation));
const LG_ClipboardData files[] = { LG_CLIPBOARD_DATA_FILES };
CHECK(!state->ops->notifyTypes(state->clipboard, files, 1));
CHECK(noClientControl(state));
const uint64_t localDataset = UINT64_C(0x123456789abcdef);
CHECK(state->ops->offerFiles(state->clipboard, localDataset));
KVMFRClipboardMessage localOffer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &localOffer));
CHECK(localOffer.clipboardGeneration == localDataset);
KVMFRClipboardMessage request =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST, claimRecord.generation);
request.clipboardGeneration = localDataset;
request.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1);
request.format = KVMFR_CLIPBOARD_FORMAT_FILES;
CHECK(postRecord(state, &request, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
CHECK(atomic_load(&state->events.request) == 0);
CHECK(state->ops->dataBegin(state->clipboard, request.transfer,
LG_CLIPBOARD_DATA_FILES, 0) == LG_CLIPBOARD_RESULT_FAILED);
KVMFRClipboardMessage remoteOffer =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation);
remoteOffer.clipboardGeneration = UINT64_C(0x2222333344445555);
remoteOffer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
CHECK(postRecord(state, &remoteOffer,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.notice, 1));
CHECK(state->events.noticeCount == 1);
CHECK(state->events.noticeTypes[0] == LG_CLIPBOARD_DATA_FILES);
CHECK(!state->ops->request(state->clipboard,
100, LG_CLIPBOARD_DATA_FILES));
CHECK(noClientControl(state));
KVMFRClipboardMessage data =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
data.clipboardGeneration = remoteOffer.clipboardGeneration;
data.transfer = UINT64_C(0x2345);
data.format = KVMFR_CLIPBOARD_FORMAT_FILES;
data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
CHECK(postStreamRecord(state, &data, NULL));
CHECK(settleHostStream(state));
CHECK(atomic_load(&state->events.dataBegin) == 0);
return true;
}
static bool stateInit(TestState * state)
{
memset(state, 0, sizeof(*state));
state->memory = MAP_FAILED;
atomic_init(&state->statusCount, 0);
atomic_init(&state->statusAvailable, false);
atomic_init(&state->statusGeneration, 0);
atomic_init(&state->autoAttach, false);
atomic_init(&state->events.notice, 0);
atomic_init(&state->events.release, 0);
atomic_init(&state->events.request, 0);
atomic_init(&state->events.requestCancel, 0);
atomic_init(&state->events.dataBegin, 0);
atomic_init(&state->events.dataChunk, 0);
atomic_init(&state->events.dataEnd, 0);
atomic_init(&state->events.dataCancel, 0);
atomic_init(&state->events.dataReady, 0);
atomic_init(&state->events.fileOffer, 0);
atomic_init(&state->events.fileAcquire, 0);
atomic_init(&state->events.fileAcquired, 0);
atomic_init(&state->events.fileRequest, 0);
atomic_init(&state->events.fileCancel, 0);
atomic_init(&state->events.acceptRequest, true);
atomic_init(&state->events.blockChunkOnce, false);
atomic_init(&state->events.attached, false);
atomic_init(&state->events.fileOfferBeforeNotice, false);
state->memory = mmap(NULL, TEST_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
CHECK(state->memory != MAP_FAILED);
const uint32_t sessionData = UINT32_C(0x12345678);
CHECK(lgmpHostInit(state->memory, TEST_SHM_SIZE, &state->host,
sizeof(sessionData), (uint8_t *)&sessionData) == LGMP_OK);
const struct LGMPQueueConfig config =
{
.queueID = LGMP_Q_CLIPBOARD,
.numMessages = LGMP_Q_CLIPBOARD_LEN,
.subTimeout = 1000,
};
CHECK(lgmpHostQueueNew(state->host, config, &state->queue) == LGMP_OK);
CHECK(lgmpClientInit(state->memory, TEST_SHM_SIZE, &state->client) ==
LGMP_OK);
usleep(300000);
CHECK(hostProcess(state));
uint32_t dataSize;
uint32_t remoteVersion;
uint8_t * data;
CHECK(lgmpClientSessionInit(state->client, &dataSize, &data,
&state->clientID, &remoteVersion) == LGMP_OK);
CHECK(state->clientID != 0);
CHECK(dataSize == sizeof(sessionData));
CHECK(memcmp(data, &sessionData, sizeof(sessionData)) == 0);
const struct LGMPStreamConfig hostToClientConfig =
{
.direction = LGMP_STREAM_HOST_TO_CLIENT,
.policy = LGMP_STREAM_RELIABLE_FIFO,
.slotCount = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
.slotSize = SLOT_BYTES,
};
CHECK(lgmpHostStreamNew(state->host, hostToClientConfig,
&state->hostToClientStream) == LGMP_OK);
struct LGMPStreamDescriptor descriptor;
lgmpHostStreamGetDescriptor(state->hostToClientStream, &descriptor);
state->hostToClientDescriptor = exportStreamDescriptor(&descriptor);
const struct LGMPStreamConfig clientToHostConfig =
{
.direction = LGMP_STREAM_CLIENT_TO_HOST,
.policy = LGMP_STREAM_RELIABLE_FIFO,
.slotCount = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
.slotSize = SLOT_BYTES,
};
CHECK(lgmpHostStreamNew(state->host, clientToHostConfig,
&state->clientToHostStream) == LGMP_OK);
lgmpHostStreamGetDescriptor(state->clientToHostStream, &descriptor);
state->clientToHostDescriptor = exportStreamDescriptor(&descriptor);
CHECK(lgmpClipboard_create(state->client, &state->clipboard));
CHECK(lgmpClipboard_connect(state->clipboard, state->clientID));
state->ops = lgmpClipboard_getOps();
CHECK(state->ops);
state->ops->setStatusListener(
state->clipboard, statusChanged, state);
CHECK(atomic_load(&state->statusCount) == 1);
CHECK(!atomic_load(&state->statusAvailable));
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
if (lgmpHostQueueHasSubs(state->queue))
return true;
usleep(1000);
}
return false;
}
static void stateFree(TestState * state)
{
if (state->clipboard)
{
state->ops->setStatusListener(state->clipboard, NULL, NULL);
if (atomic_load(&state->events.attached))
state->ops->detach(state->clipboard);
disconnectAndDrain(state, NULL);
lgmpClipboard_destroy(&state->clipboard);
}
lgmpClientFree(&state->client);
for (unsigned i = 0; i < state->memoryCount; ++i)
lgmpHostMemFree(&state->memories[i]);
lgmpHostStreamFree(&state->clientToHostStream);
lgmpHostStreamFree(&state->hostToClientStream);
lgmpHostFree(&state->host);
if (state->memory != MAP_FAILED)
munmap(state->memory, TEST_SHM_SIZE);
}
typedef bool (*TestFn)(TestState * state);
static const struct
{
const char * name;
TestFn run;
}
TESTS[] =
{
{ "claim" , testClaim },
{ "offer-request" , testOfferRequest },
{ "inbound-stream" , testInboundStream },
{ "outbound-stream" , testOutboundStream },
{ "blocked" , testBlocked },
{ "cancel" , testCancel },
{ "restart" , testRestart },
{ "process-restart" , testProcessRestart },
{ "file-stream" , testFileStream },
{ "file-limits" , testFileLimits },
{ "file-owner-loss" , testFileOwnerLoss },
{ "file-malformed" , testMalformedFileData },
{ "disconnect" , testDisconnect },
{ "malformed" , testMalformed },
{ "file-format" , testFileFormatIsolation },
};
int main(int argc, char * argv[])
{
if (argc != 2)
{
fprintf(stderr, "usage: %s <test-case>\n", argv[0]);
return 2;
}
TestFn test = NULL;
for (unsigned i = 0; i < sizeof(TESTS) / sizeof(TESTS[0]); ++i)
if (strcmp(argv[1], TESTS[i].name) == 0)
{
test = TESTS[i].run;
break;
}
if (!test)
{
fprintf(stderr, "unknown test case: %s\n", argv[1]);
return 2;
}
debug_init();
TestState state = { 0 };
state.memory = MAP_FAILED;
const bool initialized = stateInit(&state);
const bool passed = initialized && test(&state);
stateFree(&state);
return passed ? 0 : 1;
}