/** * 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 "core/clipboard.h" #include "main.h" #include "test.h" #include "common/debug.h" #include #include #include #include #include #include #include #define MAX_CALL 16U #define MAX_DATA 32U #define TEST_ALARM_S 5U struct Provider { bool avail; bool attachOK; bool callOK; bool reqOK; uint32_t gen; LG_ClipboardStatusFn statFn; void * statCtx; const LG_ClipboardEventOps * ev; void * evCtx; const LG_ClipboardEventOps * oldEv; void * oldCtx; unsigned int statSet; unsigned int statClear; unsigned int attach; unsigned int detach; unsigned int release; unsigned int notice; unsigned int data; unsigned int begin; unsigned int chunk; unsigned int end; unsigned int cancel; unsigned int ready; unsigned int request; unsigned int fileOffer; uint64_t fileDataset[MAX_CALL]; LG_ClipboardData noticeTypes[LG_CLIPBOARD_DATA_NONE]; size_t noticeCount; LG_ClipboardRequest reqId[MAX_CALL]; LG_ClipboardData reqType[MAX_CALL]; LG_ClipboardRequest dataId[MAX_CALL]; LG_ClipboardData dataType[MAX_CALL]; size_t dataSize[MAX_CALL]; uint8_t dataBuf[MAX_CALL][MAX_DATA]; LG_ClipboardResult beginResult; LG_ClipboardResult chunkResult; LG_ClipboardResult endResult; LG_ClipboardRequest streamId; LG_ClipboardData streamType; uint64_t sizeHint; uint64_t chunkOffset[MAX_CALL]; size_t chunkSize[MAX_CALL]; uint8_t chunkBuf[MAX_CALL][MAX_DATA]; uint64_t finalSize; LG_ClipboardCancelReason cancelReason; bool earlyBegin; LG_ClipboardResult earlyResult; }; struct Display { unsigned int notice; unsigned int release; unsigned int request; unsigned int ready; unsigned int cancel; LG_ClipboardData noticeType[MAX_CALL]; LG_ClipboardRequest reqId[MAX_CALL]; LG_ClipboardData reqType[MAX_CALL]; bool autoData; LG_ClipboardData autoType; const void * autoBuf; size_t autoSize; bool autoBegin; LG_ClipboardResult readyResult; LG_ClipboardCancelReason cancelReason; atomic_bool blockCancel; atomic_bool cancelEntered; atomic_bool releaseCancel; }; struct Reply { unsigned int count; LG_ClipboardData type; uint32_t size; uint8_t data[MAX_DATA]; struct Reply * next; bool nested; }; struct StreamSink { unsigned int begin; unsigned int chunk; unsigned int end; unsigned int cancel; LG_ClipboardResult beginResult; LG_ClipboardResult chunkResult; LG_ClipboardResult endResult; LG_ClipboardData type; uint64_t sizeHint; uint64_t offset; uint64_t finalSize; LG_ClipboardCancelReason reason; LG_ClipboardRequest request; bool requireRequest; size_t size; uint8_t data[MAX_DATA]; }; static struct Provider p; static struct Provider q; static struct Provider r; static struct Display d; extern unsigned clipboardFilesStubRemoteOfferCount; extern unsigned clipboardFilesStubRemoteClearCount; extern uint64_t clipboardFilesStubRemoteDataset; extern bool clipboardFilesStubRemoteReady; extern bool clipboardFilesStubRemoteOfferResult; struct AppState g_state; struct AppParams g_params; static LG_ClipboardResult sinkBegin(void * opaque, LG_ClipboardData type, uint64_t sizeHint) { struct StreamSink * sink = opaque; if (sink->requireRequest) CHECK(sink->request != LG_CLIPBOARD_REQUEST_INVALID); ++sink->begin; sink->type = type; sink->sizeHint = sizeHint; return sink->beginResult; } static LG_ClipboardResult sinkChunk(void * opaque, uint64_t offset, const void * data, size_t size) { struct StreamSink * sink = opaque; ++sink->chunk; sink->offset = offset; if (sink->chunkResult == LG_CLIPBOARD_RESULT_ACCEPTED) { CHECK(offset + size <= MAX_DATA); memcpy(sink->data + offset, data, size); sink->size = offset + size; } return sink->chunkResult; } static LG_ClipboardResult sinkEnd(void * opaque, uint64_t finalSize) { struct StreamSink * sink = opaque; ++sink->end; sink->finalSize = finalSize; return sink->endResult; } static void sinkCancel(void * opaque, LG_ClipboardCancelReason reason) { struct StreamSink * sink = opaque; ++sink->cancel; sink->reason = reason; } static const LG_ClipboardStreamOps sinkOps = { .begin = sinkBegin, .chunk = sinkChunk, .end = sinkEnd, .cancel = sinkCancel, }; static void initSink(struct StreamSink * sink) { sink->beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; sink->chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; sink->endResult = LG_CLIPBOARD_RESULT_ACCEPTED; } static void setStat(void * opaque, LG_ClipboardStatusFn callback, void * callbackOpaque) { struct Provider * provider = opaque; provider->statFn = callback; provider->statCtx = callbackOpaque; if (!callback) { ++provider->statClear; return; } ++provider->statSet; const LG_ClipboardStatus status = { .available = provider->avail, .generation = provider->gen, }; callback(callbackOpaque, &status); } static bool attach(void * opaque, const LG_ClipboardEventOps * events, void * eventOpaque) { struct Provider * provider = opaque; ++provider->attach; provider->ev = events; provider->evCtx = eventOpaque; return provider->attachOK; } static void detach(void * opaque) { struct Provider * provider = opaque; ++provider->detach; provider->oldEv = provider->ev; provider->oldCtx = provider->evCtx; provider->ev = NULL; provider->evCtx = NULL; } static bool release(void * opaque) { struct Provider * provider = opaque; ++provider->release; return provider->callOK; } static bool notify(void * opaque, const LG_ClipboardData types[], size_t count) { struct Provider * provider = opaque; CHECK(types); CHECK(count <= LG_CLIPBOARD_DATA_NONE); ++provider->notice; provider->noticeCount = count; memcpy(provider->noticeTypes, types, count * sizeof(*types)); return provider->callOK; } static bool offerFiles(void * opaque, uint64_t dataset) { struct Provider * provider = opaque; CHECK(dataset); CHECK(provider->fileOffer < MAX_CALL); provider->fileDataset[provider->fileOffer++] = dataset; return provider->callOK; } static bool data(void * opaque, LG_ClipboardRequest id, LG_ClipboardData type, const void * buf, size_t size) { struct Provider * provider = opaque; CHECK(provider->data < MAX_CALL); const unsigned int no = provider->data++; provider->dataId[no] = id; provider->dataType[no] = type; provider->dataSize[no] = size; if (size) { CHECK(buf); CHECK(size <= MAX_DATA); memcpy(provider->dataBuf[no], buf, size); } return provider->callOK; } static LG_ClipboardResult dataBegin(void * opaque, LG_ClipboardRequest id, LG_ClipboardData type, uint64_t sizeHint) { struct Provider * provider = opaque; ++provider->begin; provider->streamId = id; provider->streamType = type; provider->sizeHint = sizeHint; return provider->beginResult; } static LG_ClipboardResult dataChunk(void * opaque, LG_ClipboardRequest id, uint64_t offset, const void * buf, size_t size) { struct Provider * provider = opaque; CHECK(provider->chunk < MAX_CALL); CHECK(id == provider->streamId); const unsigned int no = provider->chunk++; provider->chunkOffset[no] = offset; provider->chunkSize[no] = size; if (size <= MAX_DATA) memcpy(provider->chunkBuf[no], buf, size); return provider->chunkResult; } static LG_ClipboardResult dataEnd(void * opaque, LG_ClipboardRequest id, uint64_t finalSize) { struct Provider * provider = opaque; CHECK(id == provider->streamId); ++provider->end; provider->finalSize = finalSize; return provider->endResult; } static bool dataCancel(void * opaque, LG_ClipboardRequest id, LG_ClipboardCancelReason reason) { struct Provider * provider = opaque; ++provider->cancel; provider->streamId = id; provider->cancelReason = reason; return provider->callOK; } static bool dataReady(void * opaque, LG_ClipboardRequest id) { struct Provider * provider = opaque; ++provider->ready; provider->streamId = id; return provider->callOK; } struct EarlyRequest { struct Provider * provider; LG_ClipboardRequest request; LG_ClipboardData type; }; static void * earlyRequestThread(void * opaque) { struct EarlyRequest * early = opaque; early->provider->earlyResult = early->provider->ev->dataBegin( early->provider->evCtx, early->request, early->type, LG_CLIPBOARD_SIZE_UNKNOWN); return NULL; } static bool request(void * opaque, LG_ClipboardRequest id, LG_ClipboardData type) { struct Provider * provider = opaque; CHECK(provider->request < MAX_CALL); const unsigned int no = provider->request++; provider->reqId[no] = id; provider->reqType[no] = type; if (provider->earlyBegin) { struct EarlyRequest early = { .provider = provider, .request = id, .type = type, }; pthread_t thread; CHECK(pthread_create(&thread, NULL, earlyRequestThread, &early) == 0); CHECK(pthread_join(thread, NULL) == 0); } return provider->reqOK; } static const LG_ClipboardOps plainOps = { .name = "plain", .attach = attach, .detach = detach, .release = release, .notifyTypes = notify, .data = data, .request = request, }; static const LG_ClipboardOps statOps = { .name = "status", .setStatusListener = setStat, .attach = attach, .detach = detach, .release = release, .notifyTypes = notify, .data = data, .request = request, }; static const LG_ClipboardOps streamOps = { .name = "stream", .attach = attach, .detach = detach, .release = release, .notifyTypes = notify, .dataBegin = dataBegin, .dataChunk = dataChunk, .dataEnd = dataEnd, .dataCancel = dataCancel, .dataReady = dataReady, .request = request, }; static const LG_ClipboardOps fileOps = { .name = "files", .attach = attach, .detach = detach, .release = release, .notifyTypes = notify, .offerFiles = offerFiles, .data = data, .request = request, }; static void dsNotice(LG_ClipboardData type) { CHECK(d.notice < MAX_CALL); d.noticeType[d.notice++] = type; } static void dsRelease(void) { ++d.release; } static void dsRequest(LG_ClipboardRequest id, LG_ClipboardData type) { CHECK(d.request < MAX_CALL); const unsigned int no = d.request++; d.reqId[no] = id; d.reqType[no] = type; if (d.autoData) clipboard_data(id, d.autoType, d.autoBuf, d.autoSize); } static void dsRequestReady(LG_ClipboardRequest id) { ++d.ready; if (d.autoBegin) d.readyResult = clipboard_dataBegin( id, LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN); } static void dsRequestCancel(LG_ClipboardRequest id, LG_ClipboardCancelReason reason) { (void)id; if (atomic_load_explicit(&d.blockCancel, memory_order_acquire)) { atomic_store_explicit( &d.cancelEntered, true, memory_order_release); while (!atomic_load_explicit(&d.releaseCancel, memory_order_acquire)) usleep(1000); } ++d.cancel; d.cancelReason = reason; } static struct LG_DisplayServerOps dsOps = { .cbNotice = dsNotice, .cbRelease = dsRelease, .cbRequest = dsRequest, .cbRequestReady = dsRequestReady, .cbRequestCancel = dsRequestCancel, }; static void initProvider(struct Provider * provider) { provider->avail = true; provider->attachOK = true; provider->callOK = true; provider->reqOK = true; provider->gen = 1; provider->beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; provider->chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; provider->endResult = LG_CLIPBOARD_RESULT_ACCEPTED; } static void init(void) { initProvider(&p); initProvider(&q); initProvider(&r); atomic_init(&d.blockCancel, false); atomic_init(&d.cancelEntered, false); atomic_init(&d.releaseCancel, false); clipboardFilesStubRemoteOfferCount = 0; clipboardFilesStubRemoteClearCount = 0; clipboardFilesStubRemoteDataset = 0; clipboardFilesStubRemoteReady = false; clipboardFilesStubRemoteOfferResult = true; g_state.ds = &dsOps; g_params.clipboardToVM = true; g_params.clipboardToLocal = true; CHECK(clipboard_init()); clipboard_setLocalAvailable(true); } static void bind(struct Provider * provider) { clipboard_setFallback(&plainOps, provider); CHECK(provider->attach == 1); } static void setStatus(struct Provider * provider, bool avail, uint32_t gen) { CHECK(provider->statFn); provider->avail = avail; provider->gen = gen; const LG_ClipboardStatus status = { .available = avail, .generation = gen, }; provider->statFn(provider->statCtx, &status); } static void notice(struct Provider * provider, const LG_ClipboardData types[], size_t count) { CHECK(provider->ev); provider->ev->notice(provider->evCtx, types, count); } static void remoteData(struct Provider * provider, LG_ClipboardRequest id, LG_ClipboardData type, const void * buf, size_t size) { CHECK(provider->ev); provider->ev->data(provider->evCtx, id, type, buf, size); } static void onReply(void * opaque, LG_ClipboardData type, const uint8_t * buf, uint32_t size) { struct Reply * reply = opaque; ++reply->count; reply->type = type; reply->size = size; CHECK(size <= MAX_DATA); if (size) { CHECK(buf); memcpy(reply->data, buf, size); } else CHECK(!buf); if (type != LG_CLIPBOARD_DATA_NONE && reply->next) reply->nested = clipboard_request( LG_CLIPBOARD_DATA_TEXT, onReply, reply->next); } static void checkNone(const struct Reply * reply) { CHECK(reply->count == 1); CHECK(reply->type == LG_CLIPBOARD_DATA_NONE); CHECK(reply->size == 0); } static void testPreference(void) { init(); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; clipboard_notifyTypes(types, 1); clipboard_setFallback(&statOps, &p); CHECK(p.attach == 1); CHECK(p.notice == 1); clipboard_setTransport(&statOps, &q); CHECK(p.release == 1); CHECK(p.detach == 1); CHECK(q.attach == 1); CHECK(q.notice == 1); CHECK(p.oldEv); p.oldEv->notice(p.oldCtx, types, 1); p.oldEv->release(p.oldCtx); CHECK(!p.oldEv->request(p.oldCtx, 1, LG_CLIPBOARD_DATA_TEXT)); CHECK(d.notice == 0); CHECK(d.release == 0); const LG_ClipboardStatusFn staleFn = q.statFn; void * staleCtx = q.statCtx; CHECK(staleFn); clipboard_dropTransport(); CHECK(q.release == 0); CHECK(q.detach == 0); CHECK(p.attach == 2); const LG_ClipboardStatus stale = { .available = false, .generation = 2, }; staleFn(staleCtx, &stale); CHECK(p.attach == 2); clipboard_setTransport(&statOps, &r); CHECK(p.release == 2); CHECK(p.detach == 2); CHECK(r.attach == 1); clipboard_setTransport(NULL, NULL); CHECK(r.statClear == 1); CHECK(r.release == 1); CHECK(r.detach == 1); CHECK(p.attach == 3); clipboard_free(); } static void testFilePublication(void) { init(); clipboard_setFallback(&fileOps, &p); const uint64_t dataset = UINT64_C(0x123456789abcdef); clipboard_notifyFiles(dataset); CHECK(p.fileOffer == 1); CHECK(p.fileDataset[0] == dataset); const LG_ClipboardData files[] = { LG_CLIPBOARD_DATA_FILES }; clipboard_notifyTypes(files, 1); CHECK(p.notice == 0); CHECK(p.fileOffer == 1); clipboard_setTransport(&plainOps, &r); CHECK(r.attach == 1); CHECK(r.release == 1); const uint64_t replacement = dataset + 1U; clipboard_notifyFiles(replacement); CHECK(r.release == 2); clipboard_dropTransport(); CHECK(p.attach == 2); CHECK(p.fileOffer == 2); CHECK(p.fileDataset[1] == replacement); clipboard_setTransport(&fileOps, &q); CHECK(q.attach == 1); CHECK(q.fileOffer == 1); CHECK(q.fileDataset[0] == replacement); clipboard_dropTransport(); CHECK(p.attach == 3); CHECK(p.fileOffer == 3); CHECK(p.fileDataset[2] == replacement); clipboard_free(); } static void testFileReplacement(void) { init(); bind(&p); clipboardFilesStubRemoteReady = true; const uint64_t first = UINT64_C(0x2000000000000001); p.ev->fileOffer(p.evCtx, first); CHECK(clipboardFilesStubRemoteOfferCount == 1); CHECK(clipboardFilesStubRemoteDataset == first); const LG_ClipboardData files[] = { LG_CLIPBOARD_DATA_FILES }; p.ev->notice(p.evCtx, files, 1); CHECK(d.notice == 1); CHECK(d.noticeType[0] == LG_CLIPBOARD_DATA_FILES); const LG_ClipboardData text[] = { LG_CLIPBOARD_DATA_TEXT }; p.ev->notice(p.evCtx, text, 1); CHECK(clipboardFilesStubRemoteClearCount == 1); CHECK(clipboardFilesStubRemoteDataset == 0); CHECK(d.notice == 2); CHECK(d.noticeType[1] == LG_CLIPBOARD_DATA_TEXT); const uint64_t mixedDataset = first + 1U; p.ev->fileOffer(p.evCtx, mixedDataset); const LG_ClipboardData mixed[] = { LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_DATA_FILES, }; p.ev->notice(p.evCtx, mixed, 2); CHECK(clipboardFilesStubRemoteClearCount == 1); CHECK(clipboardFilesStubRemoteDataset == mixedDataset); CHECK(d.notice == 3); CHECK(d.noticeType[2] == LG_CLIPBOARD_DATA_FILES); p.ev->notice(p.evCtx, text, 1); CHECK(clipboardFilesStubRemoteClearCount == 2); CHECK(clipboardFilesStubRemoteDataset == 0); CHECK(d.notice == 4); CHECK(d.noticeType[3] == LG_CLIPBOARD_DATA_TEXT); clipboard_free(); } static void testFileFailure(void) { init(); bind(&p); const LG_ClipboardData text[] = { LG_CLIPBOARD_DATA_TEXT }; p.ev->notice(p.evCtx, text, 1); CHECK(d.notice == 1); CHECK(d.release == 0); const LG_ClipboardData files[] = { LG_CLIPBOARD_DATA_FILES }; const uint64_t first = UINT64_C(0x2100000000000001); p.ev->fileOffer(p.evCtx, first); p.ev->notice(p.evCtx, files, 1); CHECK(d.notice == 1); CHECK(d.release == 0); const uint64_t second = first + 1U; p.ev->fileOffer(p.evCtx, second); p.ev->notice(p.evCtx, files, 1); CHECK(d.notice == 1); CHECK(d.release == 0); clipboard_fileRemoteFailed(first); CHECK(d.release == 0); CHECK(clipboardFilesStubRemoteDataset == second); clipboard_fileRemoteFailed(second); CHECK(d.release == 1); clipboard_fileRemoteFailed(second); CHECK(d.release == 1); p.ev->notice(p.evCtx, text, 1); CHECK(d.notice == 2); clipboardFilesStubRemoteOfferResult = false; const unsigned previousClears = clipboardFilesStubRemoteClearCount; p.ev->fileOffer(p.evCtx, second + 1U); CHECK(clipboardFilesStubRemoteClearCount == previousClears + 1U); p.ev->notice(p.evCtx, files, 1); CHECK(d.notice == 2); CHECK(d.release == 2); p.ev->notice(p.evCtx, text, 1); CHECK(d.notice == 3); clipboardFilesStubRemoteOfferResult = true; const uint64_t pending = second + 2U; p.ev->fileOffer(p.evCtx, pending); clipboard_fileRemoteFailed(pending); CHECK(d.release == 2); p.ev->notice(p.evCtx, files, 1); CHECK(d.notice == 3); CHECK(d.release == 3); clipboard_free(); } struct CallbackSerializationTask { atomic_bool switched; atomic_bool availabilityChanged; }; static void * switchClipboardProvider(void * opaque) { struct CallbackSerializationTask * task = opaque; clipboard_setTransport(&plainOps, &q); atomic_store_explicit(&task->switched, true, memory_order_release); return NULL; } static void * clearClipboardAvailability(void * opaque) { struct CallbackSerializationTask * task = opaque; clipboard_setLocalAvailable(false); atomic_store_explicit( &task->availabilityChanged, true, memory_order_release); return NULL; } static void waitAtomicBool(const atomic_bool * value) { for (unsigned i = 0; i < 1000U; ++i) { if (atomic_load_explicit(value, memory_order_acquire)) return; usleep(1000); } CHECK(false); } static void testCallbackSerialization(void) { init(); bind(&p); const LG_ClipboardData text[] = { LG_CLIPBOARD_DATA_TEXT }; clipboard_notifyTypes(text, 1); CHECK(p.ev->request(p.evCtx, 1, LG_CLIPBOARD_DATA_TEXT)); CHECK(d.request == 1); atomic_store_explicit(&d.blockCancel, true, memory_order_release); struct CallbackSerializationTask task; atomic_init(&task.switched, false); atomic_init(&task.availabilityChanged, false); pthread_t switchThread; CHECK(pthread_create(&switchThread, NULL, switchClipboardProvider, &task) == 0); waitAtomicBool(&d.cancelEntered); pthread_t availabilityThread; CHECK(pthread_create(&availabilityThread, NULL, clearClipboardAvailability, &task) == 0); usleep(20000); CHECK(!atomic_load_explicit( &task.availabilityChanged, memory_order_acquire)); atomic_store_explicit(&d.releaseCancel, true, memory_order_release); CHECK(pthread_join(switchThread, NULL) == 0); CHECK(pthread_join(availabilityThread, NULL) == 0); CHECK(atomic_load_explicit(&task.switched, memory_order_acquire)); CHECK(atomic_load_explicit( &task.availabilityChanged, memory_order_acquire)); CHECK(d.cancel == 1); CHECK(d.cancelReason == LG_CLIPBOARD_CANCEL_UNAVAILABLE); clipboard_free(); } static void testRequest(void) { init(); bind(&p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_DATA_PNG }; notice(&p, types, 2); CHECK(d.notice == 1); CHECK(d.noticeType[0] == LG_CLIPBOARD_DATA_TEXT); struct Reply a = { 0 }; struct Reply b = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &a)); CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &b)); CHECK(p.request == 2); CHECK(p.reqId[0] != LG_CLIPBOARD_REQUEST_INVALID); CHECK(p.reqId[0] != p.reqId[1]); const uint8_t one[] = { 1, 2, 3 }; const uint8_t two[] = { 4, 5 }; remoteData(&p, p.reqId[1] + 100, LG_CLIPBOARD_DATA_TEXT, one, sizeof(one)); CHECK(a.count == 0); CHECK(b.count == 0); remoteData(&p, p.reqId[1], LG_CLIPBOARD_DATA_TEXT, two, sizeof(two)); remoteData(&p, p.reqId[0], LG_CLIPBOARD_DATA_TEXT, one, sizeof(one)); CHECK(a.count == 1); CHECK(a.type == LG_CLIPBOARD_DATA_TEXT); CHECK(a.size == sizeof(one)); CHECK(memcmp(a.data, one, sizeof(one)) == 0); CHECK(b.count == 1); CHECK(b.type == LG_CLIPBOARD_DATA_TEXT); CHECK(b.size == sizeof(two)); CHECK(memcmp(b.data, two, sizeof(two)) == 0); clipboard_free(); } static void testRemoteKeepsLocalRequest(void) { init(); bind(&p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; clipboard_notifyTypes(types, 1); CHECK(p.notice == 1); g_params.clipboardToLocal = false; notice(&p, types, 1); CHECK(d.notice == 0); /* A notice which is not published locally must not discard the local * clipboard advertised towards the guest. */ clipboard_setTransport(&plainOps, &q); CHECK(q.attach == 1); CHECK(q.notice == 1); CHECK(q.release == 0); g_params.clipboardToLocal = true; notice(&q, types, 1); CHECK(d.notice == 1); /* The two clipboard directions have independent generations. A late * request can still refer to the local offer advertised before this remote * notice, so its type must remain serviceable. */ CHECK(q.ev->request(q.evCtx, 43, LG_CLIPBOARD_DATA_TEXT)); CHECK(d.request == 1); CHECK(d.reqType[0] == LG_CLIPBOARD_DATA_TEXT); clipboard_free(); } static void testPendingRemoteKeepsLocalRequest(void) { init(); bind(&p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; clipboard_notifyTypes(types, 1); CHECK(p.notice == 1); clipboard_setLocalAvailable(false); notice(&p, types, 1); CHECK(d.notice == 0); clipboard_setLocalAvailable(true); CHECK(d.notice == 1); /* Publishing a notice which arrived while the local clipboard was * unavailable must likewise retain the local request allow-list. */ CHECK(p.ev->request(p.evCtx, 44, LG_CLIPBOARD_DATA_TEXT)); CHECK(d.request == 1); CHECK(d.reqType[0] == LG_CLIPBOARD_DATA_TEXT); clipboard_free(); } static void testInvalid(void) { init(); bind(&p); const LG_ClipboardData bad[] = { LG_CLIPBOARD_DATA_NONE }; p.ev->notice(p.evCtx, NULL, 1); p.ev->notice(p.evCtx, bad, 1); p.ev->notice(p.evCtx, bad, 0); CHECK(d.notice == 0); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; notice(&p, types, 1); CHECK(!clipboard_request( LG_CLIPBOARD_DATA_NONE, onReply, &(struct Reply) { 0 })); CHECK(!clipboard_request(LG_CLIPBOARD_DATA_TEXT, NULL, NULL)); struct Reply empty = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &empty)); remoteData(&p, LG_CLIPBOARD_REQUEST_INVALID, LG_CLIPBOARD_DATA_TEXT, NULL, 0); CHECK(empty.count == 0); remoteData(&p, p.reqId[p.request - 1], LG_CLIPBOARD_DATA_TEXT, NULL, 0); CHECK(empty.count == 1); CHECK(empty.type == LG_CLIPBOARD_DATA_TEXT); CHECK(empty.size == 0); struct Reply wrong = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &wrong)); remoteData(&p, p.reqId[p.request - 1], LG_CLIPBOARD_DATA_PNG, NULL, 0); checkNone(&wrong); struct Reply null = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &null)); remoteData(&p, p.reqId[p.request - 1], LG_CLIPBOARD_DATA_TEXT, NULL, 1); checkNone(&null); #if SIZE_MAX > UINT32_MAX struct Reply large = { 0 }; const uint8_t byte = 1; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &large)); remoteData(&p, p.reqId[p.request - 1], LG_CLIPBOARD_DATA_TEXT, &byte, (size_t)UINT32_MAX + 1); checkNone(&large); #endif p.reqOK = false; struct Reply failed = { 0 }; CHECK(!clipboard_request( LG_CLIPBOARD_DATA_TEXT, onReply, &failed)); CHECK(failed.count == 0); clipboard_free(); } static void testCancel(void) { init(); bind(&p); const LG_ClipboardData text[] = { LG_CLIPBOARD_DATA_TEXT }; const LG_ClipboardData png[] = { LG_CLIPBOARD_DATA_PNG }; notice(&p, text, 1); struct Reply newer = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &newer)); const LG_ClipboardRequest newerId = p.reqId[p.request - 1]; notice(&p, png, 1); checkNone(&newer); remoteData(&p, newerId, LG_CLIPBOARD_DATA_TEXT, "x", 1); CHECK(newer.count == 1); struct Reply released = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_PNG, onReply, &released)); const LG_ClipboardRequest releaseId = p.reqId[p.request - 1]; p.ev->release(p.evCtx); checkNone(&released); remoteData(&p, releaseId, LG_CLIPBOARD_DATA_PNG, "x", 1); CHECK(released.count == 1); notice(&p, text, 1); struct Reply unavailable = { 0 }; CHECK(clipboard_request( LG_CLIPBOARD_DATA_TEXT, onReply, &unavailable)); const LG_ClipboardRequest unavailableId = p.reqId[p.request - 1]; clipboard_setLocalAvailable(false); checkNone(&unavailable); remoteData(&p, unavailableId, LG_CLIPBOARD_DATA_TEXT, "x", 1); CHECK(unavailable.count == 1); clipboard_setLocalAvailable(true); notice(&p, text, 1); struct Reply switched = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &switched)); const LG_ClipboardRequest switchedId = p.reqId[p.request - 1]; clipboard_setTransport(&plainOps, &q); checkNone(&switched); CHECK(p.oldEv); p.oldEv->data(p.oldCtx, switchedId, LG_CLIPBOARD_DATA_TEXT, "x", 1); CHECK(switched.count == 1); CHECK(d.release == 3); clipboard_free(); } static void testGeneration(void) { init(); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; clipboard_notifyTypes(types, 1); clipboard_setFallback(&statOps, &p); notice(&p, types, 1); struct Reply old = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &old)); const LG_ClipboardRequest oldId = p.reqId[p.request - 1]; setStatus(&p, true, 2); checkNone(&old); CHECK(p.attach == 2); CHECK(p.detach == 1); CHECK(d.release == 1); remoteData(&p, oldId, LG_CLIPBOARD_DATA_TEXT, "old", 3); CHECK(old.count == 1); notice(&p, types, 1); struct Reply next = { 0 }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &next)); remoteData(&p, p.reqId[p.request - 1], LG_CLIPBOARD_DATA_TEXT, "new", 3); CHECK(next.count == 1); CHECK(next.type == LG_CLIPBOARD_DATA_TEXT); CHECK(next.size == 3); CHECK(memcmp(next.data, "new", 3) == 0); clipboard_free(); } static void testLocal(void) { init(); bind(&p); const LG_ClipboardData bad[] = { LG_CLIPBOARD_DATA_NONE }; const LG_ClipboardData many[LG_CLIPBOARD_DATA_NONE + 1] = { 0 }; const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_DATA_PNG }; clipboard_notifyTypes(NULL, 1); clipboard_notifyTypes(bad, 1); clipboard_notifyTypes(many, LG_CLIPBOARD_DATA_NONE + 1); CHECK(p.notice == 0); clipboard_notifyTypes(types, 2); CHECK(p.notice == 1); CHECK(!p.ev->request( p.evCtx, LG_CLIPBOARD_REQUEST_INVALID, LG_CLIPBOARD_DATA_TEXT)); CHECK(!p.ev->request(p.evCtx, 1, LG_CLIPBOARD_DATA_NONE)); CHECK(p.ev->request(p.evCtx, 40, LG_CLIPBOARD_DATA_TEXT)); CHECK(d.request == 1); CHECK(!p.ev->request(p.evCtx, 41, LG_CLIPBOARD_DATA_PNG)); const LG_ClipboardRequest first = d.reqId[0]; clipboard_data(first + 100, LG_CLIPBOARD_DATA_TEXT, "x", 1); CHECK(p.data == 0); clipboard_data(first, LG_CLIPBOARD_DATA_PNG, "x", 1); CHECK(p.data == 1); CHECK(p.dataId[0] == 40); CHECK(p.dataType[0] == LG_CLIPBOARD_DATA_NONE); CHECK(p.dataSize[0] == 0); CHECK(p.ev->request(p.evCtx, 41, LG_CLIPBOARD_DATA_PNG)); const LG_ClipboardRequest second = d.reqId[1]; clipboard_abort(second + 100); CHECK(p.data == 1); clipboard_abort(second); CHECK(p.data == 2); CHECK(p.dataId[1] == 41); CHECK(p.dataType[1] == LG_CLIPBOARD_DATA_NONE); CHECK(p.ev->request(p.evCtx, 42, LG_CLIPBOARD_DATA_TEXT)); const LG_ClipboardRequest third = d.reqId[2]; const uint8_t buf[] = { 5, 6, 7 }; clipboard_data(third, LG_CLIPBOARD_DATA_TEXT, buf, sizeof(buf)); CHECK(p.data == 3); CHECK(p.dataId[2] == 42); CHECK(p.dataType[2] == LG_CLIPBOARD_DATA_TEXT); CHECK(p.dataSize[2] == sizeof(buf)); CHECK(memcmp(p.dataBuf[2], buf, sizeof(buf)) == 0); clipboard_data(third, LG_CLIPBOARD_DATA_TEXT, buf, sizeof(buf)); CHECK(p.data == 3); clipboard_free(); } static void testReentrant(void) { init(); bind(&p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; notice(&p, types, 1); struct Reply second = { 0 }; struct Reply first = { .next = &second }; CHECK(clipboard_request(LG_CLIPBOARD_DATA_TEXT, onReply, &first)); remoteData(&p, p.reqId[0], LG_CLIPBOARD_DATA_TEXT, "a", 1); CHECK(first.count == 1); CHECK(first.nested); CHECK(p.request == 2); remoteData(&p, p.reqId[1], LG_CLIPBOARD_DATA_TEXT, "b", 1); CHECK(second.count == 1); clipboard_notifyTypes(types, 1); d.autoData = true; d.autoType = LG_CLIPBOARD_DATA_TEXT; d.autoBuf = "local"; d.autoSize = 5; CHECK(p.ev->request(p.evCtx, 70, LG_CLIPBOARD_DATA_TEXT)); CHECK(p.data == 1); CHECK(p.dataId[0] == 70); CHECK(p.dataType[0] == LG_CLIPBOARD_DATA_TEXT); CHECK(p.dataSize[0] == 5); CHECK(memcmp(p.dataBuf[0], "local", 5) == 0); clipboard_free(); } static void testStreamLocal(void) { init(); clipboard_setFallback(&streamOps, &p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; clipboard_notifyTypes(types, 1); CHECK(p.ev->request(p.evCtx, 40, LG_CLIPBOARD_DATA_TEXT)); const LG_ClipboardRequest transfer = d.reqId[0]; CHECK(clipboard_dataBegin(transfer, LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) == LG_CLIPBOARD_RESULT_ACCEPTED); const uint8_t first[] = { 1, 2 }; const uint8_t second[] = { 3, 4, 5 }; CHECK(clipboard_dataChunk(transfer, 0, first, sizeof(first)) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(clipboard_dataChunk(transfer, sizeof(first), second, sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(clipboard_dataEnd(transfer, sizeof(first) + sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(p.begin == 1); CHECK(p.streamId == 40); CHECK(p.streamType == LG_CLIPBOARD_DATA_TEXT); CHECK(p.sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN); CHECK(p.chunk == 2); CHECK(p.chunkOffset[0] == 0); CHECK(p.chunkOffset[1] == sizeof(first)); CHECK(p.finalSize == sizeof(first) + sizeof(second)); CHECK(p.ev->request(p.evCtx, 41, LG_CLIPBOARD_DATA_TEXT)); const LG_ClipboardRequest invalid = d.reqId[1]; CHECK(clipboard_dataBegin(invalid, LG_CLIPBOARD_DATA_TEXT, 1) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(clipboard_dataChunk(invalid, 1, first, 1) == LG_CLIPBOARD_RESULT_FAILED); CHECK(p.cancel == 1); CHECK(p.streamId == 41); CHECK(p.cancelReason == LG_CLIPBOARD_CANCEL_INVALID); CHECK(p.ev->request(p.evCtx, 42, LG_CLIPBOARD_DATA_TEXT)); p.ev->requestCancel(p.evCtx, 42, LG_CLIPBOARD_CANCEL_REPLACED); CHECK(d.cancel == 1); CHECK(d.cancelReason == LG_CLIPBOARD_CANCEL_REPLACED); clipboard_free(); } static void testStreamBlocked(void) { init(); clipboard_setFallback(&streamOps, &p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; clipboard_notifyTypes(types, 1); CHECK(p.ev->request(p.evCtx, 50, LG_CLIPBOARD_DATA_TEXT)); p.beginResult = LG_CLIPBOARD_RESULT_BLOCKED; uint8_t data[] = { 7, 8, 9 }; clipboard_data(d.reqId[0], LG_CLIPBOARD_DATA_TEXT, data, sizeof(data)); CHECK(p.begin == 1); CHECK(p.chunk == 0); CHECK(p.end == 0); data[0] = 0; p.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; p.ev->dataReady(p.evCtx, 50); CHECK(p.begin == 2); CHECK(p.chunk == 1); CHECK(p.end == 1); CHECK(p.chunkSize[0] == 3); CHECK(p.chunkBuf[0][0] == 7); CHECK(p.chunkBuf[0][1] == 8); CHECK(p.chunkBuf[0][2] == 9); CHECK(p.finalSize == 3); CHECK(p.ev->request(p.evCtx, 51, LG_CLIPBOARD_DATA_TEXT)); const LG_ClipboardRequest transfer = d.reqId[1]; p.beginResult = LG_CLIPBOARD_RESULT_BLOCKED; CHECK(clipboard_dataBegin(transfer, LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) == LG_CLIPBOARD_RESULT_BLOCKED); p.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; d.autoBegin = true; p.ev->dataReady(p.evCtx, 51); CHECK(d.ready == 1); CHECK(d.readyResult == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(clipboard_dataEnd(transfer, 0) == LG_CLIPBOARD_RESULT_ACCEPTED); clipboard_free(); } static void testStreamRemote(void) { init(); clipboard_setFallback(&streamOps, &p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; notice(&p, types, 1); struct StreamSink sink = { 0 }; initSink(&sink); LG_ClipboardRequest id; CHECK(clipboard_requestStream( LG_CLIPBOARD_DATA_TEXT, &sinkOps, &sink, &id)); CHECK(id == p.reqId[0]); CHECK(p.ev->dataBegin(p.evCtx, id, LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(sink.begin == 1); CHECK(sink.sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN); const uint8_t data[] = { 1, 3, 5, 7 }; sink.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED; CHECK(p.ev->dataChunk(p.evCtx, id, 0, data, sizeof(data)) == LG_CLIPBOARD_RESULT_BLOCKED); CHECK(sink.chunk == 1); sink.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; CHECK(clipboard_requestReady(id)); CHECK(p.ready == 1); CHECK(p.streamId == id); CHECK(p.ev->dataChunk(p.evCtx, id, 0, data, sizeof(data)) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(p.ev->dataEnd(p.evCtx, id, sizeof(data)) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(sink.chunk == 2); CHECK(sink.end == 1); CHECK(sink.finalSize == sizeof(data)); CHECK(memcmp(sink.data, data, sizeof(data)) == 0); struct StreamSink cancel = { 0 }; initSink(&cancel); CHECK(clipboard_requestStream( LG_CLIPBOARD_DATA_TEXT, &sinkOps, &cancel, &id)); p.ev->dataCancel(p.evCtx, id, LG_CLIPBOARD_CANCEL_ABORTED); CHECK(cancel.cancel == 1); CHECK(cancel.reason == LG_CLIPBOARD_CANCEL_ABORTED); clipboard_free(); } static void testRequestPublication(void) { init(); clipboard_setFallback(&streamOps, &p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; notice(&p, types, 1); struct StreamSink sink = { 0 }; initSink(&sink); sink.requireRequest = true; p.earlyBegin = true; CHECK(clipboard_requestStream(LG_CLIPBOARD_DATA_TEXT, &sinkOps, &sink, &sink.request)); CHECK(sink.request == p.reqId[0]); CHECK(p.earlyResult == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(sink.begin == 1); CHECK(p.ev->dataEnd(p.evCtx, sink.request, 0) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(sink.end == 1); clipboard_free(); } static void testStreamLegacy(void) { init(); bind(&p); const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; clipboard_notifyTypes(types, 1); CHECK(p.ev->request(p.evCtx, 60, LG_CLIPBOARD_DATA_TEXT)); const LG_ClipboardRequest transfer = d.reqId[0]; const uint8_t first[] = { 2, 4 }; const uint8_t second[] = { 6, 8 }; CHECK(clipboard_dataBegin(transfer, LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(clipboard_dataChunk(transfer, 0, first, sizeof(first)) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(clipboard_dataChunk(transfer, sizeof(first), second, sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(clipboard_dataEnd(transfer, 4) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(p.data == 1); CHECK(p.dataId[0] == 60); CHECK(p.dataType[0] == LG_CLIPBOARD_DATA_TEXT); CHECK(p.dataSize[0] == 4); CHECK(memcmp(p.dataBuf[0], "\x02\x04\x06\x08", 4) == 0); notice(&p, types, 1); struct StreamSink sink = { 0 }; initSink(&sink); sink.beginResult = LG_CLIPBOARD_RESULT_BLOCKED; LG_ClipboardRequest id; CHECK(clipboard_requestStream( LG_CLIPBOARD_DATA_TEXT, &sinkOps, &sink, &id)); const uint8_t remote[] = { 9, 8, 7 }; remoteData(&p, id, LG_CLIPBOARD_DATA_TEXT, remote, sizeof(remote)); CHECK(sink.begin == 1); CHECK(sink.chunk == 0); sink.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; CHECK(clipboard_requestReady(id)); CHECK(p.ready == 0); CHECK(sink.begin == 2); CHECK(sink.chunk == 1); CHECK(sink.end == 1); CHECK(sink.size == sizeof(remote)); CHECK(memcmp(sink.data, remote, sizeof(remote)) == 0); clipboard_free(); } struct Test { const char * name; void (*run)(void); }; static const struct Test tests[] = { { "preference", testPreference }, { "file-publication", testFilePublication }, { "file-replacement", testFileReplacement }, { "file-failure", testFileFailure }, { "callback-serial", testCallbackSerialization }, { "request" , testRequest }, { "remote-local", testRemoteKeepsLocalRequest }, { "pending-remote", testPendingRemoteKeepsLocalRequest }, { "invalid" , testInvalid }, { "cancel" , testCancel }, { "generation", testGeneration }, { "local" , testLocal }, { "reentrant" , testReentrant }, { "stream-local" , testStreamLocal }, { "stream-blocked", testStreamBlocked }, { "stream-remote" , testStreamRemote }, { "request-publish", testRequestPublication }, { "stream-legacy" , testStreamLegacy }, }; int main(int argc, char ** argv) { if (argc != 2) { fprintf(stderr, "usage: %s \n", argv[0]); return EXIT_FAILURE; } alarm(TEST_ALARM_S); debug_init(); for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i) if (strcmp(argv[1], tests[i].name) == 0) { tests[i].run(); return 0; } fprintf(stderr, "unknown test: %s\n", argv[1]); return EXIT_FAILURE; }