[client] clipboard: add seamless file backends

This commit is contained in:
Geoffrey McRae
2026-08-14 16:07:30 +10:00
parent 9679c94b91
commit a91343be96
20 changed files with 7145 additions and 105 deletions

View File

@@ -195,6 +195,9 @@ if(ENABLE_WAYLAND)
)
set(WAYLAND_CLIPBOARD_CASES
mime
file-import
file-source
file-replace
replace
read-eof
read-block
@@ -241,6 +244,10 @@ if(ENABLE_X11)
)
set(X11_CLIPBOARD_CASES
targets
file-import
file-incr
file-source
file-source-active-replace
normal
incr
incr-block
@@ -457,6 +464,7 @@ endforeach()
add_executable(clipboard-tests
clipboard_test.c
clipboard_files_stub.c
../src/clipboard.c
)
target_compile_definitions(clipboard-tests PRIVATE
@@ -473,6 +481,10 @@ target_link_libraries(clipboard-tests
)
set(CLIPBOARD_CASES
preference
file-publication
file-replacement
file-failure
callback-serial
request
remote-local
pending-remote
@@ -496,8 +508,41 @@ foreach(name IN LISTS CLIPBOARD_CASES)
)
endforeach()
add_executable(clipboard-files-tests
clipboard_files_test.c
../src/clipboard_files.c
)
target_include_directories(clipboard-files-tests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
)
target_link_libraries(clipboard-files-tests
${EXE_FLAGS}
lg_common
PkgConfig::FUSE3
)
add_test(NAME clipboard-files-lifecycle
COMMAND clipboard-files-tests
)
set_tests_properties(clipboard-files-lifecycle PROPERTIES
TIMEOUT 10
)
add_test(NAME clipboard-files-failures
COMMAND clipboard-files-tests failures
)
set_tests_properties(clipboard-files-failures PROPERTIES
TIMEOUT 10
)
add_test(NAME clipboard-files-mount
COMMAND clipboard-files-tests mount
)
set_tests_properties(clipboard-files-mount PROPERTIES
TIMEOUT 10
SKIP_RETURN_CODE 77
)
add_executable(spice-clipboard-tests
spice_clipboard_test.c
clipboard_files_stub.c
../src/clipboard.c
../transports/SPICE/clipboard.c
)

View File

@@ -0,0 +1,116 @@
/**
* 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 "../src/clipboard_files.h"
unsigned lgClipboardFilesStubRemoteOfferCount;
unsigned lgClipboardFilesStubRemoteClearCount;
uint64_t lgClipboardFilesStubRemoteDataset;
bool lgClipboardFilesStubRemoteReady;
bool lgClipboardFilesStubRemoteOfferResult = true;
bool lgClipboardFiles_init(void) { return true; }
void lgClipboardFiles_free(void) {}
bool lgClipboardFiles_setLocal(const char * mime,
const void * data, size_t size)
{
(void)mime; (void)data; (void)size; return false;
}
void lgClipboardFiles_clearLocal(void) {}
bool lgClipboardFiles_getRemote(const char * mime, char ** data, size_t * size)
{
(void)mime; (void)data; (void)size; return false;
}
uint64_t lgClipboardFiles_remotePresentationAcquire(void) { return 0; }
bool lgClipboardFiles_getRemotePresentation(uint64_t presentation,
const char * mime, char ** data, size_t * size)
{
(void)presentation; (void)mime; (void)data; (void)size; return false;
}
void lgClipboardFiles_remotePresentationDelivered(uint64_t presentation)
{
(void)presentation;
}
void lgClipboardFiles_remotePresentationRelease(uint64_t presentation)
{
(void)presentation;
}
bool lgClipboardFiles_remoteReady(uint64_t dataset)
{
return lgClipboardFilesStubRemoteReady &&
dataset == lgClipboardFilesStubRemoteDataset;
}
bool lgClipboardFiles_remoteOffer(uint64_t dataset)
{
++lgClipboardFilesStubRemoteOfferCount;
lgClipboardFilesStubRemoteDataset = dataset;
return lgClipboardFilesStubRemoteOfferResult;
}
void lgClipboardFiles_remoteClear(void)
{
++lgClipboardFilesStubRemoteClearCount;
lgClipboardFilesStubRemoteDataset = 0;
}
void lgClipboardFiles_providerUnavailable(void) {}
void lgClipboardFiles_remoteAcquired(uint64_t dataset,
uint64_t acquisition, LG_ClipboardFileError error)
{
(void)dataset; (void)acquisition; (void)error;
}
LG_ClipboardResult lgClipboardFiles_remoteDataBegin(
const LG_ClipboardFileRequest * request, uint64_t sizeHint)
{
(void)request; (void)sizeHint; return LG_CLIPBOARD_RESULT_FAILED;
}
LG_ClipboardResult lgClipboardFiles_remoteDataChunk(
const LG_ClipboardFileRequest * request, uint64_t offset,
const void * data, size_t size)
{
(void)request; (void)offset; (void)data; (void)size;
return LG_CLIPBOARD_RESULT_FAILED;
}
LG_ClipboardResult lgClipboardFiles_remoteDataEnd(
const LG_ClipboardFileRequest * request, uint64_t size)
{
(void)request; (void)size; return LG_CLIPBOARD_RESULT_FAILED;
}
void lgClipboardFiles_remoteCancel(uint64_t dataset,
uint64_t request, LG_ClipboardFileError reason)
{
(void)dataset; (void)request; (void)reason;
}
void lgClipboardFiles_localAcquire(uint64_t dataset, uint64_t acquisition)
{
(void)dataset; (void)acquisition;
}
void lgClipboardFiles_localRelease(uint64_t dataset, uint64_t acquisition)
{
(void)dataset; (void)acquisition;
}
void lgClipboardFiles_localRequest(const LG_ClipboardFileRequest * request)
{
(void)request;
}
void lgClipboardFiles_localCancel(uint64_t dataset,
uint64_t request, LG_ClipboardFileError reason)
{
(void)dataset; (void)request; (void)reason;
}
void lgClipboardFiles_localReady(uint64_t request) { (void)request; }

View File

@@ -0,0 +1,908 @@
/**
* 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 "../src/clipboard.h"
#include "../src/clipboard_files.h"
#include "test.h"
#include "common/debug.h"
#include "common/KVMFRClipboard.h"
#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
static unsigned notices;
static uint64_t datasets[16];
static unsigned acquireCount;
static uint64_t acquiredDataset;
static uint64_t acquiredRequest;
static unsigned releaseCount;
static uint64_t releasedDataset;
static uint64_t releasedAcquisition;
static unsigned requestCount;
static LG_ClipboardFileRequest remoteRequest;
static unsigned remoteReadyCount;
static unsigned remoteFailedCount;
static uint64_t remoteFailedDataset;
static bool remoteRequestSucceeds = true;
static uint8_t * responseData;
static size_t responseSize;
static size_t responseCapacity;
static bool responseOpen;
static bool responseComplete;
static unsigned cancelCount;
static uint64_t cancelledDataset;
static uint64_t cancelledRequest;
static LG_ClipboardFileError cancelledReason;
void lgClipboard_notifyFiles(uint64_t dataset)
{
CHECK(notices < sizeof(datasets) / sizeof(datasets[0]));
datasets[notices++] = dataset;
}
bool lgClipboard_fileAcquire(uint64_t dataset, uint64_t acquisition)
{
++acquireCount;
acquiredDataset = dataset;
acquiredRequest = acquisition;
return true;
}
bool lgClipboard_fileAcquired(uint64_t dataset, uint64_t acquisition,
LG_ClipboardFileError error)
{
(void)dataset;
(void)acquisition;
(void)error;
return false;
}
bool lgClipboard_fileRelease(uint64_t dataset, uint64_t acquisition)
{
++releaseCount;
releasedDataset = dataset;
releasedAcquisition = acquisition;
return true;
}
bool lgClipboard_fileRequest(const LG_ClipboardFileRequest * request)
{
CHECK(request);
++requestCount;
remoteRequest = *request;
return remoteRequestSucceeds;
}
LG_ClipboardResult lgClipboard_fileDataBegin(
const LG_ClipboardFileRequest * request, uint64_t sizeHint)
{
CHECK(request);
(void)sizeHint;
responseSize = 0;
responseOpen = true;
responseComplete = false;
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
LG_ClipboardResult lgClipboard_fileDataChunk(
const LG_ClipboardFileRequest * request, uint64_t responseOffset,
const void * data, size_t size)
{
CHECK(request);
CHECK(responseOpen);
CHECK(responseOffset == responseSize);
CHECK(data || !size);
CHECK(size <= SIZE_MAX - responseSize);
const size_t wanted = responseSize + size;
if (wanted > responseCapacity)
{
uint8_t * resized = realloc(responseData, wanted);
CHECK(resized);
responseData = resized;
responseCapacity = wanted;
}
if (size)
memcpy(responseData + responseSize, data, size);
responseSize = wanted;
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
LG_ClipboardResult lgClipboard_fileDataEnd(
const LG_ClipboardFileRequest * request, uint64_t finalSize)
{
CHECK(request);
CHECK(responseOpen);
CHECK(finalSize == responseSize);
responseOpen = false;
responseComplete = true;
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
bool lgClipboard_fileCancel(uint64_t dataset, uint64_t request,
LG_ClipboardFileError reason)
{
++cancelCount;
cancelledDataset = dataset;
cancelledRequest = request;
cancelledReason = reason;
return true;
}
void lgClipboard_fileRemoteReady(uint64_t dataset)
{
CHECK(dataset == acquiredDataset);
++remoteReadyCount;
}
void lgClipboard_fileRemoteFailed(uint64_t dataset)
{
CHECK(dataset);
++remoteFailedCount;
remoteFailedDataset = dataset;
}
static bool runLocalRequest(uint64_t dataset, uint64_t node,
LG_ClipboardFileOperation operation, uint64_t offset, uint32_t length)
{
static uint64_t requestId = 1;
responseSize = 0;
responseOpen = false;
responseComplete = false;
cancelCount = 0;
cancelledDataset = 0;
cancelledRequest = 0;
cancelledReason = LG_CLIPBOARD_FILE_ERROR_NONE;
const LG_ClipboardFileRequest request =
{
.dataset = dataset,
.request = requestId++,
.node = node,
.offset = offset,
.length = length,
.operation = operation,
};
const bool result = lgClipboardFiles_testLocalRequest(&request);
CHECK(!result || responseComplete);
CHECK(result ? cancelCount == 0U : cancelCount == 1U);
if (!result)
{
CHECK(cancelledDataset == dataset);
CHECK(cancelledRequest == request.request);
CHECK(cancelledReason != LG_CLIPBOARD_FILE_ERROR_NONE);
}
return result;
}
static bool responseEntry(const char * name, KVMFRClipboardFileEntry * result)
{
size_t offset = 0;
while (offset < responseSize)
{
CHECK(responseSize - offset >= sizeof(*result));
KVMFRClipboardFileEntry entry;
memcpy(&entry, responseData + offset, sizeof(entry));
const uint64_t bytes = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(
entry.nameLength);
CHECK(bytes <= SIZE_MAX);
CHECK((size_t)bytes <= responseSize - offset);
const char * entryName = (const char *)responseData + offset +
sizeof(entry);
if (strlen(name) == entry.nameLength &&
!memcmp(entryName, name, entry.nameLength))
{
*result = entry;
return true;
}
offset += (size_t)bytes;
}
CHECK(offset == responseSize);
return false;
}
static void createFile(const char * path, const char * data)
{
const int fd = open(path,
O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
CHECK(fd >= 0);
const size_t length = strlen(data);
CHECK(write(fd, data, length) == (ssize_t)length);
CHECK(close(fd) == 0);
}
static void testLocalLifecycle(void)
{
char directory[] = "/tmp/lg-clipboard-files-XXXXXX";
CHECK(mkdtemp(directory));
char path[256];
CHECK(snprintf(path, sizeof(path), "%s/item.txt", directory) > 0);
const int fd = open(path, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
CHECK(fd >= 0);
CHECK(write(fd, "clipboard", 9) == 9);
CHECK(close(fd) == 0);
CHECK(lgClipboardFiles_testInit(UINT64_C(0x1234000012340000)));
char uri[320];
const int uriLength = snprintf(uri, sizeof(uri), "file://%s\r\n", path);
CHECK(uriLength > 0 && (size_t)uriLength < sizeof(uri));
CHECK(lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)uriLength));
CHECK(notices == 1);
CHECK(datasets[0] != 0);
CHECK(kvmfrClipboardTransferFromClient(datasets[0]));
lgClipboardFiles_clearLocal();
char gnome[336];
const int gnomeLength = snprintf(
gnome, sizeof(gnome), "copy\nfile://%s\n", path);
CHECK(gnomeLength > 0 && (size_t)gnomeLength < sizeof(gnome));
CHECK(lgClipboardFiles_setLocal("x-special/gnome-copied-files",
gnome, (size_t)gnomeLength));
CHECK(notices == 2);
CHECK(datasets[1] != datasets[0]);
static const char invalid[] = "file:///does/not/exist\n";
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", invalid, sizeof(invalid) - 1U));
lgClipboardFiles_clearLocal();
lgClipboardFiles_free();
lgClipboardFiles_free();
CHECK(lgClipboardFiles_testInit(UINT64_C(0x5678000056780000)));
CHECK(lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)uriLength));
CHECK(notices == 3);
CHECK(datasets[2] != datasets[0]);
CHECK(datasets[2] != datasets[1]);
lgClipboardFiles_free();
CHECK(unlink(path) == 0);
CHECK(rmdir(directory) == 0);
}
static void testLocalUriValidation(void)
{
char directory[] = "/tmp/lg-clipboard-uri-XXXXXX";
CHECK(mkdtemp(directory));
char plain[256];
char spaced[256];
char link[256];
char alias[256];
CHECK(snprintf(plain, sizeof(plain), "%s/item.txt", directory) > 0);
CHECK(snprintf(spaced, sizeof(spaced),
"%s/item space.txt", directory) > 0);
CHECK(snprintf(link, sizeof(link), "%s/link.txt", directory) > 0);
CHECK(snprintf(alias, sizeof(alias), "%s/alias", directory) > 0);
createFile(plain, "plain");
createFile(spaced, "spaced");
CHECK(symlink("item.txt", link) == 0);
CHECK(symlink(".", alias) == 0);
CHECK(lgClipboardFiles_testInit(UINT64_C(0x684b2452684b2452)));
const unsigned before = notices;
char uri[512];
int length = snprintf(uri, sizeof(uri),
"FILE://LOCALHOST%s/item%%20space.txt\r\n", directory);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
lgClipboardFiles_clearLocal();
length = snprintf(uri, sizeof(uri), "file://%s%%\n", plain);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://%s%%0\n", plain);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://%s%%GG\n", plain);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://%s%%00suffix\n", plain);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://%s?query\n", plain);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://%s#fragment\n", plain);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://example.invalid%s\n", plain);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
static const char root[] = "file:///\n";
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", root, sizeof(root) - 1U));
length = snprintf(uri, sizeof(uri), "file://%s/\n", directory);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
lgClipboardFiles_clearLocal();
length = snprintf(uri, sizeof(uri), "file://%s/./item.txt\n", directory);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://%s//item.txt\n", directory);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://%s\n", link);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri),
"file://%s/alias/item.txt\n", directory);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri), "file://%s\n", spaced);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)length));
length = snprintf(uri, sizeof(uri),
"cutx\nfile://%s\n", plain);
CHECK(length > 0 && (size_t)length < sizeof(uri));
CHECK(!lgClipboardFiles_setLocal(
"x-special/gnome-copied-files", uri, (size_t)length));
CHECK(notices == before + 2U);
lgClipboardFiles_free();
CHECK(unlink(alias) == 0);
CHECK(unlink(link) == 0);
CHECK(unlink(spaced) == 0);
CHECK(unlink(plain) == 0);
CHECK(rmdir(directory) == 0);
}
static void testLocalSnapshot(void)
{
char directory[] = "/tmp/lg-clipboard-snapshot-XXXXXX";
CHECK(mkdtemp(directory));
char stable[256];
char changed[256];
CHECK(snprintf(stable, sizeof(stable),
"%s/stable.txt", directory) > 0);
CHECK(snprintf(changed, sizeof(changed),
"%s/changed.txt", directory) > 0);
createFile(stable, "stable");
createFile(changed, "before");
CHECK(lgClipboardFiles_testInit(UINT64_C(0x1a72021b1a72021b)));
char uri[320];
const int uriLength = snprintf(
uri, sizeof(uri), "file://%s\r\n", directory);
CHECK(uriLength > 0 && (size_t)uriLength < sizeof(uri));
CHECK(lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)uriLength));
const uint64_t dataset = datasets[notices - 1U];
CHECK(runLocalRequest(dataset, KVMFR_CLIPBOARD_FILE_ROOT_NODE,
LG_CLIPBOARD_FILE_LIST, 0, 0));
const char * base = strrchr(directory, '/');
CHECK(base && base[1]);
KVMFRClipboardFileEntry rootEntry;
CHECK(responseEntry(base + 1, &rootEntry));
CHECK(rootEntry.type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY);
CHECK(runLocalRequest(dataset, rootEntry.node,
LG_CLIPBOARD_FILE_LIST, 0, 0));
KVMFRClipboardFileEntry stableEntry;
KVMFRClipboardFileEntry changedEntry;
CHECK(responseEntry("stable.txt", &stableEntry));
CHECK(responseEntry("changed.txt", &changedEntry));
char moved[320];
CHECK(snprintf(moved, sizeof(moved), "%s-moved", directory) > 0);
CHECK(rename(directory, moved) == 0);
CHECK(mkdir(directory, 0700) == 0);
char replacement[384];
CHECK(snprintf(replacement, sizeof(replacement),
"%s/stable.txt", directory) > 0);
createFile(replacement, "replacement");
CHECK(runLocalRequest(dataset, stableEntry.node,
LG_CLIPBOARD_FILE_READ, 0, (uint32_t)stableEntry.size));
CHECK(responseSize == strlen("stable"));
CHECK(!memcmp(responseData, "stable", responseSize));
lgClipboardFiles_testForceLocalEof();
CHECK(!runLocalRequest(dataset, stableEntry.node,
LG_CLIPBOARD_FILE_READ, 0, (uint32_t)stableEntry.size));
CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_IO);
char movedChanged[384];
CHECK(snprintf(movedChanged, sizeof(movedChanged),
"%s/changed.txt", moved) > 0);
const int changedFd = open(movedChanged,
O_WRONLY | O_APPEND | O_CLOEXEC);
CHECK(changedFd >= 0);
CHECK(write(changedFd, "-modified", 9) == 9);
CHECK(close(changedFd) == 0);
CHECK(!runLocalRequest(dataset, changedEntry.node,
LG_CLIPBOARD_FILE_READ, 0, 64));
CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_INVALID);
CHECK(!runLocalRequest(dataset, changedEntry.node,
LG_CLIPBOARD_FILE_READ, 0, (uint32_t)changedEntry.size));
CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_STALE);
char movedStable[384];
char oldStable[384];
CHECK(snprintf(movedStable, sizeof(movedStable),
"%s/stable.txt", moved) > 0);
CHECK(snprintf(oldStable, sizeof(oldStable),
"%s/stable.old", moved) > 0);
CHECK(rename(movedStable, oldStable) == 0);
createFile(movedStable, "stable");
CHECK(!runLocalRequest(dataset, stableEntry.node,
LG_CLIPBOARD_FILE_READ, 0, 64));
CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_INVALID);
CHECK(!runLocalRequest(dataset, stableEntry.node,
LG_CLIPBOARD_FILE_READ, 0, (uint32_t)stableEntry.size));
CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_STALE);
lgClipboardFiles_free();
CHECK(unlink(replacement) == 0);
CHECK(rmdir(directory) == 0);
CHECK(unlink(movedStable) == 0);
CHECK(unlink(oldStable) == 0);
CHECK(unlink(movedChanged) == 0);
CHECK(rmdir(moved) == 0);
}
static void testLocalUnsupportedEntry(void)
{
char directory[] = "/tmp/lg-clipboard-unsupported-XXXXXX";
CHECK(mkdtemp(directory));
char target[256];
char link[256];
char fifo[256];
CHECK(snprintf(target, sizeof(target),
"%s/target.txt", directory) > 0);
CHECK(snprintf(link, sizeof(link), "%s/link.txt", directory) > 0);
CHECK(snprintf(fifo, sizeof(fifo), "%s/special", directory) > 0);
createFile(target, "target");
CHECK(symlink("target.txt", link) == 0);
CHECK(mkfifo(fifo, 0600) == 0);
CHECK(lgClipboardFiles_testInit(UINT64_C(0x62d1147362d11473)));
char uri[320];
const int uriLength = snprintf(
uri, sizeof(uri), "file://%s\r\n", directory);
CHECK(uriLength > 0 && (size_t)uriLength < sizeof(uri));
CHECK(lgClipboardFiles_setLocal(
"text/uri-list", uri, (size_t)uriLength));
const uint64_t dataset = datasets[notices - 1U];
CHECK(runLocalRequest(dataset, KVMFR_CLIPBOARD_FILE_ROOT_NODE,
LG_CLIPBOARD_FILE_LIST, 0, 0));
const char * base = strrchr(directory, '/');
CHECK(base && base[1]);
KVMFRClipboardFileEntry rootEntry;
CHECK(responseEntry(base + 1, &rootEntry));
CHECK(!runLocalRequest(dataset, rootEntry.node,
LG_CLIPBOARD_FILE_LIST, 0, 0));
CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED);
lgClipboardFiles_free();
CHECK(unlink(fifo) == 0);
CHECK(unlink(link) == 0);
CHECK(unlink(target) == 0);
CHECK(rmdir(directory) == 0);
}
static uint64_t completeRemoteOffer(uint64_t dataset, const char * name)
{
const unsigned previousAcquires = acquireCount;
const unsigned previousRequests = requestCount;
const unsigned previousReady = remoteReadyCount;
CHECK(lgClipboardFiles_remoteOffer(dataset));
CHECK(acquireCount == previousAcquires + 1U);
CHECK(acquiredDataset == dataset);
const uint64_t acquisition = acquiredRequest;
lgClipboardFiles_remoteAcquired(dataset, acquisition,
LG_CLIPBOARD_FILE_ERROR_NONE);
CHECK(requestCount == previousRequests + 1U);
CHECK(remoteRequest.dataset == dataset);
CHECK(remoteRequest.node == KVMFR_CLIPBOARD_FILE_ROOT_NODE);
CHECK(remoteRequest.operation == LG_CLIPBOARD_FILE_LIST);
const size_t nameLength = strlen(name);
const uint64_t wireSize = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(nameLength);
CHECK(wireSize <= SIZE_MAX);
uint8_t * wire = calloc(1, (size_t)wireSize);
CHECK(wire);
const KVMFRClipboardFileEntry entry =
{
.node = 2,
.size = 9,
.createdNs = 1,
.modifiedNs = 2,
.type = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR,
.nameLength = (uint16_t)nameLength,
};
memcpy(wire, &entry, sizeof(entry));
memcpy(wire + sizeof(entry), name, nameLength);
CHECK(lgClipboardFiles_remoteDataBegin(
&remoteRequest, wireSize) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(lgClipboardFiles_remoteDataChunk(&remoteRequest, 0,
wire, (size_t)wireSize) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(lgClipboardFiles_remoteDataEnd(
&remoteRequest, wireSize) == LG_CLIPBOARD_RESULT_ACCEPTED);
free(wire);
CHECK(remoteReadyCount == previousReady + 1U);
CHECK(lgClipboardFiles_remoteReady(dataset));
return acquisition;
}
static uint64_t beginRemoteOffer(uint64_t dataset)
{
const unsigned previousAcquires = acquireCount;
CHECK(lgClipboardFiles_remoteOffer(dataset));
CHECK(acquireCount == previousAcquires + 1U);
CHECK(acquiredDataset == dataset);
return acquiredRequest;
}
static LG_ClipboardFileRequest beginRemoteList(uint64_t dataset,
uint64_t * acquisition)
{
const unsigned previousRequests = requestCount;
*acquisition = beginRemoteOffer(dataset);
lgClipboardFiles_remoteAcquired(dataset, *acquisition,
LG_CLIPBOARD_FILE_ERROR_NONE);
CHECK(requestCount == previousRequests + 1U);
CHECK(remoteRequest.dataset == dataset);
CHECK(remoteRequest.operation == LG_CLIPBOARD_FILE_LIST);
return remoteRequest;
}
static void checkRemoteFailure(uint64_t dataset,
unsigned previousFailures, unsigned previousReleases,
bool acquired)
{
CHECK(remoteFailedCount == previousFailures + 1U);
CHECK(remoteFailedDataset == dataset);
CHECK(releaseCount == previousReleases + (acquired ? 1U : 0U));
if (acquired)
CHECK(releasedDataset == dataset);
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 0U);
}
static void testRemoteFailures(void)
{
CHECK(lgClipboardFiles_testInit(UINT64_C(0x7315000073150000)));
acquireCount = 0;
releaseCount = 0;
requestCount = 0;
remoteFailedCount = 0;
remoteFailedDataset = 0;
remoteRequestSucceeds = true;
uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x73150001);
uint64_t acquisition = beginRemoteOffer(dataset);
unsigned previousFailures = remoteFailedCount;
unsigned previousReleases = releaseCount;
lgClipboardFiles_remoteAcquired(dataset, acquisition,
LG_CLIPBOARD_FILE_ERROR_ACCESS);
checkRemoteFailure(dataset, previousFailures, previousReleases, false);
dataset += 1U;
acquisition = beginRemoteOffer(dataset);
previousFailures = remoteFailedCount;
previousReleases = releaseCount;
lgClipboardFiles_remoteCancel(dataset, acquisition,
LG_CLIPBOARD_FILE_ERROR_CANCELLED);
checkRemoteFailure(dataset, previousFailures, previousReleases, false);
dataset += 1U;
LG_ClipboardFileRequest request = beginRemoteList(dataset, &acquisition);
previousFailures = remoteFailedCount;
previousReleases = releaseCount;
lgClipboardFiles_remoteCancel(dataset, request.request,
LG_CLIPBOARD_FILE_ERROR_CANCELLED);
checkRemoteFailure(dataset, previousFailures, previousReleases, true);
dataset += 1U;
acquisition = beginRemoteOffer(dataset);
previousFailures = remoteFailedCount;
previousReleases = releaseCount;
remoteRequestSucceeds = false;
lgClipboardFiles_remoteAcquired(dataset, acquisition,
LG_CLIPBOARD_FILE_ERROR_NONE);
remoteRequestSucceeds = true;
checkRemoteFailure(dataset, previousFailures, previousReleases, true);
dataset += 1U;
request = beginRemoteList(dataset, &acquisition);
LG_ClipboardFileRequest malformed = request;
malformed.node += 1U;
previousFailures = remoteFailedCount;
previousReleases = releaseCount;
CHECK(lgClipboardFiles_remoteDataBegin(&malformed, 0) ==
LG_CLIPBOARD_RESULT_FAILED);
checkRemoteFailure(dataset, previousFailures, previousReleases, true);
dataset += 1U;
request = beginRemoteList(dataset, &acquisition);
CHECK(lgClipboardFiles_remoteDataBegin(&request, 1) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
previousFailures = remoteFailedCount;
previousReleases = releaseCount;
CHECK(lgClipboardFiles_remoteDataChunk(&request, 0, NULL, 1) ==
LG_CLIPBOARD_RESULT_FAILED);
checkRemoteFailure(dataset, previousFailures, previousReleases, true);
dataset += 1U;
request = beginRemoteList(dataset, &acquisition);
CHECK(lgClipboardFiles_remoteDataBegin(&request, 1) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
previousFailures = remoteFailedCount;
previousReleases = releaseCount;
CHECK(lgClipboardFiles_remoteDataEnd(&request, 0) ==
LG_CLIPBOARD_RESULT_FAILED);
checkRemoteFailure(dataset, previousFailures, previousReleases, true);
dataset += 1U;
request = beginRemoteList(dataset, &acquisition);
static const char invalidName[] = "bad/name";
const size_t wireSize = (size_t)KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(
sizeof(invalidName) - 1U);
uint8_t * wire = calloc(1, wireSize);
CHECK(wire);
const KVMFRClipboardFileEntry entry =
{
.node = 2,
.size = 1,
.type = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR,
.nameLength = sizeof(invalidName) - 1U,
};
memcpy(wire, &entry, sizeof(entry));
memcpy(wire + sizeof(entry), invalidName, sizeof(invalidName) - 1U);
CHECK(lgClipboardFiles_remoteDataBegin(&request, wireSize) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(lgClipboardFiles_remoteDataChunk(&request, 0, wire, wireSize) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
previousFailures = remoteFailedCount;
previousReleases = releaseCount;
CHECK(lgClipboardFiles_remoteDataEnd(&request, wireSize) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
free(wire);
checkRemoteFailure(dataset, previousFailures, previousReleases, true);
lgClipboardFiles_free();
}
static void checkRemoteUri(uint64_t presentation, const char * name)
{
char * uri = NULL;
size_t size = 0;
CHECK(lgClipboardFiles_getRemotePresentation(
presentation, "text/uri-list", &uri, &size));
CHECK(uri);
CHECK(size == strlen(uri));
CHECK(strstr(uri, name));
free(uri);
}
typedef struct ReleasePresentationTask
{
pthread_barrier_t * barrier;
uint64_t presentation;
}
ReleasePresentationTask;
static void * releasePresentation(void * opaque)
{
ReleasePresentationTask * task = opaque;
const int result = pthread_barrier_wait(task->barrier);
CHECK(result == 0 || result == PTHREAD_BARRIER_SERIAL_THREAD);
lgClipboardFiles_remotePresentationRelease(task->presentation);
return NULL;
}
static void releasePresentationConcurrently(uint64_t presentation)
{
pthread_barrier_t barrier;
CHECK(pthread_barrier_init(&barrier, NULL, 3) == 0);
ReleasePresentationTask task =
{
.barrier = &barrier,
.presentation = presentation,
};
pthread_t first;
pthread_t second;
CHECK(pthread_create(&first, NULL, releasePresentation, &task) == 0);
CHECK(pthread_create(&second, NULL, releasePresentation, &task) == 0);
const int result = pthread_barrier_wait(&barrier);
CHECK(result == 0 || result == PTHREAD_BARRIER_SERIAL_THREAD);
CHECK(pthread_join(first, NULL) == 0);
CHECK(pthread_join(second, NULL) == 0);
CHECK(pthread_barrier_destroy(&barrier) == 0);
}
static void testRemoteLifecycle(void)
{
CHECK(lgClipboardFiles_testFuseStopWake());
CHECK(lgClipboardFiles_testInit(UINT64_C(0x7311000073110000)));
CHECK(lgClipboardFiles_testUnsentRemoteOwnership());
acquireCount = 0;
releaseCount = 0;
requestCount = 0;
remoteReadyCount = 0;
cancelCount = 0;
const uint64_t firstDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x73110001);
const uint64_t first = completeRemoteOffer(firstDataset, "first.txt");
CHECK(lgClipboardFiles_testRemoteRead(first, 2, 0, 9));
LG_ClipboardFileRequest shortRead = remoteRequest;
CHECK(lgClipboardFiles_remoteDataBegin(&shortRead, 8) ==
LG_CLIPBOARD_RESULT_FAILED);
CHECK(lgClipboardFiles_testRemoteRead(first, 2, 0, 9));
shortRead = remoteRequest;
CHECK(lgClipboardFiles_remoteDataBegin(&shortRead, 9) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
const uint8_t shortData[8] = { 0 };
CHECK(lgClipboardFiles_remoteDataChunk(&shortRead, 0,
shortData, sizeof(shortData)) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(lgClipboardFiles_remoteDataEnd(&shortRead, sizeof(shortData)) ==
LG_CLIPBOARD_RESULT_FAILED);
const uint64_t firstPresentation =
lgClipboardFiles_remotePresentationAcquire();
CHECK(firstPresentation == first);
checkRemoteUri(firstPresentation, "first.txt");
const uint64_t secondDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x73110002);
completeRemoteOffer(secondDataset, "second.txt");
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 2U);
CHECK(releaseCount == 0U);
checkRemoteUri(firstPresentation, "first.txt");
lgClipboardFiles_remotePresentationRelease(firstPresentation);
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 1U);
CHECK(releaseCount == 1U);
CHECK(releasedDataset == firstDataset);
CHECK(releasedAcquisition == first);
const uint64_t secondPresentation =
lgClipboardFiles_remotePresentationAcquire();
CHECK(secondPresentation != 0);
checkRemoteUri(secondPresentation, "second.txt");
lgClipboardFiles_remotePresentationDelivered(secondPresentation);
lgClipboardFiles_remotePresentationRelease(secondPresentation);
const uint64_t thirdDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x73110003);
completeRemoteOffer(thirdDataset, "third.txt");
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 2U);
CHECK(releaseCount == 1U);
CHECK(lgClipboardFiles_testBeginRemoteLookup(secondPresentation));
lgClipboardFiles_testExpireRemoteDeliveries();
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 2U);
CHECK(releaseCount == 1U);
lgClipboardFiles_testEndRemoteLookup(secondPresentation);
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 1U);
CHECK(releaseCount == 2U);
CHECK(releasedDataset == secondDataset);
lgClipboardFiles_remoteClear();
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 0U);
CHECK(releaseCount == 3U);
for (uint64_t i = 0; i < 10; ++i)
{
const uint64_t dataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
(UINT64_C(0x73120000) + i);
const uint64_t acquisition =
completeRemoteOffer(dataset, "grace.txt");
const uint64_t presentation =
lgClipboardFiles_remotePresentationAcquire();
CHECK(presentation == acquisition);
checkRemoteUri(presentation, "grace.txt");
lgClipboardFiles_remotePresentationDelivered(presentation);
lgClipboardFiles_remotePresentationRelease(presentation);
CHECK(lgClipboardFiles_testRemoteDatasetCount() <= 5U);
}
lgClipboardFiles_remoteClear();
lgClipboardFiles_testExpireRemoteDeliveries();
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 0U);
for (uint64_t i = 0; i < 16; ++i)
{
const uint64_t oldDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
(UINT64_C(0x73130000) + i * 2U);
const uint64_t oldAcquisition =
completeRemoteOffer(oldDataset, "old.txt");
const uint64_t presentation =
lgClipboardFiles_remotePresentationAcquire();
CHECK(presentation == oldAcquisition);
const uint64_t currentDataset = oldDataset + 1U;
completeRemoteOffer(currentDataset, "current.txt");
const unsigned previousReleases = releaseCount;
releasePresentationConcurrently(presentation);
CHECK(releaseCount == previousReleases + 1U);
CHECK(releasedDataset == oldDataset);
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 1U);
lgClipboardFiles_remoteClear();
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 0U);
}
const uint64_t pendingDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER |
UINT64_C(0x73140001);
CHECK(lgClipboardFiles_remoteOffer(pendingDataset));
CHECK(acquiredDataset == pendingDataset);
const uint64_t pendingAcquisition = acquiredRequest;
const uint64_t replacementDataset = pendingDataset + 1U;
const unsigned previousCancels = cancelCount;
CHECK(lgClipboardFiles_remoteOffer(replacementDataset));
CHECK(cancelCount == previousCancels + 1U);
CHECK(cancelledDataset == pendingDataset);
CHECK(cancelledRequest == pendingAcquisition);
CHECK(cancelledReason == LG_CLIPBOARD_FILE_ERROR_CANCELLED);
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 1U);
lgClipboardFiles_remoteClear();
CHECK(lgClipboardFiles_testRemoteDatasetCount() == 0U);
lgClipboardFiles_free();
}
static int testMount(void)
{
if (access("/dev/fuse", R_OK | W_OK) < 0)
return 77;
if (!getenv("XDG_RUNTIME_DIR"))
CHECK(setenv("XDG_RUNTIME_DIR", "/tmp", 1) == 0);
CHECK(lgClipboardFiles_init());
lgClipboardFiles_free();
return 0;
}
int main(int argc, char * argv[])
{
debug_init();
if (argc == 2 && !strcmp(argv[1], "mount"))
return testMount();
if (argc == 2 && !strcmp(argv[1], "failures"))
{
testRemoteFailures();
return 0;
}
testLocalLifecycle();
testLocalUriValidation();
testLocalSnapshot();
testLocalUnsupportedEntry();
testRemoteLifecycle();
free(responseData);
return 0;
}

View File

@@ -26,6 +26,7 @@
#include <pthread.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@@ -61,6 +62,8 @@ struct Provider
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];
@@ -86,21 +89,24 @@ struct Provider
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;
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
@@ -138,6 +144,12 @@ static struct Provider q;
static struct Provider r;
static struct Display d;
extern unsigned lgClipboardFilesStubRemoteOfferCount;
extern unsigned lgClipboardFilesStubRemoteClearCount;
extern uint64_t lgClipboardFilesStubRemoteDataset;
extern bool lgClipboardFilesStubRemoteReady;
extern bool lgClipboardFilesStubRemoteOfferResult;
struct AppState g_state;
struct AppParams g_params;
@@ -258,6 +270,15 @@ static bool notify(void * opaque, const LG_ClipboardData 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)
{
@@ -406,6 +427,18 @@ static const LG_ClipboardOps streamOps =
.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);
@@ -439,6 +472,13 @@ 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;
}
@@ -469,10 +509,18 @@ 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);
lgClipboardFilesStubRemoteOfferCount = 0;
lgClipboardFilesStubRemoteClearCount = 0;
lgClipboardFilesStubRemoteDataset = 0;
lgClipboardFilesStubRemoteReady = false;
lgClipboardFilesStubRemoteOfferResult = true;
g_state.ds = &dsOps;
g_params.clipboardToVM = true;
g_params.clipboardToLocal = true;
lgClipboard_init();
CHECK(lgClipboard_init());
lgClipboard_setLocalAvailable(true);
}
@@ -589,6 +637,214 @@ static void testPreference(void)
lgClipboard_free();
}
static void testFilePublication(void)
{
init();
lgClipboard_setFallback(&fileOps, &p);
const uint64_t dataset = UINT64_C(0x123456789abcdef);
lgClipboard_notifyFiles(dataset);
CHECK(p.fileOffer == 1);
CHECK(p.fileDataset[0] == dataset);
const LG_ClipboardData files[] = { LG_CLIPBOARD_DATA_FILES };
lgClipboard_notifyTypes(files, 1);
CHECK(p.notice == 0);
CHECK(p.fileOffer == 1);
lgClipboard_setTransport(&plainOps, &r);
CHECK(r.attach == 1);
CHECK(r.release == 1);
const uint64_t replacement = dataset + 1U;
lgClipboard_notifyFiles(replacement);
CHECK(r.release == 2);
lgClipboard_dropTransport();
CHECK(p.attach == 2);
CHECK(p.fileOffer == 2);
CHECK(p.fileDataset[1] == replacement);
lgClipboard_setTransport(&fileOps, &q);
CHECK(q.attach == 1);
CHECK(q.fileOffer == 1);
CHECK(q.fileDataset[0] == replacement);
lgClipboard_dropTransport();
CHECK(p.attach == 3);
CHECK(p.fileOffer == 3);
CHECK(p.fileDataset[2] == replacement);
lgClipboard_free();
}
static void testFileReplacement(void)
{
init();
bind(&p);
lgClipboardFilesStubRemoteReady = true;
const uint64_t first = UINT64_C(0x2000000000000001);
p.ev->fileOffer(p.evCtx, first);
CHECK(lgClipboardFilesStubRemoteOfferCount == 1);
CHECK(lgClipboardFilesStubRemoteDataset == 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(lgClipboardFilesStubRemoteClearCount == 1);
CHECK(lgClipboardFilesStubRemoteDataset == 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(lgClipboardFilesStubRemoteClearCount == 1);
CHECK(lgClipboardFilesStubRemoteDataset == mixedDataset);
CHECK(d.notice == 3);
CHECK(d.noticeType[2] == LG_CLIPBOARD_DATA_FILES);
p.ev->notice(p.evCtx, text, 1);
CHECK(lgClipboardFilesStubRemoteClearCount == 2);
CHECK(lgClipboardFilesStubRemoteDataset == 0);
CHECK(d.notice == 4);
CHECK(d.noticeType[3] == LG_CLIPBOARD_DATA_TEXT);
lgClipboard_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);
lgClipboard_fileRemoteFailed(first);
CHECK(d.release == 0);
CHECK(lgClipboardFilesStubRemoteDataset == second);
lgClipboard_fileRemoteFailed(second);
CHECK(d.release == 1);
lgClipboard_fileRemoteFailed(second);
CHECK(d.release == 1);
p.ev->notice(p.evCtx, text, 1);
CHECK(d.notice == 2);
lgClipboardFilesStubRemoteOfferResult = false;
const unsigned previousClears = lgClipboardFilesStubRemoteClearCount;
p.ev->fileOffer(p.evCtx, second + 1U);
CHECK(lgClipboardFilesStubRemoteClearCount == 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);
lgClipboardFilesStubRemoteOfferResult = true;
const uint64_t pending = second + 2U;
p.ev->fileOffer(p.evCtx, pending);
lgClipboard_fileRemoteFailed(pending);
CHECK(d.release == 2);
p.ev->notice(p.evCtx, files, 1);
CHECK(d.notice == 3);
CHECK(d.release == 3);
lgClipboard_free();
}
struct CallbackSerializationTask
{
atomic_bool switched;
atomic_bool availabilityChanged;
};
static void * switchClipboardProvider(void * opaque)
{
struct CallbackSerializationTask * task = opaque;
lgClipboard_setTransport(&plainOps, &q);
atomic_store_explicit(&task->switched, true, memory_order_release);
return NULL;
}
static void * clearClipboardAvailability(void * opaque)
{
struct CallbackSerializationTask * task = opaque;
lgClipboard_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 };
lgClipboard_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);
lgClipboard_free();
}
static void testRequest(void)
{
init();
@@ -1129,6 +1385,10 @@ struct Test
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 },

View File

@@ -143,7 +143,7 @@ static SpiceClipboard * setup(void)
g_state.ds = &displayOps;
g_params.clipboardToVM = true;
g_params.clipboardToLocal = true;
lgClipboard_init();
CHECK(lgClipboard_init());
lgClipboard_setLocalAvailable(true);
lgClipboard_setFallback(spiceClipboard_getOps(), clipboard);
return clipboard;

View File

@@ -21,6 +21,7 @@
#include "wayland.h"
#include "test.h"
#include "../src/clipboard.h"
#include "../src/clipboard_files.h"
#include "common/debug.h"
@@ -158,6 +159,18 @@ struct Log
bool expectReleaseLocked;
bool firing;
unsigned int releaseLockedN;
unsigned int fileSetN;
unsigned int fileClearN;
unsigned int presentationAcquireN;
unsigned int presentationDeliveredN;
unsigned int presentationReleaseN;
uint64_t presentationDelivered[MAX_SOURCE];
uint64_t presentationReleased[MAX_SOURCE];
char fileMime[80];
uint8_t fileData[MAX_TRANSFER];
size_t fileSize;
bool fileSetOK;
bool presentationOK;
};
struct WaylandDSState wlWm;
@@ -556,6 +569,67 @@ bool lgClipboard_requestReady(LG_ClipboardRequest request)
return true;
}
bool lgClipboardFiles_setLocal(const char * mime,
const void * data, size_t size)
{
CHECK(mime);
CHECK(!size || data);
CHECK(size <= sizeof(rec.fileData));
++rec.fileSetN;
snprintf(rec.fileMime, sizeof(rec.fileMime), "%s", mime);
rec.fileSize = size;
if (size)
memcpy(rec.fileData, data, size);
return rec.fileSetOK;
}
void lgClipboardFiles_clearLocal(void)
{
++rec.fileClearN;
}
uint64_t lgClipboardFiles_remotePresentationAcquire(void)
{
++rec.presentationAcquireN;
return rec.presentationOK ? 1000U + rec.presentationAcquireN : 0;
}
bool lgClipboardFiles_getRemotePresentation(uint64_t presentation,
const char * mime, char ** data, size_t * size)
{
CHECK(presentation >= 1001U);
const char * value;
if (!strcmp(mime, "text/uri-list"))
value = "file:///run/user/1000/looking-glass/guest.txt\r\n";
else if (!strcmp(mime, "x-special/gnome-copied-files"))
value = "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n";
else if (!strcmp(mime, "application/x-kde-cutselection"))
value = "0";
else
return false;
*size = strlen(value);
*data = malloc(*size);
CHECK(*data);
memcpy(*data, value, *size);
return true;
}
void lgClipboardFiles_remotePresentationDelivered(uint64_t presentation)
{
CHECK(presentation);
CHECK(rec.presentationDeliveredN <
ARRAY_LENGTH(rec.presentationDelivered));
rec.presentationDelivered[rec.presentationDeliveredN++] = presentation;
}
void lgClipboardFiles_remotePresentationRelease(uint64_t presentation)
{
CHECK(presentation);
CHECK(rec.presentationReleaseN <
ARRAY_LENGTH(rec.presentationReleased));
rec.presentationReleased[rec.presentationReleaseN++] = presentation;
}
static const struct wl_data_device_listener * deviceListener(void)
{
CHECK(proto.device.listener);
@@ -620,6 +694,8 @@ static void start(void)
rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.fileSetOK = true;
rec.presentationOK = true;
proto.manager.kind = PROXY_MANAGER;
proto.device.kind = PROXY_DEVICE;
wlWm.dataDeviceManager =
@@ -697,6 +773,121 @@ static void testMime(void)
CHECK(proto.dirtyDestroyN == 0);
}
static void testFileImport(void)
{
start();
const char payload[] =
"copy\nfile:///home/user/first.txt\r\nfile:///home/user/second.txt\r\n";
proto.receiveData = payload;
proto.receiveSize = sizeof(payload) - 1U;
struct Proxy * offer = newOffer();
offerMime(offer, "text/plain;charset=utf-8");
offerMime(offer, "text/uri-list");
offerMime(offer, "x-special/gnome-copied-files");
offerMime(offer, "application/x-kde-cutselection");
selectOffer(offer);
CHECK(rec.noticeN == 0);
CHECK(proto.receiveN == 1);
CHECK(strcmp(proto.receiveMime,
"x-special/gnome-copied-files") == 0);
CHECK(rec.pollN == 1);
pollFire(0, EPOLLIN);
CHECK(rec.fileSetN == 1);
CHECK(strcmp(rec.fileMime,
"x-special/gnome-copied-files") == 0);
CHECK(rec.fileSize == sizeof(payload) - 1U);
CHECK(memcmp(rec.fileData, payload, sizeof(payload) - 1U) == 0);
finish();
}
static void testFileSource(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_FILES);
CHECK(rec.presentationAcquireN == 1);
CHECK(rec.presentationReleaseN == 0);
CHECK(proto.sourceN == 1);
struct Proxy * source = &proto.source[0];
CHECK(source->mimeN == 4);
CHECK(strcmp(source->mime[0],
"x-special/gnome-copied-files") == 0);
CHECK(strcmp(source->mime[1], "text/uri-list") == 0);
CHECK(strcmp(source->mime[2],
"application/x-kde-cutselection") == 0);
int fds[2];
CHECK(pipe(fds) == 0);
sourceListener(source)->send(source->data,
(struct wl_data_source *)source, "text/uri-list", fds[1]);
CHECK(rec.pollN == 1);
CHECK(rec.poll[0].events == EPOLLOUT);
pollFire(0, EPOLLOUT);
CHECK(rec.presentationDeliveredN == 1);
CHECK(rec.presentationDelivered[0] == 1001U);
const char expected[] =
"file:///run/user/1000/looking-glass/guest.txt\r\n";
char actual[sizeof(expected)] = { 0 };
CHECK(read(fds[0], actual, sizeof(actual)) ==
(ssize_t)(sizeof(expected) - 1U));
CHECK(memcmp(actual, expected, sizeof(expected) - 1U) == 0);
CHECK(read(fds[0], actual, 1) == 0);
CHECK(close(fds[0]) == 0);
int kde[2];
CHECK(pipe(kde) == 0);
sourceListener(source)->send(source->data,
(struct wl_data_source *)source,
"application/x-kde-cutselection", kde[1]);
CHECK(rec.pollN == 2);
pollFire(1, EPOLLOUT);
CHECK(rec.presentationDeliveredN == 1);
CHECK(read(kde[0], actual, sizeof(actual)) == 1);
CHECK(actual[0] == '0');
CHECK(read(kde[0], actual, 1) == 0);
CHECK(close(kde[0]) == 0);
sourceListener(source)->cancelled(source->data,
(struct wl_data_source *)source);
CHECK(rec.presentationReleaseN == 1);
CHECK(rec.presentationReleased[0] == 1001U);
finish();
CHECK(rec.presentationReleaseN == 1);
}
static void testFileSourceReplacement(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_FILES);
struct Proxy * first = &proto.source[0];
CHECK(rec.presentationReleaseN == 0);
int fds[2];
CHECK(pipe(fds) == 0);
sourceListener(first)->send(first->data,
(struct wl_data_source *)first, "text/uri-list", fds[1]);
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.presentationReleaseN == 0);
CHECK(((struct WCBTransfer *)first->data)->filePresentation == 1001U);
sourceListener(first)->cancelled(first->data,
(struct wl_data_source *)first);
CHECK(rec.presentationReleaseN == 0);
pollFire(0, EPOLLOUT);
CHECK(rec.presentationDeliveredN == 1);
CHECK(rec.presentationDelivered[0] == 1001U);
CHECK(rec.presentationReleaseN == 1);
CHECK(rec.presentationReleased[0] == 1001U);
char value[64];
CHECK(read(fds[0], value, sizeof(value)) > 0);
CHECK(close(fds[0]) == 0);
finish();
}
static void testSelfCopy(void)
{
start();
@@ -1381,6 +1572,9 @@ struct Test
static const struct Test tests[] =
{
{ "mime" , testMime },
{ "file-import", testFileImport },
{ "file-source", testFileSource },
{ "file-replace", testFileSourceReplacement },
{ "replace" , testReplace },
{ "read-eof" , testReadEOF },
{ "read-block", testReadBlocked },

View File

@@ -23,6 +23,7 @@
#include "test.h"
#include "x11.h"
#include "../src/clipboard.h"
#include "../src/clipboard_files.h"
#include "common/debug.h"
@@ -56,6 +57,9 @@
#define A_BMP 102UL
#define A_TIFF 103UL
#define A_JPEG 104UL
#define A_URI 105UL
#define A_GNOME 106UL
#define A_KDE 107UL
struct WindowLog
{
@@ -136,6 +140,7 @@ struct Log
unsigned int grabN;
unsigned int ungrabN;
unsigned int selectN;
unsigned int structureSelectN;
LG_ClipboardData requestType;
const LG_ClipboardStreamOps * requestStream;
void * requestOpaque;
@@ -159,6 +164,18 @@ struct Log
bool requestCancelSync;
bool requestBeginSync;
bool fixesOK;
bool fileSetOK;
bool presentationOK;
unsigned int fileSetN;
unsigned int fileClearN;
unsigned int presentationAcquireN;
unsigned int presentationDeliveredN;
unsigned int presentationReleaseN;
uint64_t presentationDelivered[8];
uint64_t presentationReleased[8];
char fileMime[80];
uint8_t fileData[MAX_TRANSFER];
size_t fileSize;
};
struct X11DSState x11;
@@ -201,6 +218,12 @@ Atom XInternAtom(Display * display, const char * name, Bool onlyIfExists)
return A_TIFF;
if (!strcmp(name, "image/jpeg"))
return A_JPEG;
if (!strcmp(name, "text/uri-list"))
return A_URI;
if (!strcmp(name, "x-special/gnome-copied-files"))
return A_GNOME;
if (!strcmp(name, "application/x-kde-cutselection"))
return A_KDE;
CHECK(false);
return None;
}
@@ -277,7 +300,10 @@ int XConvertSelection(Display * display, Atom selection, Atom target,
int XSelectInput(Display * display, Window window, long mask)
{
CHECK(display == x11.display);
CHECK(mask == PropertyChangeMask);
CHECK(mask == PropertyChangeMask ||
mask == (PropertyChangeMask | StructureNotifyMask));
if (mask & StructureNotifyMask)
++rec.structureSelectN;
CHECK(window != None);
return Success;
}
@@ -549,6 +575,67 @@ bool lgClipboard_requestReady(LG_ClipboardRequest request)
return true;
}
bool lgClipboardFiles_setLocal(const char * mime,
const void * data, size_t size)
{
CHECK(mime);
CHECK(!size || data);
CHECK(size <= sizeof(rec.fileData));
++rec.fileSetN;
snprintf(rec.fileMime, sizeof(rec.fileMime), "%s", mime);
rec.fileSize = size;
if (size)
memcpy(rec.fileData, data, size);
return rec.fileSetOK;
}
void lgClipboardFiles_clearLocal(void)
{
++rec.fileClearN;
}
uint64_t lgClipboardFiles_remotePresentationAcquire(void)
{
++rec.presentationAcquireN;
return rec.presentationOK ? 2001U : 0;
}
bool lgClipboardFiles_getRemotePresentation(uint64_t presentation,
const char * mime, char ** data, size_t * size)
{
CHECK(presentation >= 2001U);
const char * value;
if (!strcmp(mime, "text/uri-list"))
value = "file:///run/user/1000/looking-glass/guest.txt\r\n";
else if (!strcmp(mime, "x-special/gnome-copied-files"))
value = "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n";
else if (!strcmp(mime, "application/x-kde-cutselection"))
value = "0";
else
return false;
*size = strlen(value);
*data = malloc(*size);
CHECK(*data);
memcpy(*data, value, *size);
return true;
}
void lgClipboardFiles_remotePresentationDelivered(uint64_t presentation)
{
CHECK(presentation);
CHECK(rec.presentationDeliveredN <
ARRAY_LENGTH(rec.presentationDelivered));
rec.presentationDelivered[rec.presentationDeliveredN++] = presentation;
}
void lgClipboardFiles_remotePresentationRelease(uint64_t presentation)
{
CHECK(presentation);
CHECK(rec.presentationReleaseN <
ARRAY_LENGTH(rec.presentationReleased));
rec.presentationReleased[rec.presentationReleaseN++] = presentation;
}
static struct Property * prop(void)
{
CHECK(rec.propN < ARRAY_LENGTH(rec.prop));
@@ -633,6 +720,17 @@ static void owner(Window owner)
CHECK(x11CBEventThread((const XEvent *)&event));
}
static void selectionClear(Window owner)
{
rec.owner = owner;
XEvent event = {};
event.xselectionclear.type = SelectionClear;
event.xselectionclear.display = x11.display;
event.xselectionclear.window = x11.window;
event.xselectionclear.selection = x11atoms.CLIPBOARD;
CHECK(x11CBEventThread(&event));
}
static void discover(Window ownerWindow,
const unsigned long * targets, size_t count)
{
@@ -676,12 +774,14 @@ static void start(void)
memset(&x11, 0, sizeof(x11));
memset(&x11atoms, 0, sizeof(x11atoms));
memset(&rec, 0, sizeof(rec));
rec.nextWindow = 1000;
rec.requestOK = true;
rec.fixesOK = true;
rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.nextWindow = 1000;
rec.requestOK = true;
rec.fixesOK = true;
rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.fileSetOK = true;
rec.presentationOK = true;
pthread_mutex_lock(&readRaceLock);
readRacePause = false;
@@ -727,6 +827,189 @@ static void testTargets(void)
finish();
}
static void testFileImport(void)
{
start();
const unsigned long targets[] = { A_TEXT, A_URI };
discover(601, targets, ARRAY_LENGTH(targets));
CHECK(rec.convertN == 2);
const struct ConvertLog * convert = &rec.convert[1];
CHECK(convert->target == A_URI);
CHECK(convert->property == x11atoms.SEL_DATA);
CHECK(rec.noticeN == 0);
const char payload[] =
"file:///home/user/first.txt\r\nfile:///home/user/second.txt\r\n";
prop8(A_URI, payload, sizeof(payload) - 1U);
selection(convert->requestor, A_URI, x11atoms.SEL_DATA);
CHECK(rec.fileSetN == 1);
CHECK(strcmp(rec.fileMime, "text/uri-list") == 0);
CHECK(rec.fileSize == sizeof(payload) - 1U);
CHECK(memcmp(rec.fileData, payload, sizeof(payload) - 1U) == 0);
CHECK(rec.noticeN == 0);
finish();
}
static void testFileImportIncr(void)
{
start();
const unsigned long targets[] = { A_URI, A_TEXT, A_GNOME };
discover(602, targets, ARRAY_LENGTH(targets));
CHECK(rec.convertN == 2);
const struct ConvertLog * convert = &rec.convert[1];
CHECK(convert->target == A_GNOME);
const unsigned long capacity = 128;
prop32(x11atoms.INCR, &capacity, 1);
selection(convert->requestor, A_GNOME, x11atoms.SEL_DATA);
CHECK(rec.fileSetN == 0);
const char first[] = "copy\nfile:///home/user/";
const char second[] = "large%20file.txt\r\n";
prop8(A_GNOME, first, sizeof(first) - 1U);
property(convert->requestor);
CHECK(rec.fileSetN == 0);
prop8(A_GNOME, second, sizeof(second) - 1U);
property(convert->requestor);
propNull(A_GNOME, 8, 0);
property(convert->requestor);
CHECK(rec.fileSetN == 1);
CHECK(strcmp(rec.fileMime,
"x-special/gnome-copied-files") == 0);
CHECK(rec.fileSize == sizeof(first) + sizeof(second) - 2U);
CHECK(memcmp(rec.fileData,
"copy\nfile:///home/user/large%20file.txt\r\n",
rec.fileSize) == 0);
CHECK(rec.noticeN == 0);
finish();
}
static void testFileSource(void)
{
start();
x11CBNotice(LG_CLIPBOARD_DATA_FILES);
CHECK(rec.owner == x11.window);
CHECK(rec.presentationAcquireN == 1);
CHECK(rec.presentationReleaseN == 0);
selectionRequest(x11atoms.TARGETS, 710);
CHECK(rec.changeN == 1);
CHECK(rec.change[0].format == 32);
CHECK(rec.change[0].count == 4);
const Atom * targets = (const Atom *)rec.change[0].data;
CHECK(targets[0] == x11atoms.TARGETS);
CHECK(targets[1] == A_URI);
CHECK(targets[2] == A_GNOME);
CHECK(targets[3] == A_KDE);
selectionRequest(A_URI, 711);
CHECK(rec.presentationAcquireN == 2);
CHECK(rec.structureSelectN == 1);
CHECK(rec.changeN == 2);
CHECK(rec.change[1].type == x11atoms.INCR);
CHECK(rec.change[1].format == 32);
CHECK(rec.change[1].count == 1);
CHECK(rec.sendN == 2);
propertyState(900, 711, PropertyDelete);
CHECK(rec.changeN == 3);
CHECK(rec.change[2].type == A_URI);
CHECK(rec.change[2].format == 8);
const char expected[] =
"file:///run/user/1000/looking-glass/guest.txt\r\n";
CHECK(rec.change[2].size == sizeof(expected) - 1U);
CHECK(memcmp(rec.change[2].data,
expected, sizeof(expected) - 1U) == 0);
propertyState(900, 711, PropertyDelete);
CHECK(rec.changeN == 4);
CHECK(rec.change[3].type == A_URI);
CHECK(rec.change[3].count == 0);
CHECK(rec.presentationDeliveredN == 1);
CHECK(rec.presentationDelivered[0] == 2001U);
CHECK(rec.presentationReleaseN == 1);
CHECK(rec.presentationReleased[0] == 2001U);
selectionRequest(A_KDE, 712);
CHECK(rec.presentationAcquireN == 3);
CHECK(rec.structureSelectN == 2);
CHECK(rec.changeN == 5);
CHECK(rec.change[4].type == x11atoms.INCR);
propertyState(900, 712, PropertyDelete);
CHECK(rec.changeN == 6);
CHECK(rec.change[5].type == A_KDE);
CHECK(rec.change[5].size == 1);
CHECK(rec.change[5].data[0] == '0');
propertyState(900, 712, PropertyDelete);
CHECK(rec.changeN == 7);
CHECK(rec.change[6].type == A_KDE);
CHECK(rec.change[6].count == 0);
CHECK(rec.presentationDeliveredN == 1);
CHECK(rec.presentationReleaseN == 2);
CHECK(rec.presentationReleased[1] == 2001U);
x11CBNotice(LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.presentationReleaseN == 3);
CHECK(rec.presentationReleased[2] == 2001U);
finish();
CHECK(rec.presentationReleaseN == 3);
}
static void testFileSourceActiveReplacement(void)
{
start();
x11CBNotice(LG_CLIPBOARD_DATA_FILES);
selectionRequest(A_URI, 713);
CHECK(rec.changeN == 1);
CHECK(rec.change[0].type == x11atoms.INCR);
CHECK(rec.presentationAcquireN == 2);
CHECK(rec.presentationReleaseN == 0);
x11CBNotice(LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.presentationReleaseN == 1);
CHECK(rec.changeN == 1);
selectionClear(901);
CHECK(rec.presentationReleaseN == 1);
CHECK(rec.changeN == 1);
propertyState(900, 713, PropertyDelete);
CHECK(rec.changeN == 2);
CHECK(rec.change[1].type == A_URI);
CHECK(rec.change[1].size > 0);
CHECK(rec.presentationDeliveredN == 0);
propertyState(900, 713, PropertyDelete);
CHECK(rec.changeN == 3);
CHECK(rec.change[2].type == A_URI);
CHECK(rec.change[2].count == 0);
CHECK(rec.presentationDeliveredN == 1);
CHECK(rec.presentationDelivered[0] == 2001U);
CHECK(rec.presentationReleaseN == 2);
CHECK(rec.presentationReleased[1] == 2001U);
finish();
CHECK(rec.presentationReleaseN == 2);
start();
x11CBNotice(LG_CLIPBOARD_DATA_FILES);
selectionRequest(A_URI, 714);
CHECK(rec.changeN == 1);
CHECK(rec.presentationAcquireN == 2);
XEvent destroyed = {};
destroyed.xdestroywindow.type = DestroyNotify;
destroyed.xdestroywindow.display = x11.display;
destroyed.xdestroywindow.window = 900;
CHECK(x11CBEventThread(&destroyed));
CHECK(rec.changeN == 1);
CHECK(rec.presentationReleaseN == 1);
CHECK(rec.presentationDeliveredN == 0);
finish();
CHECK(rec.presentationReleaseN == 2);
}
static void testNormal(void)
{
start();
@@ -1146,18 +1429,22 @@ struct Test
static const struct Test tests[] =
{
{ "targets" , testTargets },
{ "normal" , testNormal },
{ "incr" , testIncr },
{ "incr-block", testIncrBlocked },
{ "incr-ready", testIncrReadyRace },
{ "incr-cancel", testIncrCancelRace },
{ "incr-large", testIncrLargeProperty },
{ "malformed", testMalformed },
{ "replace" , testReplace },
{ "source" , testSource },
{ "source-early", testSourceBeginDuringRequest },
{ "teardown" , testTeardown },
{ "targets" , testTargets },
{ "file-import" , testFileImport },
{ "file-incr" , testFileImportIncr },
{ "file-source" , testFileSource },
{ "file-source-active-replace", testFileSourceActiveReplacement },
{ "normal" , testNormal },
{ "incr" , testIncr },
{ "incr-block" , testIncrBlocked },
{ "incr-ready" , testIncrReadyRace },
{ "incr-cancel" , testIncrCancelRace },
{ "incr-large" , testIncrLargeProperty },
{ "malformed" , testMalformed },
{ "replace" , testReplace },
{ "source" , testSource },
{ "source-early" , testSourceBeginDuringRequest },
{ "teardown" , testTeardown },
};
int main(int argc, char ** argv)