diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 02279b9b..d37a3434 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -27,6 +27,7 @@ include(UninstallTarget) find_package(PkgConfig) pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig) +pkg_check_modules(FUSE3 REQUIRED IMPORTED_TARGET fuse3>=3.10) option(ENABLE_OPENGL "Enable the OpenGL renderer" ON) add_feature_info(ENABLE_OPENGL ENABLE_OPENGL "Legacy OpenGL renderer.") @@ -177,6 +178,7 @@ set(SOURCES src/font.c src/util.c src/clipboard.c + src/clipboard_files.c src/kb.c src/kb_hid.c src/gl_dynprocs.c @@ -252,6 +254,7 @@ add_custom_command(TARGET looking-glass-client POST_BUILD target_link_libraries(looking-glass-client ${EXE_FLAGS} PkgConfig::FONTCONFIG + PkgConfig::FUSE3 lg_resources lg_common displayservers diff --git a/client/displayservers/Wayland/clipboard.c b/client/displayservers/Wayland/clipboard.c index 0bbe818c..73ee7c8c 100644 --- a/client/displayservers/Wayland/clipboard.c +++ b/client/displayservers/Wayland/clipboard.c @@ -30,6 +30,7 @@ #include #include "../../src/clipboard.h" +#include "../../src/clipboard_files.h" #include "common/debug.h" #include "common/KVMFRClipboard.h" @@ -75,6 +76,14 @@ static const char * jpegMimetypes[] = NULL, }; +static const char * fileMimetypes[] = +{ + "x-special/gnome-copied-files", + "text/uri-list", + "application/x-kde-cutselection", + NULL, +}; + static const char ** cbTypeToMimetypes(enum LG_ClipboardData type) { switch (type) @@ -89,6 +98,8 @@ static const char ** cbTypeToMimetypes(enum LG_ClipboardData type) return tiffMimetypes; case LG_CLIPBOARD_DATA_JPEG: return jpegMimetypes; + case LG_CLIPBOARD_DATA_FILES: + return fileMimetypes; default: DEBUG_ERROR("invalid clipboard type"); abort(); @@ -141,6 +152,10 @@ static bool isTextMimetype(const char * mimetype) static enum LG_ClipboardData mimetypeToCbType(const char * mimetype) { + if (!strcmp(mimetype, "x-special/gnome-copied-files") || + !strcmp(mimetype, "text/uri-list")) + return LG_CLIPBOARD_DATA_FILES; + if (isTextMimetype(mimetype)) return LG_CLIPBOARD_DATA_TEXT; @@ -164,6 +179,7 @@ static bool isImageCbtype(enum LG_ClipboardData type) switch (type) { case LG_CLIPBOARD_DATA_TEXT: + case LG_CLIPBOARD_DATA_FILES: return false; case LG_CLIPBOARD_DATA_PNG: case LG_CLIPBOARD_DATA_BMP: @@ -244,6 +260,146 @@ static void clipboardReadCleanup(void * opaque) clipboardReadRelease(data); } +struct ClipboardFileImport +{ + int fd; + struct wl_data_offer * offer; + char * mime; + uint8_t * data; + size_t size; + size_t capacity; +}; + +static bool fileImportGrow(struct ClipboardFileImport * import, + size_t wanted) +{ + if (wanted <= import->capacity) + return true; + size_t capacity = import->capacity ? import->capacity : 4096U; + while (capacity < wanted) + { + if (capacity > SIZE_MAX / 2U) + { + capacity = wanted; + break; + } + capacity *= 2U; + } + uint8_t * data = realloc(import->data, capacity); + if (!data) + return false; + import->data = data; + import->capacity = capacity; + return true; +} + +static void fileImportCleanup(void * opaque) +{ + struct ClipboardFileImport * import = opaque; + close(import->fd); + free(import->mime); + free(import->data); + free(import); +} + +static void fileImportCallback(uint32_t events, void * opaque) +{ + struct ClipboardFileImport * import = opaque; + bool complete = false; + bool failed = (events & EPOLLERR) != 0; + while (!failed) + { + if (import->size > SIZE_MAX - 4096U) + break; + if (!fileImportGrow(import, import->size + 4096U)) + break; + const ssize_t bytes = read(import->fd, + import->data + import->size, import->capacity - import->size); + if (bytes > 0) + { + import->size += (size_t)bytes; + continue; + } + if (!bytes) + { + complete = true; + break; + } + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return; + failed = true; + } + + LG_LOCK(wlCb.lock); + const bool current = wlCb.fileImport == import && + wlCb.offer == import->offer; + if (wlCb.fileImport == import) + wlCb.fileImport = NULL; + LG_UNLOCK(wlCb.lock); + if (current && (!complete || + !lgClipboardFiles_setLocal(import->mime, + import->data, import->size))) + { + lgClipboardFiles_clearLocal(); + lgClipboard_release(); + } + if (current) + waylandPollUnregister(import->fd); +} + +static bool fileImportStart(struct wl_data_offer * offer, const char * mime) +{ + int fds[2]; + if (pipe(fds) < 0) + return false; + const int flags = fcntl(fds[0], F_GETFL); + const int readFdFlags = fcntl(fds[0], F_GETFD); + const int writeFdFlags = fcntl(fds[1], F_GETFD); + struct ClipboardFileImport * import = calloc(1, sizeof(*import)); + if (!import || flags < 0 || readFdFlags < 0 || writeFdFlags < 0 || + fcntl(fds[0], F_SETFL, flags | O_NONBLOCK) < 0 || + fcntl(fds[0], F_SETFD, readFdFlags | FD_CLOEXEC) < 0 || + fcntl(fds[1], F_SETFD, writeFdFlags | FD_CLOEXEC) < 0) + { + free(import); + close(fds[0]); + close(fds[1]); + return false; + } + import->fd = fds[0]; + import->offer = offer; + import->mime = strdup(mime); + if (!import->mime) + { + fileImportCleanup(import); + close(fds[1]); + return false; + } + wl_data_offer_receive(offer, mime, fds[1]); + close(fds[1]); + + LG_LOCK(wlCb.lock); + const bool installed = wlCb.offer == offer; + struct ClipboardFileImport * old = installed ? wlCb.fileImport : NULL; + bool registered = false; + if (installed) + { + wlCb.fileImport = import; + registered = waylandPollRegisterWithCleanup(import->fd, + fileImportCallback, import, fileImportCleanup, EPOLLIN); + if (!registered) + wlCb.fileImport = old; + } + LG_UNLOCK(wlCb.lock); + if (registered && old) + waylandPollUnregister(old->fd); + if (!registered) + fileImportCleanup(import); + return registered; +} + // Destination client handlers. static void dataOfferHandleOffer(void * opaque, struct wl_data_offer * offer, @@ -276,7 +432,14 @@ static void dataOfferHandleOffer(void * opaque, struct wl_data_offer * offer, return; if (data->mimetypes[type]) - return; + { + if (type != LG_CLIPBOARD_DATA_FILES || + strcmp(mimetype, "x-special/gnome-copied-files") || + !strcmp(data->mimetypes[type], + "x-special/gnome-copied-files")) + return; + free(data->mimetypes[type]); + } data->mimetypes[type] = strdup(mimetype); } @@ -356,6 +519,8 @@ static void dataDeviceHandleSelection(void * opaque, char * oldMimetypes[LG_CLIPBOARD_DATA_NONE]; LG_LOCK(wlCb.lock); struct ClipboardRead * read = clipboardReadTakeCurrentNL(); + struct ClipboardFileImport * oldImport = wlCb.fileImport; + wlCb.fileImport = NULL; struct wl_data_offer * oldOffer = wlCb.offer; memcpy(oldMimetypes, wlCb.mimetypes, sizeof(oldMimetypes)); wlCb.selectionSource = NULL; @@ -373,12 +538,22 @@ static void dataDeviceHandleSelection(void * opaque, clipboardReadRetire(read); lgClipboard_abort(request); } + if (oldImport) + waylandPollUnregister(oldImport->fd); if (oldOffer && oldOffer != offer) wl_data_offer_destroy(oldOffer); for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) free(oldMimetypes[i]); - - lgClipboard_notifyTypes(types, idx); + if (wlCb.mimetypes[LG_CLIPBOARD_DATA_FILES]) + { + if (!fileImportStart(offer, wlCb.mimetypes[LG_CLIPBOARD_DATA_FILES])) + { + lgClipboardFiles_clearLocal(); + lgClipboard_release(); + } + } + else + lgClipboard_notifyTypes(types, idx); } static void dataDeviceHandleEnter(void * data, struct wl_data_device * device, @@ -671,11 +846,13 @@ static void waylandCBInvalidateLocal(enum ClipboardInvalidateMode mode) return; } struct ClipboardRead * read = clipboardReadTakeCurrentNL(); + struct ClipboardFileImport * import = wlCb.fileImport; + wlCb.fileImport = NULL; struct wl_data_offer * offer = wlCb.offer; wlCb.offer = NULL; memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes)); memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes)); - const bool hadLocal = read || offer || hasAnyMimetype(mimetypes); + const bool hadLocal = read || import || offer || hasAnyMimetype(mimetypes); const bool notifyProvider = mode == CLIPBOARD_INVALIDATE_FORCE || (mode == CLIPBOARD_INVALIDATE_CONDITIONAL && hadLocal); const LG_ClipboardRequest request = read ? read->request : @@ -694,6 +871,10 @@ static void waylandCBInvalidateLocal(enum ClipboardInvalidateMode mode) } LG_UNLOCK(wlCb.lock); + if (import) + waylandPollUnregister(import->fd); + lgClipboardFiles_clearLocal(); + if (read && !notifyProvider) lgClipboard_abort(request); @@ -997,6 +1178,138 @@ static const LG_ClipboardStreamOps clipboardWriteStream = .cancel = clipboardWriteCancel, }; +struct ClipboardFileWrite +{ + int fd; + uint8_t * data; + size_t size; + size_t offset; + struct WCBTransfer * transfer; + bool deliversUri; + bool complete; +}; + +static void fileTransferDestroy(struct WCBTransfer * transfer) +{ + if (transfer->filePresentation) + lgClipboardFiles_remotePresentationRelease(transfer->filePresentation); + free(transfer->fileUri); + free(transfer->fileGnome); + free(transfer->fileKde); + free(transfer); +} + +static void fileTransferRetain(struct WCBTransfer * transfer) +{ + atomic_fetch_add_explicit( + &transfer->references, 1, memory_order_relaxed); +} + +static void fileTransferRelease(struct WCBTransfer * transfer) +{ + if (atomic_fetch_sub_explicit( + &transfer->references, 1, memory_order_acq_rel) == 1) + fileTransferDestroy(transfer); +} + +static void clipboardFileWriteCleanup(void * opaque) +{ + struct ClipboardFileWrite * write = opaque; + if (write->complete && write->deliversUri) + lgClipboardFiles_remotePresentationDelivered( + write->transfer->filePresentation); + close(write->fd); + free(write->data); + fileTransferRelease(write->transfer); + free(write); +} + +static void clipboardFileWriteCallback(uint32_t events, void * opaque) +{ + struct ClipboardFileWrite * output = opaque; + if (events & (EPOLLERR | EPOLLHUP)) + { + waylandPollUnregister(output->fd); + return; + } + while (output->offset < output->size) + { + const ssize_t bytes = write(output->fd, + output->data + output->offset, output->size - output->offset); + if (bytes > 0) + { + output->offset += (size_t)bytes; + continue; + } + if (bytes < 0 && errno == EINTR) + continue; + if (bytes < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) + return; + waylandPollUnregister(output->fd); + return; + } + output->complete = true; + waylandPollUnregister(output->fd); +} + +static bool clipboardFileWriteStart(int fd, const void * data, size_t size, + struct WCBTransfer * transfer, bool deliversUri) +{ + struct ClipboardFileWrite * output = calloc(1, sizeof(*output)); + if (!output) + return false; + output->data = size ? malloc(size) : NULL; + if (size && !output->data) + { + free(output); + return false; + } + if (size) + memcpy(output->data, data, size); + output->fd = fd; + output->size = size; + output->transfer = transfer; + output->deliversUri = deliversUri; + fileTransferRetain(transfer); + const int flags = fcntl(fd, F_GETFL); + const int fdFlags = fcntl(fd, F_GETFD); + if (flags < 0 || fdFlags < 0 || + fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0 || + fcntl(fd, F_SETFD, fdFlags | FD_CLOEXEC) < 0 || + !waylandPollRegisterWithCleanup(fd, clipboardFileWriteCallback, + output, clipboardFileWriteCleanup, EPOLLOUT)) + { + free(output->data); + fileTransferRelease(output->transfer); + free(output); + return false; + } + return true; +} + +static bool fileTransferPayload(const struct WCBTransfer * transfer, + const char * mime, const char ** data, size_t * size) +{ + if (!strcmp(mime, "text/uri-list")) + { + *data = transfer->fileUri; + *size = transfer->fileUriSize; + } + else if (!strcmp(mime, "x-special/gnome-copied-files")) + { + *data = transfer->fileGnome; + *size = transfer->fileGnomeSize; + } + else if (!strcmp(mime, "application/x-kde-cutselection")) + { + *data = transfer->fileKde; + *size = transfer->fileKdeSize; + } + else + return false; + return *data != NULL; +} + static void clipboardWriteCleanup(void * opaque) { struct ClipboardWrite * data = opaque; @@ -1092,6 +1405,19 @@ static void dataSourceHandleSend(void * data, struct wl_data_source * source, const char * mimetype, int fd) { struct WCBTransfer * transfer = (struct WCBTransfer *) data; + if (transfer->type == LG_CLIPBOARD_DATA_FILES) + { + const char * payload; + size_t size; + const bool deliversUri = !strcmp(mimetype, "text/uri-list") || + !strcmp(mimetype, "x-special/gnome-copied-files"); + if (fileTransferPayload(transfer, mimetype, &payload, &size) && + clipboardFileWriteStart(fd, payload, size, + transfer, deliversUri)) + return; + close(fd); + return; + } if (containsMimetype(transfer->mimetypes, mimetype)) { struct ClipboardWrite * data = calloc(1, sizeof(*data)); @@ -1178,7 +1504,7 @@ static void dataSourceHandleCancelled(void * data, wlCb.selectionSource = NULL; LG_UNLOCK(wlCb.lock); - free(transfer); + fileTransferRelease(transfer); wl_data_source_destroy(source); } @@ -1188,34 +1514,53 @@ static const struct wl_data_source_listener dataSourceListener = { .cancelled = dataSourceHandleCancelled, }; -static void waylandCBPublish(LG_ClipboardData type) +static bool waylandCBPublish(LG_ClipboardData type) { - struct WCBTransfer * transfer = malloc(sizeof(*transfer)); + struct WCBTransfer * transfer = calloc(1, sizeof(*transfer)); if (!transfer) { DEBUG_ERROR("Out of memory when allocating WCBTransfer"); - return; + return false; } + atomic_init(&transfer->references, 1); transfer->mimetypes = cbTypeToMimetypes(type); transfer->type = type; transfer->next = NULL; + if (type == LG_CLIPBOARD_DATA_FILES) + transfer->filePresentation = + lgClipboardFiles_remotePresentationAcquire(); + if (type == LG_CLIPBOARD_DATA_FILES && + (!transfer->filePresentation || + !lgClipboardFiles_getRemotePresentation(transfer->filePresentation, + "text/uri-list", + &transfer->fileUri, &transfer->fileUriSize) || + !lgClipboardFiles_getRemotePresentation(transfer->filePresentation, + "x-special/gnome-copied-files", + &transfer->fileGnome, &transfer->fileGnomeSize) || + !lgClipboardFiles_getRemotePresentation(transfer->filePresentation, + "application/x-kde-cutselection", + &transfer->fileKde, &transfer->fileKdeSize))) + { + fileTransferRelease(transfer); + return false; + } struct wl_data_source * source = wl_data_device_manager_create_data_source(wlWm.dataDeviceManager); if (!source) { DEBUG_ERROR("Failed to create clipboard data source"); - free(transfer); - return; + fileTransferRelease(transfer); + return false; } transfer->source = source; if (wl_data_source_add_listener(source, &dataSourceListener, transfer) < 0) { DEBUG_ERROR("Failed to listen to clipboard data source"); wl_data_source_destroy(source); - free(transfer); - return; + fileTransferRelease(transfer); + return false; } for (const char ** mimetype = transfer->mimetypes; *mimetype; mimetype++) wl_data_source_offer(source, *mimetype); @@ -1230,17 +1575,19 @@ static void waylandCBPublish(LG_ClipboardData type) wl_data_device_set_selection(wlCb.dataDevice, source, wlWm.keyboardEnterSerial); LG_UNLOCK(wlCb.lock); - return; + return true; } LG_UNLOCK(wlCb.lock); wl_data_source_destroy(source); - free(transfer); + fileTransferRelease(transfer); + return false; } void waylandCBNotice(LG_ClipboardData type) { - waylandCBPublish(type); + if (!waylandCBPublish(type)) + waylandCBRelease(); } void waylandCBRelease(void) @@ -1260,6 +1607,7 @@ void waylandCBFree(void) char * mimetypes[LG_CLIPBOARD_DATA_NONE]; LG_LOCK(wlCb.lock); struct ClipboardRead * read = clipboardReadTakeCurrentNL(); + struct ClipboardFileImport * import = wlCb.fileImport; struct wl_data_offer * offer = wlCb.offer; struct wl_data_offer * dndOffer = wlCb.dndOffer; struct wl_data_device * dataDevice = wlCb.dataDevice; @@ -1267,16 +1615,20 @@ void waylandCBFree(void) if (dataDevice && wlCb.selectionSource) wl_data_device_set_selection(dataDevice, NULL, wlWm.keyboardEnterSerial); - wlCb.offer = NULL; - wlCb.dndOffer = NULL; - wlCb.dataDevice = NULL; + wlCb.offer = NULL; + wlCb.dndOffer = NULL; + wlCb.dataDevice = NULL; wlCb.selectionSource = NULL; wlCb.sources = NULL; + wlCb.fileImport = NULL; memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes)); memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes)); LG_UNLOCK(wlCb.lock); clipboardReadRetire(read); + if (import) + waylandPollUnregister(import->fd); + lgClipboardFiles_clearLocal(); if (offer) wl_data_offer_destroy(offer); if (dndOffer && dndOffer != offer) @@ -1287,7 +1639,7 @@ void waylandCBFree(void) { struct WCBTransfer * next = sources->next; wl_data_source_destroy(sources->source); - free(sources); + fileTransferRelease(sources); sources = next; } for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index 477c4481..ce0f8c46 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -287,12 +287,22 @@ struct WaylandDSState struct WCBTransfer { - LG_ClipboardData type; + atomic_uint references; + LG_ClipboardData type; const char ** mimetypes; struct wl_data_source * source; struct WCBTransfer * next; + uint64_t filePresentation; + char * fileUri; + size_t fileUriSize; + char * fileGnome; + size_t fileGnomeSize; + char * fileKde; + size_t fileKdeSize; }; +struct ClipboardFileImport; + struct ClipboardRead { atomic_uint references; @@ -311,18 +321,19 @@ struct ClipboardRead struct WCBState { - struct wl_data_device * dataDevice; - char lgMimetype[64]; + struct wl_data_device * dataDevice; + char lgMimetype[64]; - char * mimetypes[LG_CLIPBOARD_DATA_NONE]; - struct wl_data_offer * offer; - struct wl_data_offer * dndOffer; + char * mimetypes[LG_CLIPBOARD_DATA_NONE]; + struct wl_data_offer * offer; + struct wl_data_offer * dndOffer; - struct wl_data_source * selectionSource; - struct WCBTransfer * sources; + struct wl_data_source * selectionSource; + struct WCBTransfer * sources; - LG_Lock lock; - struct ClipboardRead * currentRead; + LG_Lock lock; + struct ClipboardRead * currentRead; + struct ClipboardFileImport * fileImport; }; extern struct WaylandDSState wlWm; diff --git a/client/displayservers/X11/clipboard.c b/client/displayservers/X11/clipboard.c index f8413ff9..7cf02785 100644 --- a/client/displayservers/X11/clipboard.c +++ b/client/displayservers/X11/clipboard.c @@ -29,6 +29,7 @@ #include #include "../../src/clipboard.h" +#include "../../src/clipboard_files.h" #include "common/array.h" #include "common/debug.h" #include "common/KVMFRClipboard.h" @@ -67,19 +68,39 @@ struct X11ClipboardWrite bool ready; bool endPending; bool creating; + bool file; + uint8_t * fileData; + size_t fileSize; + uint64_t filePresentation; +}; + +struct X11ClipboardFileImport +{ + Window window; + Atom target; + const char * mime; + long propertyOffset; + uint8_t * data; + size_t size; + size_t capacity; + bool incremental; }; struct X11ClipboardState { - LG_Lock lock; - Atom aCurSelection; - Atom aTypes[LG_CLIPBOARD_DATA_NONE]; - Window targetsWindow; - LG_ClipboardData type; - bool haveRequest; + LG_Lock lock; + Atom aCurSelection; + Atom aTypes[LG_CLIPBOARD_DATA_NONE]; + Atom aFileGnome; + Atom aFileKde; + Window targetsWindow; + LG_ClipboardData type; + bool haveRequest; + uint64_t filePresentation; - struct X11ClipboardRead read; - struct X11ClipboardWrite * writes; + struct X11ClipboardRead read; + struct X11ClipboardWrite * writes; + struct X11ClipboardFileImport fileImport; }; static const char * atomTypes[] = @@ -88,9 +109,14 @@ static const char * atomTypes[] = "image/png", "image/bmp", "image/tiff", - "image/jpeg" + "image/jpeg", + "text/uri-list", }; +static const char fileGnomeMime[] = "x-special/gnome-copied-files"; +static const char fileKdeMime[] = "application/x-kde-cutselection"; +static const char fileUriMime[] = "text/uri-list"; + static struct X11ClipboardState x11cb; // forwards @@ -104,6 +130,23 @@ static bool writeLinkedNL(const struct X11ClipboardWrite * write); static void writeRemoveNL(struct X11ClipboardWrite * write); static bool advanceReadPropertyNL(unsigned long units); static void clearTargetsNL(void); +static void clearFileImportNL(void); +static struct X11ClipboardWrite * takeFileWritesNL(void); +static struct X11ClipboardWrite * takeFileWritesForWindowNL(Window window); +static void cancelFileWrites( + struct X11ClipboardWrite * writes, bool terminate); +static void x11CBFileImportIncr(const XPropertyEvent e); + +static const char * fileMimeForAtom(Atom atom) +{ + if (atom == x11cb.aTypes[LG_CLIPBOARD_DATA_FILES]) + return fileUriMime; + if (atom == x11cb.aFileGnome) + return fileGnomeMime; + if (atom == x11cb.aFileKde) + return fileKdeMime; + return NULL; +} bool x11CBEventThread(const XEvent * xe) { @@ -121,11 +164,30 @@ bool x11CBEventThread(const XEvent * xe) x11CBSelectionNotify(xe->xselection); return true; + case DestroyNotify: + { + LG_LOCK(x11cb.lock); + struct X11ClipboardWrite * writes = + takeFileWritesForWindowNL(xe->xdestroywindow.window); + LG_UNLOCK(x11cb.lock); + if (!writes) + return false; + cancelFileWrites(writes, false); + return true; + } + case PropertyNotify: if (xe->xproperty.state == PropertyNewValue && xe->xproperty.atom == x11atoms.SEL_DATA) { - x11CBSelectionIncr(xe->xproperty); + LG_LOCK(x11cb.lock); + const bool fileImport = x11cb.fileImport.window && + xe->xproperty.window == x11cb.fileImport.window; + LG_UNLOCK(x11cb.lock); + if (fileImport) + x11CBFileImportIncr(xe->xproperty); + else + x11CBSelectionIncr(xe->xproperty); return true; } if (xe->xproperty.state == PropertyDelete) @@ -134,12 +196,47 @@ bool x11CBEventThread(const XEvent * xe) LG_LOCK(x11cb.lock); struct X11ClipboardWrite * write; for (write = x11cb.writes; write; write = write->next) - if (write->request != LG_CLIPBOARD_REQUEST_INVALID && - xe->xproperty.window == write->event.xselection.requestor && + if (xe->xproperty.window == write->event.xselection.requestor && xe->xproperty.atom == write->event.xselection.property) break; if (write) { + if (write->file) + { + const size_t remaining = write->fileSize - (size_t)write->offset; + if (remaining) + { + const size_t chunk = remaining > KVMFR_CLIPBOARD_DATA_BYTES ? + KVMFR_CLIPBOARD_DATA_BYTES : remaining; + XChangeProperty(x11.display, + write->event.xselection.requestor, + write->event.xselection.property, + write->event.xselection.target, 8, PropModeReplace, + write->fileData + (size_t)write->offset, (int)chunk); + write->offset += chunk; + XFlush(x11.display); + LG_UNLOCK(x11cb.lock); + return true; + } + + XChangeProperty(x11.display, + write->event.xselection.requestor, + write->event.xselection.property, + write->event.xselection.target, 8, PropModeReplace, + NULL, 0); + XFlush(x11.display); + if (write->event.xselection.target != x11cb.aFileKde) + lgClipboardFiles_remotePresentationDelivered( + write->filePresentation); + const uint64_t presentation = write->filePresentation; + writeRemoveNL(write); + LG_UNLOCK(x11cb.lock); + lgClipboardFiles_remotePresentationRelease(presentation); + free(write->fileData); + free(write); + return true; + } + write->ready = true; if (write->blocked) { @@ -186,6 +283,15 @@ bool x11CBInit(void) } } + x11cb.aFileGnome = XInternAtom(x11.display, fileGnomeMime, False); + x11cb.aFileKde = XInternAtom(x11.display, fileKdeMime, False); + if (x11cb.aFileGnome == BadAlloc || x11cb.aFileGnome == BadValue || + x11cb.aFileKde == BadAlloc || x11cb.aFileKde == BadValue) + { + DEBUG_ERROR("failed to get clipboard file atoms"); + return false; + } + // use xfixes to get clipboard change notifications if (!XFixesQueryExtension(x11.display, &x11.eventBase, &x11.errorBase)) { @@ -372,9 +478,15 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e) // target list requested if (e.target == x11atoms.TARGETS) { - Atom targets[2]; + Atom targets[4]; targets[0] = x11atoms.TARGETS; targets[1] = x11cb.aTypes[requestType]; + int count = 2; + if (requestType == LG_CLIPBOARD_DATA_FILES) + { + targets[count++] = x11cb.aFileGnome; + targets[count++] = x11cb.aFileKde; + } XChangeProperty( e.display, @@ -384,12 +496,98 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e) 32, PropModeReplace, (unsigned char*)targets, - ARRAY_LENGTH(targets) + count ); goto send; } + const char * fileMime = fileMimeForAtom(e.target); + if (requestType == LG_CLIPBOARD_DATA_FILES && fileMime) + { + char * data = NULL; + size_t size = 0; + uint64_t writePresentation = 0; + LG_LOCK(x11cb.lock); + const uint64_t presentation = x11cb.filePresentation; + bool available = x11cb.haveRequest && + x11cb.type == LG_CLIPBOARD_DATA_FILES && presentation; + if (available) + { + writePresentation = + lgClipboardFiles_remotePresentationAcquire(); + available = writePresentation == presentation && + lgClipboardFiles_getRemotePresentation(writePresentation, + fileMime, &data, &size); + } + LG_UNLOCK(x11cb.lock); + if (!available) + { + free(data); + if (writePresentation) + lgClipboardFiles_remotePresentationRelease(writePresentation); + goto nodata; + } + + struct X11ClipboardWrite * write = calloc(1, sizeof(*write)); + if (!write) + { + free(data); + lgClipboardFiles_remotePresentationRelease(writePresentation); + DEBUG_ERROR("out of memory"); + goto nodata; + } + write->event = *s; + write->request = LG_CLIPBOARD_REQUEST_INVALID; + write->type = LG_CLIPBOARD_DATA_FILES; + write->file = true; + write->fileData = (uint8_t *)data; + write->fileSize = size; + write->filePresentation = writePresentation; + write->begun = true; + + LG_LOCK(x11cb.lock); + bool duplicate = false; + for (struct X11ClipboardWrite * current = x11cb.writes; + current; current = current->next) + { + duplicate = current->event.xselection.requestor == e.requestor && + current->event.xselection.property == e.property; + if (duplicate) + break; + } + if (!duplicate && x11cb.haveRequest && + x11cb.type == LG_CLIPBOARD_DATA_FILES && + x11cb.filePresentation == presentation) + { + write->next = x11cb.writes; + x11cb.writes = write; + const unsigned long hint = size > UINT32_MAX ? + UINT32_MAX : (unsigned long)size; + XChangeProperty(x11.display, e.requestor, e.property, + x11atoms.INCR, 32, PropModeReplace, + (const unsigned char *)&hint, 1); + XSelectInput(x11.display, e.requestor, + PropertyChangeMask | StructureNotifyMask); + } + else + duplicate = true; + LG_UNLOCK(x11cb.lock); + + if (duplicate) + { + free(write->fileData); + lgClipboardFiles_remotePresentationRelease( + write->filePresentation); + free(write); + goto nodata; + } + + x11CBWriteSend(s); + free(s); + return; + } + // look to see if we can satisfy the data type for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) if (x11cb.aTypes[i] == e.target && requestType == i) @@ -467,7 +665,22 @@ send: static void x11CBSelectionClear(const XSelectionClearEvent e) { - (void)e; + if (e.selection != x11atoms.CLIPBOARD) + return; + + LG_LOCK(x11cb.lock); + if (XGetSelectionOwner(x11.display, x11atoms.CLIPBOARD) == x11.window) + { + LG_UNLOCK(x11cb.lock); + return; + } + x11cb.haveRequest = false; + const uint64_t presentation = x11cb.filePresentation; + x11cb.filePresentation = 0; + LG_UNLOCK(x11cb.lock); + + if (presentation) + lgClipboardFiles_remotePresentationRelease(presentation); } /* x11cb.lock must be held. */ @@ -531,6 +744,298 @@ static void clearTargetsNL(void) x11cb.targetsWindow = 0; } +/* x11cb.lock must be held. */ +static struct X11ClipboardFileImport takeFileImportNL(void) +{ + const struct X11ClipboardFileImport import = x11cb.fileImport; + x11cb.fileImport = (struct X11ClipboardFileImport) { 0 }; + if (import.window) + XDestroyWindow(x11.display, import.window); + return import; +} + +/* x11cb.lock must be held. */ +static void clearFileImportNL(void) +{ + const struct X11ClipboardFileImport import = takeFileImportNL(); + free(import.data); +} + +/* x11cb.lock must be held. */ +static struct X11ClipboardWrite * takeFileWritesNL(void) +{ + struct X11ClipboardWrite * result = NULL; + struct X11ClipboardWrite ** link = &x11cb.writes; + while (*link) + { + struct X11ClipboardWrite * write = *link; + if (!write->file) + { + link = &write->next; + continue; + } + + *link = write->next; + write->next = result; + result = write; + } + return result; +} + +/* x11cb.lock must be held. */ +static struct X11ClipboardWrite * takeFileWritesForWindowNL(Window window) +{ + struct X11ClipboardWrite * result = NULL; + struct X11ClipboardWrite ** link = &x11cb.writes; + while (*link) + { + struct X11ClipboardWrite * write = *link; + if (!write->file || + write->event.xselection.requestor != window) + { + link = &write->next; + continue; + } + + *link = write->next; + write->next = result; + result = write; + } + return result; +} + +static void cancelFileWrites( + struct X11ClipboardWrite * writes, bool terminate) +{ + const bool flush = terminate && writes; + while (writes) + { + struct X11ClipboardWrite * next = writes->next; + if (terminate) + XChangeProperty(x11.display, writes->event.xselection.requestor, + writes->event.xselection.property, + writes->event.xselection.target, 8, PropModeReplace, NULL, 0); + lgClipboardFiles_remotePresentationRelease( + writes->filePresentation); + free(writes->fileData); + free(writes); + writes = next; + } + if (flush) + XFlush(x11.display); +} + +static bool fileImportGrowNL(size_t wanted) +{ + if (wanted <= x11cb.fileImport.capacity) + return true; + size_t capacity = x11cb.fileImport.capacity ? + x11cb.fileImport.capacity : 4096U; + while (capacity < wanted) + { + if (capacity > SIZE_MAX / 2U) + { + capacity = wanted; + break; + } + capacity *= 2U; + } + uint8_t * data = realloc(x11cb.fileImport.data, capacity); + if (!data) + return false; + x11cb.fileImport.data = data; + x11cb.fileImport.capacity = capacity; + return true; +} + +static bool startFileImport(Atom target) +{ + const char * mime = fileMimeForAtom(target); + if (!mime) + return false; + + LG_LOCK(x11cb.lock); + clearFileImportNL(); + if (x11cb.aCurSelection == BadValue) + { + LG_UNLOCK(x11cb.lock); + return false; + } + + const Window window = XCreateSimpleWindow( + x11.display, x11.window, 0, 0, 1, 1, 0, 0, 0); + if (!window) + { + LG_UNLOCK(x11cb.lock); + return false; + } + + x11cb.fileImport = (struct X11ClipboardFileImport) + { + .window = window, + .target = target, + .mime = mime, + }; + XSelectInput(x11.display, window, PropertyChangeMask); + XConvertSelection(x11.display, x11cb.aCurSelection, target, + x11atoms.SEL_DATA, window, CurrentTime); + XFlush(x11.display); + LG_UNLOCK(x11cb.lock); + return true; +} + +static void finishFileImport(struct X11ClipboardFileImport import, + const void * data, size_t size) +{ + if (!lgClipboardFiles_setLocal(import.mime, data, size)) + { + lgClipboardFiles_clearLocal(); + lgClipboard_release(); + } + free(import.data); +} + +static bool x11CBFileImportSelectionNotify(const XSelectionEvent e) +{ + Atom type; + int format; + unsigned long itemCount; + unsigned long after; + unsigned char * data = NULL; + + LG_LOCK(x11cb.lock); + if (!x11cb.fileImport.window || e.requestor != x11cb.fileImport.window) + { + LG_UNLOCK(x11cb.lock); + return false; + } + + if (e.target != x11cb.fileImport.target || + e.property != x11atoms.SEL_DATA || + XGetWindowProperty(e.display, e.requestor, e.property, 0, ~0L, + True, AnyPropertyType, &type, &format, &itemCount, &after, + &data) != Success) + { + const struct X11ClipboardFileImport failed = takeFileImportNL(); + LG_UNLOCK(x11cb.lock); + if (data) + XFree(data); + free(failed.data); + lgClipboard_release(); + return true; + } + + if (type == x11atoms.INCR) + { + if (!data || format != 32 || itemCount < 1 || after) + { + const struct X11ClipboardFileImport failed = takeFileImportNL(); + LG_UNLOCK(x11cb.lock); + if (data) + XFree(data); + free(failed.data); + lgClipboard_release(); + return true; + } + x11cb.fileImport.incremental = true; + XFree(data); + LG_UNLOCK(x11cb.lock); + return true; + } + + const bool valid = type == x11cb.fileImport.target && format == 8 && + !after && (!itemCount || data); + const struct X11ClipboardFileImport import = takeFileImportNL(); + LG_UNLOCK(x11cb.lock); + if (valid) + finishFileImport(import, data, itemCount); + else + { + free(import.data); + lgClipboard_release(); + } + if (data) + XFree(data); + return true; +} + +static void x11CBFileImportIncr(const XPropertyEvent e) +{ + Atom type; + int format; + unsigned long itemCount; + unsigned long after; + unsigned char * data = NULL; + + LG_LOCK(x11cb.lock); + if (!x11cb.fileImport.window || !x11cb.fileImport.incremental || + e.window != x11cb.fileImport.window || e.atom != x11atoms.SEL_DATA) + { + LG_UNLOCK(x11cb.lock); + return; + } + +readProperty: + if (XGetWindowProperty(e.display, e.window, e.atom, + x11cb.fileImport.propertyOffset, + (KVMFR_CLIPBOARD_DATA_BYTES + 3U) / 4U, + True, AnyPropertyType, &type, &format, &itemCount, &after, + &data) != Success || (itemCount && !data) || (!itemCount && after) || + type != x11cb.fileImport.target || format != 8 || + itemCount > SIZE_MAX || + (size_t)itemCount > SIZE_MAX - x11cb.fileImport.size || + !fileImportGrowNL(x11cb.fileImport.size + (size_t)itemCount)) + { + const struct X11ClipboardFileImport failed = takeFileImportNL(); + LG_UNLOCK(x11cb.lock); + if (data) + XFree(data); + free(failed.data); + lgClipboard_release(); + return; + } + + if (itemCount) + { + memcpy(x11cb.fileImport.data + x11cb.fileImport.size, + data, (size_t)itemCount); + x11cb.fileImport.size += (size_t)itemCount; + } + if (data) + { + XFree(data); + data = NULL; + } + + if (after) + { + const unsigned long units = itemCount / 4U + + (itemCount % 4U != 0); + if (units > LONG_MAX || + x11cb.fileImport.propertyOffset > LONG_MAX - (long)units) + { + const struct X11ClipboardFileImport failed = takeFileImportNL(); + LG_UNLOCK(x11cb.lock); + free(failed.data); + lgClipboard_release(); + return; + } + x11cb.fileImport.propertyOffset += (long)units; + goto readProperty; + } + + x11cb.fileImport.propertyOffset = 0; + if (itemCount) + { + LG_UNLOCK(x11cb.lock); + return; + } + + const struct X11ClipboardFileImport import = takeFileImportNL(); + LG_UNLOCK(x11cb.lock); + finishFileImport(import, import.data, import.size); +} + enum X11ClipboardReadOperation { X11_CLIPBOARD_READ_BEGIN, @@ -781,6 +1286,10 @@ static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) const LG_ClipboardRequest oldRequest = x11cb.read.window ? cancelReadNL(true) : LG_CLIPBOARD_REQUEST_INVALID; clearTargetsNL(); + clearFileImportNL(); + x11cb.haveRequest = false; + const uint64_t presentation = x11cb.filePresentation; + x11cb.filePresentation = 0; if (e.owner == 0) { @@ -790,6 +1299,9 @@ static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) lgClipboard_abort(oldRequest); + if (presentation) + lgClipboardFiles_remotePresentationRelease(presentation); + lgClipboardFiles_clearLocal(); lgClipboard_release(); return; } @@ -806,6 +1318,9 @@ static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) lgClipboard_abort(oldRequest); + if (presentation) + lgClipboardFiles_remotePresentationRelease(presentation); + lgClipboardFiles_clearLocal(); lgClipboard_release(); return; } @@ -823,6 +1338,9 @@ static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) lgClipboard_abort(oldRequest); + if (presentation) + lgClipboardFiles_remotePresentationRelease(presentation); + lgClipboardFiles_clearLocal(); } static void x11CBSelectionNotify(const XSelectionEvent e) @@ -833,6 +1351,9 @@ static void x11CBSelectionNotify(const XSelectionEvent e) unsigned long after; unsigned char * data = NULL; + if (x11CBFileImportSelectionNotify(e)) + return; + LG_LOCK(x11cb.lock); const bool targetReply = x11cb.targetsWindow && e.requestor == x11cb.targetsWindow && e.target == x11atoms.TARGETS; @@ -881,6 +1402,29 @@ static void x11CBSelectionNotify(const XSelectionEvent e) // see if we support any of the targets listed const Atom * targets = (const Atom *)data; + Atom fileTarget = None; + for (unsigned long i = 0; i < itemCount; ++i) + if (targets[i] == x11cb.aFileGnome) + { + fileTarget = x11cb.aFileGnome; + break; + } + if (fileTarget == None) + for (unsigned long i = 0; i < itemCount; ++i) + if (targets[i] == x11cb.aTypes[LG_CLIPBOARD_DATA_FILES]) + { + fileTarget = x11cb.aTypes[LG_CLIPBOARD_DATA_FILES]; + break; + } + + if (fileTarget != None) + { + lgClipboardFiles_clearLocal(); + if (!startFileImport(fileTarget)) + lgClipboard_release(); + goto out; + } + for(int n = 0; n < LG_CLIPBOARD_DATA_NONE; ++n) for(unsigned long i = 0; i < itemCount; ++i) if (x11cb.aTypes[n] == targets[i]) @@ -1010,24 +1554,35 @@ out: void x11CBNotice(LG_ClipboardData type) { + const uint64_t nextPresentation = type == LG_CLIPBOARD_DATA_FILES ? + lgClipboardFiles_remotePresentationAcquire() : 0; LG_LOCK(x11cb.lock); const LG_ClipboardRequest oldRequest = x11cb.read.window ? cancelReadNL(true) : LG_CLIPBOARD_REQUEST_INVALID; clearTargetsNL(); - x11cb.aCurSelection = BadValue; - x11cb.haveRequest = true; - x11cb.type = type; - XSetSelectionOwner(x11.display, x11atoms.CLIPBOARD, x11.window, CurrentTime); + clearFileImportNL(); + const uint64_t oldPresentation = x11cb.filePresentation; + x11cb.filePresentation = nextPresentation; + x11cb.aCurSelection = BadValue; + x11cb.haveRequest = type != LG_CLIPBOARD_DATA_FILES || nextPresentation; + x11cb.type = type; + XSetSelectionOwner(x11.display, x11atoms.CLIPBOARD, + x11cb.haveRequest ? x11.window : None, CurrentTime); XFlush(x11.display); LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) lgClipboard_abort(oldRequest); + if (oldPresentation) + lgClipboardFiles_remotePresentationRelease(oldPresentation); + lgClipboardFiles_clearLocal(); } void x11CBRelease(void) { LG_LOCK(x11cb.lock); x11cb.haveRequest = false; + const uint64_t presentation = x11cb.filePresentation; + x11cb.filePresentation = 0; XGrabServer(x11.display); if (XGetSelectionOwner(x11.display, x11atoms.CLIPBOARD) == x11.window) XSetSelectionOwner( @@ -1035,6 +1590,26 @@ void x11CBRelease(void) XUngrabServer(x11.display); XFlush(x11.display); LG_UNLOCK(x11cb.lock); + if (presentation) + lgClipboardFiles_remotePresentationRelease(presentation); +} + +void x11CBFree(void) +{ + x11CBRelease(); + + LG_LOCK(x11cb.lock); + const LG_ClipboardRequest request = x11cb.read.window ? + cancelReadNL(true) : LG_CLIPBOARD_REQUEST_INVALID; + clearTargetsNL(); + clearFileImportNL(); + struct X11ClipboardWrite * writes = takeFileWritesNL(); + LG_UNLOCK(x11cb.lock); + + if (request != LG_CLIPBOARD_REQUEST_INVALID) + lgClipboard_abort(request); + cancelFileWrites(writes, true); + lgClipboardFiles_clearLocal(); } void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type) diff --git a/client/displayservers/X11/clipboard.h b/client/displayservers/X11/clipboard.h index faf42ff7..aa8e759e 100644 --- a/client/displayservers/X11/clipboard.h +++ b/client/displayservers/X11/clipboard.h @@ -29,6 +29,7 @@ bool x11CBEventThread(const XEvent * xe); bool x11CBInit(void); +void x11CBFree(void); void x11CBNotice(LG_ClipboardData type); void x11CBRelease(void); void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type); diff --git a/client/displayservers/X11/x11.c b/client/displayservers/X11/x11.c index d4f7ba31..2da62203 100644 --- a/client/displayservers/X11/x11.c +++ b/client/displayservers/X11/x11.c @@ -980,6 +980,7 @@ static void x11Shutdown(void) static void x11Free(void) { x11Shutdown(); + x11CBFree(); if (x11.jitRender) { diff --git a/client/include/interface/clipboard.h b/client/include/interface/clipboard.h index bea00bf6..d19ec61a 100644 --- a/client/include/interface/clipboard.h +++ b/client/include/interface/clipboard.h @@ -32,6 +32,7 @@ typedef enum LG_ClipboardData LG_CLIPBOARD_DATA_BMP, LG_CLIPBOARD_DATA_TIFF, LG_CLIPBOARD_DATA_JPEG, + LG_CLIPBOARD_DATA_FILES, LG_CLIPBOARD_DATA_NONE } @@ -65,6 +66,42 @@ typedef enum LG_ClipboardCancelReason } LG_ClipboardCancelReason; +typedef enum LG_ClipboardFileOperation +{ + LG_CLIPBOARD_FILE_LIST = 1, + LG_CLIPBOARD_FILE_READ = 2, +} +LG_ClipboardFileOperation; + +typedef enum LG_ClipboardFileError +{ + LG_CLIPBOARD_FILE_ERROR_NONE = 0, + LG_CLIPBOARD_FILE_ERROR_NOT_FOUND, + LG_CLIPBOARD_FILE_ERROR_ACCESS, + LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY, + LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY, + LG_CLIPBOARD_FILE_ERROR_IO, + LG_CLIPBOARD_FILE_ERROR_INVALID, + LG_CLIPBOARD_FILE_ERROR_NO_MEMORY, + LG_CLIPBOARD_FILE_ERROR_NO_SPACE, + LG_CLIPBOARD_FILE_ERROR_DISCONNECTED, + LG_CLIPBOARD_FILE_ERROR_CANCELLED, + LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED, + LG_CLIPBOARD_FILE_ERROR_STALE, +} +LG_ClipboardFileError; + +typedef struct LG_ClipboardFileRequest +{ + uint64_t dataset; + uint64_t request; + uint64_t node; + uint64_t offset; + uint32_t length; + LG_ClipboardFileOperation operation; +} +LG_ClipboardFileRequest; + /* A consumer of a provider-to-client stream. Callbacks are serialized and * buffers are borrowed only for the duration of chunk(). A callback which * returns BLOCKED consumes nothing; the consumer must subsequently call @@ -116,6 +153,28 @@ typedef struct LG_ClipboardEventOps void (*release)(void * opaque); bool (*request)(void * opaque, LG_ClipboardRequest request, LG_ClipboardData type); + + /* File datasets use independent, multiplexed request streams. Dataset and + * node identifiers are opaque outside the publisher. */ + void (*fileOffer)(void * opaque, uint64_t dataset); + void (*fileAcquire)(void * opaque, uint64_t dataset, + uint64_t acquisition); + void (*fileAcquired)(void * opaque, uint64_t dataset, + uint64_t acquisition, LG_ClipboardFileError error); + void (*fileRelease)(void * opaque, uint64_t dataset, + uint64_t acquisition); + void (*fileRequest)(void * opaque, + const LG_ClipboardFileRequest * request); + LG_ClipboardResult (*fileDataBegin)(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t sizeHint); + LG_ClipboardResult (*fileDataChunk)(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t responseOffset, + const void * data, size_t size); + LG_ClipboardResult (*fileDataEnd)(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t finalSize); + void (*fileDataReady)(void * opaque, uint64_t request); + void (*fileCancel)(void * opaque, uint64_t dataset, + uint64_t request, LG_ClipboardFileError reason); } LG_ClipboardEventOps; @@ -144,6 +203,9 @@ typedef struct LG_ClipboardOps /* All outbound arrays and data are borrowed only until the call returns. */ bool (*notifyTypes)(void * opaque, const LG_ClipboardData types[], size_t count); + /* Publishes an immutable local file dataset. The nonzero dataset ID is + * process-unique and is used as the wire clipboard generation. */ + bool (*offerFiles)(void * opaque, uint64_t dataset); /* Data is a complete response to a remote request. NONE with no payload * reports that the request could not be completed. This operation is the * legacy whole-buffer alternative to the stream operation group below. */ @@ -174,6 +236,24 @@ typedef struct LG_ClipboardOps * or release first. */ bool (*request)(void * opaque, LG_ClipboardRequest request, LG_ClipboardData type); + + bool (*fileAcquire)(void * opaque, uint64_t dataset, + uint64_t acquisition); + bool (*fileAcquired)(void * opaque, uint64_t dataset, + uint64_t acquisition, LG_ClipboardFileError error); + bool (*fileRelease)(void * opaque, uint64_t dataset, + uint64_t acquisition); + bool (*fileRequest)(void * opaque, + const LG_ClipboardFileRequest * request); + LG_ClipboardResult (*fileDataBegin)(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t sizeHint); + LG_ClipboardResult (*fileDataChunk)(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t responseOffset, + const void * data, size_t size); + LG_ClipboardResult (*fileDataEnd)(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t finalSize); + bool (*fileCancel)(void * opaque, uint64_t dataset, + uint64_t request, LG_ClipboardFileError reason); } LG_ClipboardOps; diff --git a/client/src/clipboard.c b/client/src/clipboard.c index fa77eeff..411b1008 100644 --- a/client/src/clipboard.c +++ b/client/src/clipboard.c @@ -19,6 +19,7 @@ */ #include "clipboard.h" +#include "clipboard_files.h" #include "main.h" #include "common/debug.h" @@ -83,6 +84,7 @@ static struct LG_Lock requestLock; LG_Lock writeLock; LG_Lock callbackLock; + LG_Lock fileLock; ClipboardBinding fallback; ClipboardBinding transport; @@ -91,10 +93,12 @@ static struct bool localAvailable; LG_ClipboardData localTypes[LG_CLIPBOARD_DATA_NONE]; size_t localTypeCount; + uint64_t localFileDataset; bool remoteNotice; LG_ClipboardData remoteType; uint32_t remoteGeneration; + uint64_t remoteFileDataset; bool remoteRequest; ClipboardBinding remoteRequestBinding; @@ -371,18 +375,22 @@ static void resetRemote(void) bool release; LG_ClipboardRequest transfer; + LG_LOCK(clipboard.fileLock); LG_LOCK(clipboard.requestLock); LG_LOCK(clipboard.stateLock); release = clipboard.remoteNotice && clipboard.localAvailable && g_params.clipboardToLocal; - clipboard.remoteNotice = false; - clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; - clipboard.remoteGeneration = nextGeneration( + clipboard.remoteNotice = false; + clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; + clipboard.remoteFileDataset = 0; + clipboard.remoteGeneration = nextGeneration( clipboard.remoteGeneration); transfer = clearRemoteRequestNL(); LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.requestLock); + lgClipboardFiles_providerUnavailable(); + LG_LOCK(clipboard.callbackLock); cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE); if (transfer != LG_CLIPBOARD_REQUEST_INVALID && @@ -396,6 +404,7 @@ static void resetRemote(void) if (release) g_state.ds->cbRelease(); LG_UNLOCK(clipboard.callbackLock); + LG_UNLOCK(clipboard.fileLock); } static bool validOps(const LG_ClipboardOps * ops) @@ -435,11 +444,18 @@ static void publishLocal(const ClipboardBinding * binding) LG_LOCK(clipboard.stateLock); count = g_params.clipboardToVM ? clipboard.localTypeCount : 0; memcpy(types, clipboard.localTypes, count * sizeof(*types)); + const uint64_t fileDataset = clipboard.localFileDataset; LG_UNLOCK(clipboard.stateLock); - const bool result = count ? - binding->ops->notifyTypes(binding->opaque, types, count) : - binding->ops->release(binding->opaque); + bool result; + if (count == 1 && types[0] == LG_CLIPBOARD_DATA_FILES) + result = fileDataset && binding->ops->offerFiles ? + binding->ops->offerFiles(binding->opaque, fileDataset) : + binding->ops->release(binding->opaque); + else + result = count ? binding->ops->notifyTypes( + binding->opaque, types, count) : + binding->ops->release(binding->opaque); if (!result) DEBUG_WARN("Failed to publish the local clipboard to %s", binding->ops->name); @@ -454,39 +470,60 @@ static void eventNotice(void * opaque, return; LG_ClipboardData type = LG_CLIPBOARD_DATA_NONE; + bool hasFiles = false; for (size_t i = 0; i < count; ++i) if (validType(types[i])) { - type = types[i]; - break; + if (types[i] == LG_CLIPBOARD_DATA_FILES) + { + type = LG_CLIPBOARD_DATA_FILES; + hasFiles = true; + } + else if (type == LG_CLIPBOARD_DATA_NONE) + type = types[i]; } if (type == LG_CLIPBOARD_DATA_NONE) return; + LG_LOCK(clipboard.fileLock); LG_LOCK_SHARED(clipboard.activeLock); if (!bindingActiveNL(binding)) { LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.fileLock); return; } const ClipboardBinding current = *binding; LG_UNLOCK_SHARED(clipboard.activeLock); bool notice; + bool release; uint32_t generation; + uint64_t fileDataset; LG_ClipboardRequest transfer; LG_LOCK(clipboard.requestLock); LG_LOCK(clipboard.stateLock); - clipboard.remoteNotice = true; - clipboard.remoteType = type; + const bool previousNotice = clipboard.remoteNotice; + const bool usable = !hasFiles || clipboard.remoteFileDataset; + clipboard.remoteNotice = usable; + clipboard.remoteType = usable ? type : LG_CLIPBOARD_DATA_NONE; clipboard.remoteGeneration = nextGeneration( clipboard.remoteGeneration); generation = clipboard.remoteGeneration; + if (!hasFiles || !usable) + clipboard.remoteFileDataset = 0; + fileDataset = clipboard.remoteFileDataset; transfer = clearRemoteRequestNL(); - notice = clipboard.localAvailable && g_params.clipboardToLocal; + notice = usable && clipboard.localAvailable && + g_params.clipboardToLocal; + release = !usable && previousNotice && clipboard.localAvailable && + g_params.clipboardToLocal; LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.requestLock); + if (!hasFiles) + lgClipboardFiles_remoteClear(); + LG_UNLOCK(clipboard.fileLock); LG_LOCK(clipboard.callbackLock); cancelRequestsNL(¤t, generation, false, @@ -498,12 +535,21 @@ static void eventNotice(void * opaque, notice = notice && clipboard.localAvailable && clipboard.remoteNotice && clipboard.remoteGeneration == generation; + release = release && clipboard.localAvailable && + !clipboard.remoteNotice && + clipboard.remoteGeneration == generation; LG_UNLOCK(clipboard.stateLock); LG_LOCK_SHARED(clipboard.activeLock); const bool active = bindingActiveNL(¤t); LG_UNLOCK_SHARED(clipboard.activeLock); if (notice && active) - g_state.ds->cbNotice(type); + { + if (type != LG_CLIPBOARD_DATA_FILES || + lgClipboardFiles_remoteReady(fileDataset)) + g_state.ds->cbNotice(type); + } + else if (release && active && g_state.ds->cbRelease) + g_state.ds->cbRelease(); LG_UNLOCK(clipboard.callbackLock); } @@ -892,14 +938,19 @@ static void eventRelease(void * opaque) LG_LOCK(clipboard.stateLock); release = clipboard.remoteNotice && clipboard.localAvailable && g_params.clipboardToLocal; - clipboard.remoteNotice = false; - clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; - clipboard.remoteGeneration = nextGeneration( + clipboard.remoteNotice = false; + clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; + clipboard.remoteFileDataset = 0; + clipboard.remoteGeneration = nextGeneration( clipboard.remoteGeneration); transfer = clearRemoteRequestNL(); LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.requestLock); + LG_LOCK(clipboard.fileLock); + lgClipboardFiles_remoteClear(); + LG_UNLOCK(clipboard.fileLock); + LG_LOCK(clipboard.callbackLock); cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_REPLACED); if (transfer != LG_CLIPBOARD_REQUEST_INVALID && @@ -1024,6 +1075,163 @@ static void eventRequestCancel(void * opaque, LG_ClipboardRequest id, LG_UNLOCK(clipboard.callbackLock); } +static bool fileEventBindingActive(void * opaque) +{ + ClipboardBinding * binding = opaque; + LG_LOCK_SHARED(clipboard.activeLock); + const bool active = bindingActiveNL(binding); + LG_UNLOCK_SHARED(clipboard.activeLock); + return active; +} + +static void eventFileOffer(void * opaque, uint64_t dataset) +{ + LG_LOCK(clipboard.fileLock); + const bool active = fileEventBindingActive(opaque); + if (!active || !dataset) + { + LG_UNLOCK(clipboard.fileLock); + return; + } + + LG_LOCK(clipboard.stateLock); + clipboard.remoteFileDataset = dataset; + LG_UNLOCK(clipboard.stateLock); + if (!lgClipboardFiles_remoteOffer(dataset)) + { + LG_LOCK(clipboard.stateLock); + if (clipboard.remoteFileDataset == dataset) + clipboard.remoteFileDataset = 0; + LG_UNLOCK(clipboard.stateLock); + lgClipboardFiles_remoteClear(); + } + LG_UNLOCK(clipboard.fileLock); +} + +static void eventFileAcquire(void * opaque, uint64_t dataset, + uint64_t acquisition) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return; + } + lgClipboardFiles_localAcquire(dataset, acquisition); + LG_UNLOCK(clipboard.fileLock); +} + +static void eventFileAcquired(void * opaque, uint64_t dataset, + uint64_t acquisition, LG_ClipboardFileError error) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return; + } + lgClipboardFiles_remoteAcquired(dataset, acquisition, error); + LG_UNLOCK(clipboard.fileLock); +} + +static void eventFileRelease(void * opaque, uint64_t dataset, + uint64_t acquisition) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return; + } + lgClipboardFiles_localRelease(dataset, acquisition); + LG_UNLOCK(clipboard.fileLock); +} + +static void eventFileRequest(void * opaque, + const LG_ClipboardFileRequest * request) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return; + } + lgClipboardFiles_localRequest(request); + LG_UNLOCK(clipboard.fileLock); +} + +static LG_ClipboardResult eventFileDataBegin(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t sizeHint) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + const LG_ClipboardResult result = + lgClipboardFiles_remoteDataBegin(request, sizeHint); + LG_UNLOCK(clipboard.fileLock); + return result; +} + +static LG_ClipboardResult eventFileDataChunk(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t responseOffset, + const void * data, size_t size) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + const LG_ClipboardResult result = lgClipboardFiles_remoteDataChunk( + request, responseOffset, data, size); + LG_UNLOCK(clipboard.fileLock); + return result; +} + +static LG_ClipboardResult eventFileDataEnd(void * opaque, + const LG_ClipboardFileRequest * request, uint64_t finalSize) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return LG_CLIPBOARD_RESULT_FAILED; + } + const LG_ClipboardResult result = + lgClipboardFiles_remoteDataEnd(request, finalSize); + LG_UNLOCK(clipboard.fileLock); + return result; +} + +static void eventFileDataReady(void * opaque, uint64_t request) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return; + } + lgClipboardFiles_localReady(request); + LG_UNLOCK(clipboard.fileLock); +} + +static void eventFileCancel(void * opaque, uint64_t dataset, + uint64_t request, LG_ClipboardFileError reason) +{ + LG_LOCK(clipboard.fileLock); + if (!fileEventBindingActive(opaque)) + { + LG_UNLOCK(clipboard.fileLock); + return; + } + lgClipboardFiles_remoteCancel(dataset, request, reason); + lgClipboardFiles_localCancel(dataset, request, reason); + LG_UNLOCK(clipboard.fileLock); +} + static const LG_ClipboardEventOps eventOps = { .notice = eventNotice, @@ -1036,6 +1244,16 @@ static const LG_ClipboardEventOps eventOps = .requestCancel = eventRequestCancel, .release = eventRelease, .request = eventRequest, + .fileOffer = eventFileOffer, + .fileAcquire = eventFileAcquire, + .fileAcquired = eventFileAcquired, + .fileRelease = eventFileRelease, + .fileRequest = eventFileRequest, + .fileDataBegin = eventFileDataBegin, + .fileDataChunk = eventFileDataChunk, + .fileDataEnd = eventFileDataEnd, + .fileDataReady = eventFileDataReady, + .fileCancel = eventFileCancel, }; /* providerLock must be held. dropActive suppresses all calls into an endpoint @@ -1168,7 +1386,7 @@ static void setBinding(ClipboardBinding * target, LG_UNLOCK(clipboard.registrationLock); } -void lgClipboard_init(void) +bool lgClipboard_init(void) { memset(&clipboard, 0, sizeof(clipboard)); LG_LOCK_INIT(clipboard.registrationLock); @@ -1178,9 +1396,16 @@ void lgClipboard_init(void) LG_LOCK_INIT(clipboard.requestLock); LG_LOCK_INIT(clipboard.writeLock); LG_LOCK_INIT(clipboard.callbackLock); + LG_LOCK_INIT(clipboard.fileLock); clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; clipboard.remoteRequestType = LG_CLIPBOARD_DATA_NONE; clipboard.requests = ll_new(); + if (!lgClipboardFiles_init()) + { + DEBUG_ERROR("Failed to initialize clipboard file transfer"); + return false; + } + return true; } void lgClipboard_free(void) @@ -1188,6 +1413,7 @@ void lgClipboard_free(void) lgClipboard_setLocalAvailable(false); lgClipboard_setTransport(NULL, NULL); lgClipboard_setFallback(NULL, NULL); + lgClipboardFiles_free(); LG_LOCK(clipboard.callbackLock); cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE); LG_UNLOCK(clipboard.callbackLock); @@ -1201,6 +1427,7 @@ void lgClipboard_free(void) LG_LOCK_FREE(clipboard.requestLock); LG_LOCK_FREE(clipboard.stateLock); LG_LOCK_FREE(clipboard.callbackLock); + LG_LOCK_FREE(clipboard.fileLock); LG_RWLOCK_FREE(clipboard.activeLock); LG_LOCK_FREE(clipboard.providerLock); LG_LOCK_FREE(clipboard.registrationLock); @@ -1211,6 +1438,7 @@ void lgClipboard_setLocalAvailable(bool available) bool notice; bool release; LG_ClipboardData type; + uint64_t fileDataset; LG_ClipboardRequest transfer = LG_CLIPBOARD_REQUEST_INVALID; LG_LOCK(clipboard.callbackLock); @@ -1222,13 +1450,18 @@ void lgClipboard_setLocalAvailable(bool available) notice = available && clipboard.remoteNotice && g_params.clipboardToLocal; type = clipboard.remoteType; + fileDataset = clipboard.remoteFileDataset; if (!available) transfer = clearRemoteRequestNL(); LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.requestLock); if (notice) - g_state.ds->cbNotice(type); + { + if (type != LG_CLIPBOARD_DATA_FILES || + lgClipboardFiles_remoteReady(fileDataset)) + g_state.ds->cbNotice(type); + } if (!available) { cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE); @@ -1285,6 +1518,7 @@ void lgClipboard_release(void) LG_LOCK(clipboard.writeLock); LG_LOCK(clipboard.stateLock); clipboard.localTypeCount = 0; + clipboard.localFileDataset = 0; clearRemoteRequestNL(); LG_UNLOCK(clipboard.stateLock); @@ -1310,13 +1544,14 @@ void lgClipboard_notifyTypes( return; for (size_t i = 0; i < count; ++i) - if (!validType(types[i])) + if (!validType(types[i]) || types[i] == LG_CLIPBOARD_DATA_FILES) return; LG_LOCK(clipboard.writeLock); LG_LOCK(clipboard.stateLock); memcpy(clipboard.localTypes, types, count * sizeof(*types)); clipboard.localTypeCount = count; + clipboard.localFileDataset = 0; clearRemoteRequestNL(); LG_UNLOCK(clipboard.stateLock); @@ -1329,6 +1564,30 @@ void lgClipboard_notifyTypes( LG_UNLOCK(clipboard.writeLock); } +void lgClipboard_notifyFiles(uint64_t dataset) +{ + if (!g_params.clipboardToVM || !dataset) + return; + + LG_LOCK(clipboard.writeLock); + LG_LOCK(clipboard.stateLock); + clipboard.localTypes[0] = LG_CLIPBOARD_DATA_FILES; + clipboard.localTypeCount = 1; + clipboard.localFileDataset = dataset; + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + + LG_LOCK_SHARED(clipboard.activeLock); + const bool result = !clipboard.active.ops || + (clipboard.active.ops->offerFiles ? + clipboard.active.ops->offerFiles(clipboard.active.opaque, dataset) : + clipboard.active.ops->release(clipboard.active.opaque)); + if (!result) + DEBUG_WARN("Failed to publish the local clipboard file dataset"); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); +} + /* writeLock and activeLock must be held. */ static bool cancelRemoteNL(LG_ClipboardCancelReason reason) { @@ -1864,3 +2123,139 @@ bool lgClipboard_request(LG_ClipboardData type, return false; return requestClipboard(type, replyFn, NULL, opaque, NULL); } + +bool lgClipboard_fileAcquire(uint64_t dataset, uint64_t acquisition) +{ + bool result = false; + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && clipboard.active.ops->fileAcquire) + result = clipboard.active.ops->fileAcquire( + clipboard.active.opaque, dataset, acquisition); + LG_UNLOCK_SHARED(clipboard.activeLock); + return result; +} + +bool lgClipboard_fileAcquired(uint64_t dataset, uint64_t acquisition, + LG_ClipboardFileError error) +{ + bool result = false; + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && clipboard.active.ops->fileAcquired) + result = clipboard.active.ops->fileAcquired( + clipboard.active.opaque, dataset, acquisition, error); + LG_UNLOCK_SHARED(clipboard.activeLock); + return result; +} + +bool lgClipboard_fileRelease(uint64_t dataset, uint64_t acquisition) +{ + bool result = false; + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && clipboard.active.ops->fileRelease) + result = clipboard.active.ops->fileRelease( + clipboard.active.opaque, dataset, acquisition); + LG_UNLOCK_SHARED(clipboard.activeLock); + return result; +} + +bool lgClipboard_fileRequest(const LG_ClipboardFileRequest * request) +{ + bool result = false; + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && clipboard.active.ops->fileRequest) + result = clipboard.active.ops->fileRequest( + clipboard.active.opaque, request); + LG_UNLOCK_SHARED(clipboard.activeLock); + return result; +} + +LG_ClipboardResult lgClipboard_fileDataBegin( + const LG_ClipboardFileRequest * request, uint64_t sizeHint) +{ + LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && clipboard.active.ops->fileDataBegin) + result = clipboard.active.ops->fileDataBegin( + clipboard.active.opaque, request, sizeHint); + LG_UNLOCK_SHARED(clipboard.activeLock); + return result; +} + +LG_ClipboardResult lgClipboard_fileDataChunk( + const LG_ClipboardFileRequest * request, uint64_t responseOffset, + const void * data, size_t size) +{ + LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && clipboard.active.ops->fileDataChunk) + result = clipboard.active.ops->fileDataChunk( + clipboard.active.opaque, request, responseOffset, data, size); + LG_UNLOCK_SHARED(clipboard.activeLock); + return result; +} + +LG_ClipboardResult lgClipboard_fileDataEnd( + const LG_ClipboardFileRequest * request, uint64_t finalSize) +{ + LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && clipboard.active.ops->fileDataEnd) + result = clipboard.active.ops->fileDataEnd( + clipboard.active.opaque, request, finalSize); + LG_UNLOCK_SHARED(clipboard.activeLock); + return result; +} + +bool lgClipboard_fileCancel(uint64_t dataset, uint64_t request, + LG_ClipboardFileError reason) +{ + bool result = false; + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && clipboard.active.ops->fileCancel) + result = clipboard.active.ops->fileCancel( + clipboard.active.opaque, dataset, request, reason); + LG_UNLOCK_SHARED(clipboard.activeLock); + return result; +} + +void lgClipboard_fileRemoteReady(uint64_t dataset) +{ + LG_LOCK(clipboard.callbackLock); + LG_LOCK(clipboard.stateLock); + const bool notice = clipboard.localAvailable && + g_params.clipboardToLocal && clipboard.remoteNotice && + clipboard.remoteType == LG_CLIPBOARD_DATA_FILES && + clipboard.remoteFileDataset == dataset; + LG_UNLOCK(clipboard.stateLock); + if (notice && g_state.ds->cbNotice) + g_state.ds->cbNotice(LG_CLIPBOARD_DATA_FILES); + LG_UNLOCK(clipboard.callbackLock); +} + +void lgClipboard_fileRemoteFailed(uint64_t dataset) +{ + if (!dataset) + return; + + LG_LOCK(clipboard.callbackLock); + LG_LOCK(clipboard.stateLock); + const bool matchingDataset = + clipboard.remoteFileDataset == dataset; + const bool matchingNotice = matchingDataset && clipboard.remoteNotice && + clipboard.remoteType == LG_CLIPBOARD_DATA_FILES; + const bool release = matchingNotice && clipboard.localAvailable && + g_params.clipboardToLocal; + if (matchingDataset) + clipboard.remoteFileDataset = 0; + if (matchingNotice) + { + clipboard.remoteNotice = false; + clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; + clipboard.remoteGeneration = nextGeneration( + clipboard.remoteGeneration); + } + LG_UNLOCK(clipboard.stateLock); + if (release && g_state.ds->cbRelease) + g_state.ds->cbRelease(); + LG_UNLOCK(clipboard.callbackLock); +} diff --git a/client/src/clipboard.h b/client/src/clipboard.h index 90b238cb..3bba4199 100644 --- a/client/src/clipboard.h +++ b/client/src/clipboard.h @@ -23,7 +23,7 @@ #include "interface/clipboard.h" -void lgClipboard_init(void); +bool lgClipboard_init(void); void lgClipboard_free(void); void lgClipboard_setLocalAvailable(bool available); @@ -36,6 +36,7 @@ void lgClipboard_dropTransport(void); void lgClipboard_release(void); void lgClipboard_notifyTypes( const LG_ClipboardData types[], size_t count); +void lgClipboard_notifyFiles(uint64_t dataset); LG_ClipboardResult lgClipboard_dataBegin(LG_ClipboardRequest request, LG_ClipboardData type, uint64_t sizeHint); LG_ClipboardResult lgClipboard_dataChunk(LG_ClipboardRequest request, @@ -55,4 +56,22 @@ bool lgClipboard_requestReady(LG_ClipboardRequest request); bool lgClipboard_request(LG_ClipboardData type, LG_ClipboardReplyFn replyFn, void * opaque); +/* File-dataset transport entry points used by clipboard_files.c. */ +bool lgClipboard_fileAcquire(uint64_t dataset, uint64_t acquisition); +bool lgClipboard_fileAcquired(uint64_t dataset, uint64_t acquisition, + LG_ClipboardFileError error); +bool lgClipboard_fileRelease(uint64_t dataset, uint64_t acquisition); +bool lgClipboard_fileRequest(const LG_ClipboardFileRequest * request); +LG_ClipboardResult lgClipboard_fileDataBegin( + const LG_ClipboardFileRequest * request, uint64_t sizeHint); +LG_ClipboardResult lgClipboard_fileDataChunk( + const LG_ClipboardFileRequest * request, uint64_t responseOffset, + const void * data, size_t size); +LG_ClipboardResult lgClipboard_fileDataEnd( + const LG_ClipboardFileRequest * request, uint64_t finalSize); +bool lgClipboard_fileCancel(uint64_t dataset, uint64_t request, + LG_ClipboardFileError reason); +void lgClipboard_fileRemoteReady(uint64_t dataset); +void lgClipboard_fileRemoteFailed(uint64_t dataset); + #endif diff --git a/client/src/clipboard_files.c b/client/src/clipboard_files.c new file mode 100644 index 00000000..b741c999 --- /dev/null +++ b/client/src/clipboard_files.c @@ -0,0 +1,3703 @@ +/** + * 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 + */ + +#define FUSE_USE_VERSION 310 + +#include "clipboard_files.h" +#include "clipboard.h" + +#include "common/KVMFRClipboard.h" +#include "common/debug.h" +#include "common/event.h" +#include "common/locking.h" +#include "common/thread.h" +#include "common/time.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FILE_WORKERS 4U +#define FILE_ATTR_TIMEOUT 1.0 +#define FILE_ENTRY_TIMEOUT 1.0 +#define FILE_REQUEST_MAX KVMFR_CLIPBOARD_FILE_READ_BYTES +#define FILE_STREAM_CHUNK KVMFR_CLIPBOARD_DATA_BYTES +#define FILE_DELIVERY_GRACE_US UINT64_C(30000000) +#define FILE_DELIVERY_GRACE_MAX 4U +#define FILE_URI_PREFIX "file://" +#define FILE_GNOME_MIME "x-special/gnome-copied-files" +#define FILE_URI_MIME "text/uri-list" +#define FILE_KDE_CUT_MIME "application/x-kde-cutselection" + +enum RemoteReply +{ + REMOTE_REPLY_INITIAL, + REMOTE_REPLY_LOOKUP, + REMOTE_REPLY_READDIR, + REMOTE_REPLY_READ, +}; + +struct RemoteDataset; + +struct RemoteNode +{ + struct RemoteNode * next; + struct RemoteDataset * dataset; + fuse_ino_t ino; + fuse_ino_t parent; + uint64_t node; + uint64_t size; + uint64_t createdNs; + uint64_t modifiedNs; + KVMFRClipboardFileType type; + uint64_t lookups; + bool listed; + char * name; +}; + +struct RemoteDataset +{ + struct RemoteDataset * next; + uint64_t dataset; + uint64_t acquisition; + fuse_ino_t ino; + uint64_t lookupPins; + uint64_t openPins; + uint64_t requestPins; + uint64_t operationPins; + uint64_t deliveryDeadline; + unsigned presentationPins; + bool acquiring; + bool acquired; + bool ready; + bool current; + bool stale; + bool deliveryPending; + bool pruneQueued; + char name[40]; +}; + +struct RemoteRequest +{ + struct RemoteRequest * next; + LG_ClipboardFileRequest request; + struct RemoteDataset * dataset; + struct RemoteNode * directory; + enum RemoteReply reply; + fuse_req_t fuseRequest; + size_t fuseSize; + off_t fuseOffset; + char * lookupName; + uint8_t * data; + size_t dataSize; + size_t dataCapacity; + uint64_t sizeHint; + bool began; + bool transportStarted; +}; + +enum RemoteActionType +{ + REMOTE_ACTION_NONE, + REMOTE_ACTION_CANCEL_ACQUISITION, + REMOTE_ACTION_RELEASE_ACQUISITION, +}; + +struct RemoteAction +{ + enum RemoteActionType type; + struct RemoteDataset * object; + uint64_t dataset; + uint64_t acquisition; +}; + +struct LocalDataset; + +struct LocalNode +{ + struct LocalNode * next; + struct LocalDataset * dataset; + struct LocalNode * root; + uint64_t node; + uint64_t parent; + uint64_t size; + uint64_t createdNs; + uint64_t modifiedNs; + KVMFRClipboardFileType type; + dev_t device; + ino_t inode; + int fd; + char * path; + char * name; +}; + +struct LocalDataset +{ + struct LocalDataset * next; + struct LocalNode * nodes; + uint64_t nodeSerial; + uint64_t dataset; + unsigned references; + bool current; +}; + +struct LocalAcquisition +{ + struct LocalAcquisition * next; + struct LocalDataset * dataset; + uint64_t wireDataset; + uint64_t acquisition; +}; + +struct LocalJob +{ + struct LocalJob * next; + struct LocalDataset * dataset; + LG_ClipboardFileRequest request; + LGEvent * ready; + atomic_bool cancelled; +}; + +static struct +{ + LG_Lock lock; + struct fuse_session * session; + LGThread * fuseThread; + int fuseStopFd; + char mountpoint[PATH_MAX]; + bool initialized; + bool stopping; + fuse_ino_t inodeSerial; + uint64_t requestSerial; + uint64_t datasetSerial; + struct RemoteDataset * remoteDatasets; + struct RemoteDataset * remoteCurrent; + struct RemoteNode * remoteNodes; + struct RemoteRequest * remoteRequests; + struct LocalDataset * localDatasets; + struct LocalDataset * localCurrent; + struct LocalAcquisition * localAcquisitions; + struct LocalJob * jobsHead; + struct LocalJob ** jobsTail; + struct LocalJob * activeJobs; + LGEvent * jobEvent; + LGThread * workers[FILE_WORKERS]; +} +files; + +static bool filesLockInitialized; + +#ifdef ENABLE_TESTS +static atomic_bool forceLocalEof; +#endif + +static bool randomBytes(void * buffer, size_t size) +{ + uint8_t * bytes = buffer; + size_t offset = 0; + while (offset < size) + { + const ssize_t result = getrandom(bytes + offset, size - offset, 0); + if (result < 0 && errno == EINTR) + continue; + if (result <= 0) + { + if (!result) + errno = EIO; + return false; + } + offset += (size_t)result; + } + return true; +} + +static uint64_t nsFromTimespec(struct timespec value) +{ + if (value.tv_sec < 0 || value.tv_nsec < 0) + return 0; + if ((uint64_t)value.tv_sec > UINT64_MAX / UINT64_C(1000000000)) + return UINT64_MAX; + return (uint64_t)value.tv_sec * UINT64_C(1000000000) + + (uint64_t)value.tv_nsec; +} + +static struct timespec timespecFromNs(uint64_t value) +{ + return (struct timespec) + { + .tv_sec = (time_t)(value / UINT64_C(1000000000)), + .tv_nsec = (long)(value % UINT64_C(1000000000)), + }; +} + +static uint64_t nextRequestNL(void) +{ + do + ++files.requestSerial; + while (!files.requestSerial || + kvmfrClipboardTransferFromHelper(files.requestSerial)); + return files.requestSerial; +} + +static uint64_t nextAcquisitionNL(void) +{ + return nextRequestNL(); +} + +static uint64_t nextDatasetNL(void) +{ + do + ++files.datasetSerial; + while (!files.datasetSerial || + kvmfrClipboardTransferFromHelper(files.datasetSerial)); + return files.datasetSerial; +} + +static fuse_ino_t nextInodeNL(void) +{ + if (++files.inodeSerial <= FUSE_ROOT_ID) + files.inodeSerial = FUSE_ROOT_ID + 1; + return files.inodeSerial; +} + +static int errnoFromFileError(LG_ClipboardFileError error) +{ + switch (error) + { + case LG_CLIPBOARD_FILE_ERROR_NONE: return 0; + case LG_CLIPBOARD_FILE_ERROR_NOT_FOUND: return ENOENT; + case LG_CLIPBOARD_FILE_ERROR_ACCESS: return EACCES; + case LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY: return ENOTDIR; + case LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY: return EISDIR; + case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY: return ENOMEM; + case LG_CLIPBOARD_FILE_ERROR_NO_SPACE: return ENOSPC; + case LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED: return ENOTSUP; + case LG_CLIPBOARD_FILE_ERROR_CANCELLED: return EINTR; + case LG_CLIPBOARD_FILE_ERROR_STALE: return ESTALE; + default: return EIO; + } +} + +static const char * fileErrorName(LG_ClipboardFileError error) +{ + switch (error) + { + case LG_CLIPBOARD_FILE_ERROR_NONE: return "none"; + case LG_CLIPBOARD_FILE_ERROR_NOT_FOUND: return "not found"; + case LG_CLIPBOARD_FILE_ERROR_ACCESS: return "access denied"; + case LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY: return "not a directory"; + case LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY: return "is a directory"; + case LG_CLIPBOARD_FILE_ERROR_IO: return "I/O error"; + case LG_CLIPBOARD_FILE_ERROR_INVALID: return "invalid"; + case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY: return "out of memory"; + case LG_CLIPBOARD_FILE_ERROR_NO_SPACE: return "no space"; + case LG_CLIPBOARD_FILE_ERROR_DISCONNECTED: return "disconnected"; + case LG_CLIPBOARD_FILE_ERROR_CANCELLED: return "cancelled"; + case LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED: return "not supported"; + case LG_CLIPBOARD_FILE_ERROR_STALE: return "stale"; + } + return "unknown"; +} + +static LG_ClipboardFileError fileErrorFromErrno(int error) +{ + switch (error) + { + case ENOENT: return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND; + case EACCES: + case EPERM: return LG_CLIPBOARD_FILE_ERROR_ACCESS; + case ENOTDIR: return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY; + case EISDIR: return LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY; + case ENOMEM: + case EMFILE: + case ENFILE: return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + case ENOSPC: return LG_CLIPBOARD_FILE_ERROR_NO_SPACE; + case ELOOP: + case ENOTSUP: return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + case ESTALE: return LG_CLIPBOARD_FILE_ERROR_STALE; + case ECANCELED: return LG_CLIPBOARD_FILE_ERROR_CANCELLED; + default: return LG_CLIPBOARD_FILE_ERROR_IO; + } +} + +static bool grow(void ** buffer, size_t * capacity, size_t wanted) +{ + if (wanted <= *capacity) + return !wanted || *buffer; + size_t next = *capacity ? *capacity : 4096U; + while (next < wanted) + { + if (next > SIZE_MAX / 2U) + { + next = wanted; + break; + } + next *= 2U; + } + void * resized = realloc(*buffer, next); + if (!resized) + return false; + *buffer = resized; + *capacity = next; + return true; +} + +static struct RemoteDataset * remoteDatasetByIdNL(uint64_t dataset) +{ + for (struct RemoteDataset * item = files.remoteDatasets; + item; item = item->next) + if (item->dataset == dataset) + return item; + return NULL; +} + +static struct RemoteDataset * remoteDatasetByAcquisitionNL( + uint64_t acquisition) +{ + for (struct RemoteDataset * item = files.remoteDatasets; + item; item = item->next) + if (item->acquisition == acquisition) + return item; + return NULL; +} + +static struct RemoteAction retireRemoteDatasetNL( + struct RemoteDataset * dataset) +{ + struct RemoteAction action = { 0 }; + if (!dataset || dataset->current || dataset->presentationPins || + dataset->lookupPins || dataset->openPins || dataset->requestPins || + dataset->operationPins || dataset->deliveryPending) + return action; + + if (!dataset->stale) + { + dataset->ready = false; + dataset->stale = true; + action.dataset = dataset->dataset; + action.acquisition = dataset->acquisition; + if (dataset->acquired) + action.type = REMOTE_ACTION_RELEASE_ACQUISITION; + else if (dataset->acquiring) + action.type = REMOTE_ACTION_CANCEL_ACQUISITION; + dataset->acquiring = false; + dataset->acquired = false; + } + if (dataset->pruneQueued) + return action; + dataset->pruneQueued = true; + action.object = dataset; + return action; +} + +/* Retire a failed initial acquisition/list only while it is still the + * dataset backing the current remote file offer. The caller must hold + * files.lock and subsequently call retireRemoteDatasetNL so an acquired + * dataset is released before it is pruned. */ +static uint64_t failCurrentRemoteDatasetNL( + struct RemoteDataset * dataset) +{ + if (!dataset || !dataset->current) + return 0; + + dataset->current = false; + dataset->ready = false; + dataset->deliveryPending = false; + dataset->deliveryDeadline = 0; + if (files.remoteCurrent == dataset) + files.remoteCurrent = NULL; + return dataset->dataset; +} + +static void pruneRemoteDataset(struct RemoteDataset * dataset) +{ + if (!dataset) + return; + LG_LOCK(files.lock); + if (!dataset->pruneQueued || !dataset->stale || dataset->current || + dataset->presentationPins || dataset->lookupPins || + dataset->openPins || dataset->requestPins || + dataset->operationPins || dataset->deliveryPending) + { + dataset->pruneQueued = false; + LG_UNLOCK(files.lock); + return; + } + + struct RemoteDataset ** datasetLink = &files.remoteDatasets; + while (*datasetLink && *datasetLink != dataset) + datasetLink = &(*datasetLink)->next; + if (!*datasetLink) + { + LG_UNLOCK(files.lock); + return; + } + *datasetLink = dataset->next; + if (files.remoteCurrent == dataset) + files.remoteCurrent = NULL; + + struct RemoteNode * retired = NULL; + struct RemoteNode ** nodeLink = &files.remoteNodes; + while (*nodeLink) + { + struct RemoteNode * node = *nodeLink; + if (node->dataset != dataset) + { + nodeLink = &node->next; + continue; + } + *nodeLink = node->next; + node->next = retired; + retired = node; + } + LG_UNLOCK(files.lock); + + while (retired) + { + struct RemoteNode * next = retired->next; + free(retired->name); + free(retired); + retired = next; + } + free(dataset); +} + +static void performRemoteAction(struct RemoteAction action) +{ + switch (action.type) + { + case REMOTE_ACTION_CANCEL_ACQUISITION: + if (!lgClipboard_fileCancel(action.dataset, action.acquisition, + LG_CLIPBOARD_FILE_ERROR_CANCELLED)) + DEBUG_WARN("Failed to cancel retired clipboard file acquisition"); + break; + + case REMOTE_ACTION_RELEASE_ACQUISITION: + if (!lgClipboard_fileRelease(action.dataset, action.acquisition)) + DEBUG_WARN("Failed to release retired clipboard file acquisition"); + break; + + case REMOTE_ACTION_NONE: + break; + } + pruneRemoteDataset(action.object); +} + +static bool pinRemoteOperationNL( + struct RemoteDataset * dataset, bool consumeDelivery) +{ + if (!dataset || dataset->stale) + return false; + ++dataset->operationPins; + if (consumeDelivery) + { + dataset->deliveryPending = false; + dataset->deliveryDeadline = 0; + } + return true; +} + +static void releaseRemoteOperation(struct RemoteDataset * dataset) +{ + if (!dataset) + return; + LG_LOCK(files.lock); + if (dataset->operationPins) + --dataset->operationPins; + const struct RemoteAction action = retireRemoteDatasetNL(dataset); + LG_UNLOCK(files.lock); + performRemoteAction(action); +} + +static void reapRemoteDeliveries(bool pressure) +{ + for (;;) + { + const uint64_t now = microtime(); + LG_LOCK(files.lock); + struct RemoteDataset * candidate = NULL; + struct RemoteDataset * oldest = NULL; + unsigned pending = 0; + for (struct RemoteDataset * dataset = files.remoteDatasets; + dataset; dataset = dataset->next) + { + if (!dataset->deliveryPending) + continue; + if (dataset->deliveryDeadline <= now) + { + candidate = dataset; + break; + } + if (dataset->current) + continue; + ++pending; + if (!oldest || dataset->deliveryDeadline < oldest->deliveryDeadline) + oldest = dataset; + } + if (!candidate && pressure && pending > FILE_DELIVERY_GRACE_MAX) + candidate = oldest; + if (!candidate) + { + LG_UNLOCK(files.lock); + return; + } + candidate->deliveryPending = false; + candidate->deliveryDeadline = 0; + const struct RemoteAction action = retireRemoteDatasetNL(candidate); + LG_UNLOCK(files.lock); + performRemoteAction(action); + } +} + +static int nextRemoteDeliveryTimeout(void) +{ + const uint64_t now = microtime(); + uint64_t deadline = UINT64_MAX; + LG_LOCK(files.lock); + for (struct RemoteDataset * dataset = files.remoteDatasets; + dataset; dataset = dataset->next) + if (dataset->deliveryPending && + dataset->deliveryDeadline < deadline) + deadline = dataset->deliveryDeadline; + LG_UNLOCK(files.lock); + if (deadline == UINT64_MAX) + return -1; + if (deadline <= now) + return 0; + const uint64_t delta = deadline - now; + const uint64_t milliseconds = delta / UINT64_C(1000) + + (delta % UINT64_C(1000) != 0); + return milliseconds > INT_MAX ? INT_MAX : (int)milliseconds; +} + +static void pruneUnusedRemoteDatasets(void) +{ + for (;;) + { + LG_LOCK(files.lock); + struct RemoteDataset * dataset = files.remoteDatasets; + while (dataset && (dataset->current || dataset->presentationPins || + dataset->lookupPins || dataset->openPins || dataset->requestPins || + dataset->operationPins || dataset->deliveryPending || + dataset->pruneQueued)) + dataset = dataset->next; + const struct RemoteAction action = retireRemoteDatasetNL(dataset); + LG_UNLOCK(files.lock); + if (!action.object) + return; + performRemoteAction(action); + } +} + +static struct RemoteNode * remoteNodeByInodeNL(fuse_ino_t ino) +{ + for (struct RemoteNode * node = files.remoteNodes; node; node = node->next) + if (node->ino == ino) + return node; + return NULL; +} + +static struct RemoteNode * remoteChildNL(struct RemoteDataset * dataset, + fuse_ino_t parent, const char * name) +{ + for (struct RemoteNode * node = files.remoteNodes; node; node = node->next) + if (node->dataset == dataset && node->parent == parent && + !strcmp(node->name, name)) + return node; + return NULL; +} + +static struct RemoteNode * remoteNodeByWireNL( + struct RemoteDataset * dataset, uint64_t wireNode) +{ + for (struct RemoteNode * node = files.remoteNodes; node; node = node->next) + if (node->dataset == dataset && node->node == wireNode) + return node; + return NULL; +} + +static bool validUtf8(const uint8_t * value, size_t length) +{ + for (size_t i = 0; i < length;) + { + const uint8_t first = value[i++]; + if (first < 0x80) + continue; + unsigned continuation; + uint32_t codepoint; + uint32_t minimum; + if ((first & 0xe0U) == 0xc0U) + { + continuation = 1; + codepoint = first & 0x1fU; + minimum = 0x80; + } + else if ((first & 0xf0U) == 0xe0U) + { + continuation = 2; + codepoint = first & 0x0fU; + minimum = 0x800; + } + else if ((first & 0xf8U) == 0xf0U) + { + continuation = 3; + codepoint = first & 0x07U; + minimum = 0x10000; + } + else + return false; + if (continuation > length - i) + return false; + while (continuation--) + { + const uint8_t next = value[i++]; + if ((next & 0xc0U) != 0x80U) + return false; + codepoint = (codepoint << 6) | (next & 0x3fU); + } + if (codepoint < minimum || codepoint > 0x10ffffU || + (codepoint >= 0xd800U && codepoint <= 0xdfffU)) + return false; + } + return true; +} + +static struct RemoteRequest * remoteRequestTakeNL(uint64_t request) +{ + struct RemoteRequest ** link = &files.remoteRequests; + while (*link && (*link)->request.request != request) + link = &(*link)->next; + if (!*link) + return NULL; + struct RemoteRequest * result = *link; + *link = result->next; + result->next = NULL; + return result; +} + +static struct RemoteRequest * remoteRequestTakePointerNL( + struct RemoteRequest * request) +{ + struct RemoteRequest ** link = &files.remoteRequests; + while (*link && *link != request) + link = &(*link)->next; + if (!*link) + return NULL; + *link = request->next; + request->next = NULL; + return request; +} + +static struct RemoteAction completeRemoteRequestNL( + struct RemoteRequest * request) +{ + if (request && request->dataset->requestPins) + --request->dataset->requestPins; + return request ? retireRemoteDatasetNL(request->dataset) : + (struct RemoteAction) { 0 }; +} + +static void freeRemoteRequest(struct RemoteRequest * request) +{ + if (!request) + return; + free(request->lookupName); + free(request->data); + free(request); +} + +static void unregisterRemoteInterrupt(struct RemoteRequest * request) +{ + if (request && request->fuseRequest) + fuse_req_interrupt_func(request->fuseRequest, NULL, NULL); +} + +static void failRemoteRequest(struct RemoteRequest * request, int error) +{ + if (!request) + return; + if (request->reply == REMOTE_REPLY_INITIAL) + DEBUG_WARN("Remote clipboard file root list failed: " + "dataset=%" PRIu64 ", request=%" PRIu64 ", error=%d (%s)", + request->request.dataset, request->request.request, + error, strerror(error)); + unregisterRemoteInterrupt(request); + if (request->fuseRequest) + fuse_reply_err(request->fuseRequest, error); + LG_LOCK(files.lock); + const uint64_t failedDataset = request->reply == REMOTE_REPLY_INITIAL ? + failCurrentRemoteDatasetNL(request->dataset) : 0; + const struct RemoteAction action = completeRemoteRequestNL(request); + LG_UNLOCK(files.lock); + freeRemoteRequest(request); + performRemoteAction(action); + if (failedDataset) + lgClipboard_fileRemoteFailed(failedDataset); +} + +static void completeRemoteRequest(struct RemoteRequest * request) +{ + LG_LOCK(files.lock); + const struct RemoteAction action = completeRemoteRequestNL(request); + LG_UNLOCK(files.lock); + freeRemoteRequest(request); + performRemoteAction(action); +} + +static void fillStat(const struct RemoteNode * node, struct stat * value) +{ + memset(value, 0, sizeof(*value)); + value->st_ino = node->ino; + value->st_mode = node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? + S_IFDIR | 0500 : S_IFREG | 0400; + value->st_nlink = node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? 2 : 1; + value->st_uid = getuid(); + value->st_gid = getgid(); + value->st_size = node->size > INT64_MAX ? INT64_MAX : (off_t)node->size; + value->st_mtim = timespecFromNs(node->modifiedNs); + value->st_ctim = value->st_mtim; + value->st_atim = value->st_mtim; +} + +static void replyEntry(fuse_req_t request, struct RemoteNode * node) +{ + struct fuse_entry_param entry = + { + .ino = node->ino, + .generation = 1, + .attr_timeout = FILE_ATTR_TIMEOUT, + .entry_timeout = FILE_ENTRY_TIMEOUT, + }; + fillStat(node, &entry.attr); + LG_LOCK(files.lock); + const bool available = !node->dataset->stale; + if (available) + { + ++node->lookups; + ++node->dataset->lookupPins; + } + LG_UNLOCK(files.lock); + if (!available) + { + fuse_reply_err(request, ESTALE); + return; + } + if (fuse_reply_entry(request, &entry) == 0) + return; + + LG_LOCK(files.lock); + if (node->lookups) + --node->lookups; + if (node->dataset->lookupPins) + --node->dataset->lookupPins; + const struct RemoteAction action = retireRemoteDatasetNL(node->dataset); + LG_UNLOCK(files.lock); + performRemoteAction(action); +} + +static bool installRemoteListNL(struct RemoteRequest * request) +{ + struct RemoteNode * additions = NULL; + size_t offset = 0; + while (offset < request->dataSize) + { + if (request->dataSize - offset < sizeof(KVMFRClipboardFileEntry)) + goto invalid; + KVMFRClipboardFileEntry entry; + memcpy(&entry, request->data + offset, sizeof(entry)); + if (!entry.node || !entry.nameLength || entry.nameLength > NAME_MAX || + (entry.type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR && + entry.type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY)) + goto invalid; + const uint64_t bytes64 = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES( + entry.nameLength); + if (bytes64 > SIZE_MAX || bytes64 > request->dataSize - offset) + goto invalid; + const uint8_t * encodedName = request->data + offset + + sizeof(entry); + if (memchr(encodedName, 0, entry.nameLength) || + !validUtf8(encodedName, entry.nameLength) || + (entry.nameLength == 1 && encodedName[0] == '.') || + (entry.nameLength == 2 && encodedName[0] == '.' && + encodedName[1] == '.') || + memchr(encodedName, '/', entry.nameLength)) + goto invalid; + for (size_t i = sizeof(entry) + entry.nameLength; + i < (size_t)bytes64; ++i) + if (request->data[offset + i]) + goto invalid; + char * name = malloc((size_t)entry.nameLength + 1U); + if (!name) + goto invalid; + memcpy(name, encodedName, entry.nameLength); + name[entry.nameLength] = 0; + + struct RemoteNode * existing = remoteChildNL( + request->dataset, request->directory->ino, name); + struct RemoteNode * wireNode = remoteNodeByWireNL( + request->dataset, entry.node); + for (struct RemoteNode * node = additions; node; node = node->next) + { + if (node->parent == request->directory->ino && + !strcmp(node->name, name)) + existing = node; + if (node->node == entry.node) + wireNode = node; + } + + if (existing || wireNode) + { + const bool same = existing && existing == wireNode && + existing->size == entry.size && + existing->createdNs == entry.createdNs && + existing->modifiedNs == entry.modifiedNs && + existing->type == entry.type; + free(name); + if (!same) + goto invalid; + } + else + { + struct RemoteNode * node = calloc(1, sizeof(*node)); + if (!node) + { + free(name); + goto invalid; + } + *node = (struct RemoteNode) + { + .dataset = request->dataset, + .ino = nextInodeNL(), + .parent = request->directory->ino, + .node = entry.node, + .size = entry.size, + .createdNs = entry.createdNs, + .modifiedNs = entry.modifiedNs, + .type = entry.type, + .name = name, + .next = additions, + }; + additions = node; + } + offset += (size_t)bytes64; + } + while (additions) + { + struct RemoteNode * node = additions; + additions = node->next; + node->next = files.remoteNodes; + files.remoteNodes = node; + } + request->directory->listed = true; + return true; + +invalid: + while (additions) + { + struct RemoteNode * next = additions->next; + free(additions->name); + free(additions); + additions = next; + } + return false; +} + +static void replyDirectory(struct RemoteRequest * request) +{ + char * buffer = malloc(request->fuseSize ? request->fuseSize : 1U); + if (!buffer) + { + fuse_reply_err(request->fuseRequest, ENOMEM); + return; + } + size_t used = 0; + off_t index = 0; + struct stat attr; + if (request->fuseOffset <= index) + { + fillStat(request->directory, &attr); + const size_t entry = fuse_add_direntry(request->fuseRequest, + buffer + used, request->fuseSize - used, ".", &attr, index + 1); + if (entry > request->fuseSize - used) + goto done; + used += entry; + } + ++index; + if (request->fuseOffset <= index) + { + LG_LOCK(files.lock); + struct RemoteNode * parent = remoteNodeByInodeNL( + request->directory->parent); + fillStat(parent ? parent : request->directory, &attr); + LG_UNLOCK(files.lock); + const size_t entry = fuse_add_direntry(request->fuseRequest, + buffer + used, request->fuseSize - used, "..", &attr, index + 1); + if (entry > request->fuseSize - used) + goto done; + used += entry; + } + LG_LOCK(files.lock); + for (struct RemoteNode * node = files.remoteNodes; node; node = node->next) + { + if (node->dataset != request->dataset || + node->parent != request->directory->ino) + continue; + ++index; + if (request->fuseOffset > index) + continue; + fillStat(node, &attr); + const size_t entry = fuse_add_direntry(request->fuseRequest, + buffer + used, request->fuseSize - used, + node->name, &attr, index + 1); + if (entry > request->fuseSize - used) + break; + used += entry; + } + LG_UNLOCK(files.lock); + +done: + fuse_reply_buf(request->fuseRequest, buffer, used); + free(buffer); +} + +static void finishRemoteRequest(struct RemoteRequest * request) +{ + unregisterRemoteInterrupt(request); + bool installed = true; + if (request->request.operation == LG_CLIPBOARD_FILE_LIST) + { + LG_LOCK(files.lock); + installed = installRemoteListNL(request); + if (installed && request->reply == REMOTE_REPLY_INITIAL) + { + request->dataset->ready = true; + const bool current = request->dataset->current; + const uint64_t datasetId = request->dataset->dataset; + const uint64_t requestId = request->request.request; + LG_UNLOCK(files.lock); + if (current) + { + DEBUG_INFO("Remote clipboard file root list ready; publishing: " + "dataset=%" PRIu64 ", request=%" PRIu64, + datasetId, requestId); + lgClipboard_fileRemoteReady(datasetId); + } + completeRemoteRequest(request); + return; + } + if (installed && request->reply == REMOTE_REPLY_LOOKUP) + { + struct RemoteNode * node = remoteChildNL(request->dataset, + request->directory->ino, request->lookupName); + LG_UNLOCK(files.lock); + if (node) + replyEntry(request->fuseRequest, node); + else + fuse_reply_err(request->fuseRequest, ENOENT); + completeRemoteRequest(request); + return; + } + LG_UNLOCK(files.lock); + if (!installed && request->reply == REMOTE_REPLY_INITIAL) + { + failRemoteRequest(request, EIO); + return; + } + if (installed && request->reply == REMOTE_REPLY_READDIR) + replyDirectory(request); + else if (request->fuseRequest) + fuse_reply_err(request->fuseRequest, EIO); + } + else if (request->reply == REMOTE_REPLY_READ) + { + if (request->dataSize > request->request.length) + fuse_reply_err(request->fuseRequest, EIO); + else + fuse_reply_buf(request->fuseRequest, + (const char *)request->data, request->dataSize); + } + completeRemoteRequest(request); +} + +static void interruptRemoteRequest(fuse_req_t fuseRequest, void * opaque) +{ + struct RemoteRequest * request = opaque; + LG_LOCK(files.lock); + request = remoteRequestTakePointerNL(request); + const struct RemoteAction action = completeRemoteRequestNL(request); + LG_UNLOCK(files.lock); + if (!request) + return; + + const LG_ClipboardFileRequest descriptor = request->request; + if (request->transportStarted && + !lgClipboard_fileCancel(descriptor.dataset, descriptor.request, + LG_CLIPBOARD_FILE_ERROR_CANCELLED)) + DEBUG_WARN("Failed to cancel interrupted clipboard file request"); + fuse_reply_err(fuseRequest, EINTR); + freeRemoteRequest(request); + performRemoteAction(action); +} + +/* Returns true when another path already took ownership of the request. */ +static bool finishUnsentRemoteRequest(uint64_t requestId) +{ + LG_LOCK(files.lock); + struct RemoteRequest * failed = remoteRequestTakeNL(requestId); + LG_UNLOCK(files.lock); + if (!failed) + return true; + failRemoteRequest(failed, EIO); + return false; +} + +static bool requestRemote(struct RemoteDataset * dataset, + struct RemoteNode * node, LG_ClipboardFileOperation operation, + uint64_t offset, uint32_t length, enum RemoteReply reply, + fuse_req_t fuseRequest, size_t fuseSize, off_t fuseOffset, + const char * lookupName) +{ + LG_LOCK(files.lock); + const bool stale = dataset->stale; + LG_UNLOCK(files.lock); + if (stale) + return false; + struct RemoteRequest * request = calloc(1, sizeof(*request)); + if (!request) + return false; + if (lookupName && !(request->lookupName = strdup(lookupName))) + { + free(request); + return false; + } + LG_LOCK(files.lock); + if (dataset->stale || + (reply == REMOTE_REPLY_INITIAL && !dataset->current)) + { + LG_UNLOCK(files.lock); + freeRemoteRequest(request); + return false; + } + request->request = (LG_ClipboardFileRequest) + { + .dataset = dataset->dataset, + .request = nextRequestNL(), + .node = node->node, + .offset = offset, + .length = length, + .operation = operation, + }; + request->dataset = dataset; + request->directory = node; + request->reply = reply; + request->fuseRequest = fuseRequest; + request->fuseSize = fuseSize; + request->fuseOffset = fuseOffset; + request->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + request->next = files.remoteRequests; + files.remoteRequests = request; + ++dataset->requestPins; + LG_UNLOCK(files.lock); + const LG_ClipboardFileRequest descriptor = request->request; + if (fuseRequest) + fuse_req_interrupt_func(fuseRequest, interruptRemoteRequest, request); + + LG_LOCK(files.lock); + struct RemoteRequest * live = files.remoteRequests; + while (live && live != request) + live = live->next; + const bool sent = live && lgClipboard_fileRequest(&descriptor); + if (sent) + request->transportStarted = true; + LG_UNLOCK(files.lock); + if (!live) + return true; + if (sent) + return true; + return finishUnsentRemoteRequest(descriptor.request); +} + +static struct RemoteNode * datasetRootNL(struct RemoteDataset * dataset) +{ + return remoteNodeByInodeNL(dataset->ino); +} + +static void fuseLookup(fuse_req_t request, fuse_ino_t parent, + const char * name) +{ + LG_LOCK(files.lock); + if (parent == FUSE_ROOT_ID) + { + for (struct RemoteDataset * dataset = files.remoteDatasets; + dataset; dataset = dataset->next) + if (dataset->ready && !dataset->stale && + !strcmp(dataset->name, name)) + { + struct RemoteNode * node = datasetRootNL(dataset); + const bool pinned = pinRemoteOperationNL(dataset, true); + LG_UNLOCK(files.lock); + if (pinned) + replyEntry(request, node); + else + fuse_reply_err(request, ESTALE); + releaseRemoteOperation(pinned ? dataset : NULL); + return; + } + LG_UNLOCK(files.lock); + fuse_reply_err(request, ENOENT); + return; + } + struct RemoteNode * directory = remoteNodeByInodeNL(parent); + const int error = !directory ? ENOENT : + directory->dataset->stale ? ESTALE : + directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0; + if (error) + { + LG_UNLOCK(files.lock); + fuse_reply_err(request, error); + return; + } + struct RemoteNode * child = remoteChildNL(directory->dataset, parent, name); + const bool listed = directory->listed; + struct RemoteDataset * dataset = directory->dataset; + const bool pinned = pinRemoteOperationNL(dataset, false); + LG_UNLOCK(files.lock); + if (!pinned) + { + fuse_reply_err(request, ESTALE); + return; + } + if (child) + { + replyEntry(request, child); + releaseRemoteOperation(dataset); + return; + } + if (listed) + { + fuse_reply_err(request, ENOENT); + releaseRemoteOperation(dataset); + return; + } + if (!requestRemote(dataset, directory, + LG_CLIPBOARD_FILE_LIST, 0, 0, REMOTE_REPLY_LOOKUP, + request, 0, 0, name)) + fuse_reply_err(request, EIO); + releaseRemoteOperation(dataset); +} + +static void fillRootStat(struct stat * value) +{ + memset(value, 0, sizeof(*value)); + value->st_ino = FUSE_ROOT_ID; + value->st_mode = S_IFDIR | 0500; + value->st_nlink = 2; + value->st_uid = getuid(); + value->st_gid = getgid(); +} + +static void fuseGetattr(fuse_req_t request, fuse_ino_t ino, + struct fuse_file_info * info) +{ + (void)info; + struct stat value; + if (ino == FUSE_ROOT_ID) + fillRootStat(&value); + else + { + LG_LOCK(files.lock); + struct RemoteNode * node = remoteNodeByInodeNL(ino); + if (node && !node->dataset->stale) + fillStat(node, &value); + const bool stale = node && node->dataset->stale; + const bool found = node && !stale; + LG_UNLOCK(files.lock); + if (!found) + { + fuse_reply_err(request, stale ? ESTALE : ENOENT); + return; + } + } + fuse_reply_attr(request, &value, FILE_ATTR_TIMEOUT); +} + +static void fuseOpendir(fuse_req_t request, fuse_ino_t ino, + struct fuse_file_info * info) +{ + struct RemoteDataset * dataset = NULL; + if (ino != FUSE_ROOT_ID) + { + LG_LOCK(files.lock); + struct RemoteNode * node = remoteNodeByInodeNL(ino); + const int error = !node ? ENOENT : + node->dataset->stale ? ESTALE : + node->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0; + if (!error) + { + dataset = node->dataset; + ++dataset->openPins; + info->fh = dataset->acquisition; + } + LG_UNLOCK(files.lock); + if (error) + { + fuse_reply_err(request, error); + return; + } + } + if (fuse_reply_open(request, info) == 0 || !dataset) + return; + LG_LOCK(files.lock); + if (dataset->openPins) + --dataset->openPins; + const struct RemoteAction action = retireRemoteDatasetNL(dataset); + LG_UNLOCK(files.lock); + performRemoteAction(action); +} + +static void replyRootDirectory(fuse_req_t request, size_t size, off_t offset) +{ + char * buffer = malloc(size ? size : 1U); + if (!buffer) + { + fuse_reply_err(request, ENOMEM); + return; + } + size_t used = 0; + off_t index = 0; + struct stat attr; + fillRootStat(&attr); + const char * fixed[] = { ".", ".." }; + for (unsigned i = 0; i < 2; ++i, ++index) + { + if (offset > index) + continue; + const size_t entry = fuse_add_direntry(request, buffer + used, + size - used, fixed[i], &attr, index + 1); + if (entry > size - used) + goto done; + used += entry; + } + LG_LOCK(files.lock); + for (struct RemoteDataset * dataset = files.remoteDatasets; + dataset; dataset = dataset->next) + { + if (!dataset->ready || dataset->stale) + continue; + ++index; + if (offset > index) + continue; + struct RemoteNode * node = datasetRootNL(dataset); + fillStat(node, &attr); + const size_t entry = fuse_add_direntry(request, buffer + used, + size - used, dataset->name, &attr, index + 1); + if (entry > size - used) + break; + used += entry; + } + LG_UNLOCK(files.lock); + +done: + fuse_reply_buf(request, buffer, used); + free(buffer); +} + +static void fuseReaddir(fuse_req_t request, fuse_ino_t ino, + size_t size, off_t offset, struct fuse_file_info * info) +{ + if (ino == FUSE_ROOT_ID) + { + replyRootDirectory(request, size, offset); + return; + } + LG_LOCK(files.lock); + struct RemoteNode * directory = remoteNodeByInodeNL(ino); + const int error = !directory ? ENOENT : + directory->dataset->stale || + info->fh != directory->dataset->acquisition ? ESTALE : + directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? ENOTDIR : 0; + if (error) + { + LG_UNLOCK(files.lock); + fuse_reply_err(request, error); + return; + } + const bool listed = directory->listed; + struct RemoteDataset * dataset = directory->dataset; + const bool pinned = pinRemoteOperationNL(dataset, false); + LG_UNLOCK(files.lock); + if (!pinned) + { + fuse_reply_err(request, ESTALE); + return; + } + if (listed) + { + struct RemoteRequest local = + { + .dataset = dataset, + .directory = directory, + .fuseRequest = request, + .fuseSize = size, + .fuseOffset = offset, + }; + replyDirectory(&local); + releaseRemoteOperation(dataset); + return; + } + if (!requestRemote(dataset, directory, + LG_CLIPBOARD_FILE_LIST, 0, 0, REMOTE_REPLY_READDIR, + request, size, offset, NULL)) + fuse_reply_err(request, EIO); + releaseRemoteOperation(dataset); +} + +static void fuseOpen(fuse_req_t request, fuse_ino_t ino, + struct fuse_file_info * info) +{ + LG_LOCK(files.lock); + struct RemoteNode * node = remoteNodeByInodeNL(ino); + const int error = !node ? ENOENT : + node->dataset->stale ? ESTALE : + node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY ? EISDIR : 0; + if (error) + { + LG_UNLOCK(files.lock); + fuse_reply_err(request, error); + return; + } + if ((info->flags & O_ACCMODE) != O_RDONLY) + { + LG_UNLOCK(files.lock); + fuse_reply_err(request, EROFS); + return; + } + struct RemoteDataset * dataset = node->dataset; + ++dataset->openPins; + info->fh = dataset->acquisition; + LG_UNLOCK(files.lock); + info->keep_cache = 1; + if (fuse_reply_open(request, info) == 0) + return; + LG_LOCK(files.lock); + if (dataset->openPins) + --dataset->openPins; + const struct RemoteAction action = retireRemoteDatasetNL(dataset); + LG_UNLOCK(files.lock); + performRemoteAction(action); +} + +static void fuseRead(fuse_req_t request, fuse_ino_t ino, size_t size, + off_t offset, struct fuse_file_info * info) +{ + if (offset < 0) + { + fuse_reply_err(request, EINVAL); + return; + } + LG_LOCK(files.lock); + struct RemoteNode * node = remoteNodeByInodeNL(ino); + const int error = !node ? ENOENT : + node->dataset->stale || + info->fh != node->dataset->acquisition ? ESTALE : + node->type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR ? EISDIR : 0; + if (error) + { + LG_UNLOCK(files.lock); + fuse_reply_err(request, error); + return; + } + const uint64_t fileSize = node->size; + struct RemoteDataset * dataset = node->dataset; + const bool pinned = pinRemoteOperationNL(dataset, false); + LG_UNLOCK(files.lock); + if (!pinned) + { + fuse_reply_err(request, ESTALE); + return; + } + if ((uint64_t)offset >= fileSize || !size) + { + fuse_reply_buf(request, NULL, 0); + releaseRemoteOperation(dataset); + return; + } + uint64_t wanted = fileSize - (uint64_t)offset; + if (wanted > size) + wanted = size; + if (wanted > FILE_REQUEST_MAX) + wanted = FILE_REQUEST_MAX; + if (!requestRemote(dataset, node, LG_CLIPBOARD_FILE_READ, + (uint64_t)offset, (uint32_t)wanted, REMOTE_REPLY_READ, + request, 0, 0, NULL)) + fuse_reply_err(request, EIO); + releaseRemoteOperation(dataset); +} + +static void fuseStatfs(fuse_req_t request, fuse_ino_t ino) +{ + (void)ino; + struct statvfs value; + memset(&value, 0, sizeof(value)); + value.f_bsize = 4096; + value.f_frsize = 4096; + value.f_namemax = NAME_MAX; + value.f_flag = ST_RDONLY | ST_NOSUID; + fuse_reply_statfs(request, &value); +} + +static void fuseAccess(fuse_req_t request, fuse_ino_t ino, int mask) +{ + if (mask & W_OK) + { + fuse_reply_err(request, EROFS); + return; + } + if (ino == FUSE_ROOT_ID) + { + fuse_reply_err(request, 0); + return; + } + LG_LOCK(files.lock); + struct RemoteNode * node = remoteNodeByInodeNL(ino); + const bool found = node && !node->dataset->stale; + const bool stale = node && node->dataset->stale; + LG_UNLOCK(files.lock); + fuse_reply_err(request, found ? 0 : stale ? ESTALE : ENOENT); +} + +static void forgetRemoteNode(fuse_ino_t ino, uint64_t count) +{ + LG_LOCK(files.lock); + struct RemoteNode * node = remoteNodeByInodeNL(ino); + struct RemoteAction action = { 0 }; + if (node) + { + if (count > node->lookups) + count = node->lookups; + node->lookups -= count; + if (count > node->dataset->lookupPins) + node->dataset->lookupPins = 0; + else + node->dataset->lookupPins -= count; + action = retireRemoteDatasetNL(node->dataset); + } + LG_UNLOCK(files.lock); + performRemoteAction(action); +} + +static void fuseForget(fuse_req_t request, fuse_ino_t ino, + uint64_t count) +{ + if (ino != FUSE_ROOT_ID) + forgetRemoteNode(ino, count); + fuse_reply_none(request); +} + +static void fuseForgetMulti(fuse_req_t request, size_t count, + struct fuse_forget_data * forgets) +{ + for (size_t i = 0; i < count; ++i) + if (forgets[i].ino != FUSE_ROOT_ID) + forgetRemoteNode(forgets[i].ino, forgets[i].nlookup); + fuse_reply_none(request); +} + +static void releaseRemoteHandle(fuse_req_t request, + struct fuse_file_info * info) +{ + if (!info->fh) + { + fuse_reply_err(request, 0); + return; + } + LG_LOCK(files.lock); + struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(info->fh); + const bool found = dataset != NULL; + if (dataset && dataset->openPins) + --dataset->openPins; + const struct RemoteAction action = retireRemoteDatasetNL(dataset); + LG_UNLOCK(files.lock); + fuse_reply_err(request, found ? 0 : ESTALE); + performRemoteAction(action); +} + +static void fuseRelease(fuse_req_t request, fuse_ino_t ino, + struct fuse_file_info * info) +{ + (void)ino; + releaseRemoteHandle(request, info); +} + +static void fuseReleasedir(fuse_req_t request, fuse_ino_t ino, + struct fuse_file_info * info) +{ + (void)ino; + releaseRemoteHandle(request, info); +} + +static const struct fuse_lowlevel_ops fuseOps = +{ + .lookup = fuseLookup, + .forget = fuseForget, + .forget_multi = fuseForgetMulti, + .getattr = fuseGetattr, + .opendir = fuseOpendir, + .readdir = fuseReaddir, + .releasedir = fuseReleasedir, + .open = fuseOpen, + .read = fuseRead, + .release = fuseRelease, + .access = fuseAccess, + .statfs = fuseStatfs, +}; + +enum FuseWaitResult +{ + FUSE_WAIT_ERROR, + FUSE_WAIT_SESSION, + FUSE_WAIT_WAKE, + FUSE_WAIT_TIMEOUT, +}; + +static enum FuseWaitResult waitFuseEvent( + int sessionFd, int stopFd, int timeout) +{ + struct pollfd descriptors[] = + { + { .fd = sessionFd, .events = POLLIN }, + { .fd = stopFd, .events = POLLIN }, + }; + int result; + do + result = poll(descriptors, + sizeof(descriptors) / sizeof(*descriptors), timeout); + while (result < 0 && errno == EINTR); + if (result < 0) + return FUSE_WAIT_ERROR; + if (!result) + return FUSE_WAIT_TIMEOUT; + if (descriptors[1].revents & POLLIN) + { + uint64_t value; + ssize_t received; + do + received = read(stopFd, &value, sizeof(value)); + while (received < 0 && errno == EINTR); + if (received != (ssize_t)sizeof(value) && + !(received < 0 && errno == EAGAIN)) + return FUSE_WAIT_ERROR; + return FUSE_WAIT_WAKE; + } + if (descriptors[1].revents) + { + errno = EIO; + return FUSE_WAIT_ERROR; + } + if (descriptors[0].revents & POLLIN) + return FUSE_WAIT_SESSION; + errno = EIO; + return FUSE_WAIT_ERROR; +} + +static bool wakeFuseThread(int stopFd) +{ + const uint64_t value = 1; + ssize_t result; + do + result = write(stopFd, &value, sizeof(value)); + while (result < 0 && errno == EINTR); + return result == (ssize_t)sizeof(value) || + (result < 0 && errno == EAGAIN); +} + +static int fuseThread(void * opaque) +{ + struct fuse_session * session = opaque; + struct fuse_buf buffer = + { + .mem = NULL, + }; + int result = 0; + while (!fuse_session_exited(session)) + { + switch (waitFuseEvent(fuse_session_fd(session), files.fuseStopFd, + nextRemoteDeliveryTimeout())) + { + case FUSE_WAIT_WAKE: + { + LG_LOCK(files.lock); + const bool stopping = files.stopping; + LG_UNLOCK(files.lock); + if (stopping) + goto done; + reapRemoteDeliveries(false); + continue; + } + + case FUSE_WAIT_TIMEOUT: + reapRemoteDeliveries(false); + continue; + + case FUSE_WAIT_ERROR: + result = -errno; + goto done; + + case FUSE_WAIT_SESSION: + break; + } + + result = fuse_session_receive_buf(session, &buffer); + if (result == -EINTR) + continue; + if (result <= 0) + goto done; + fuse_session_process_buf(session, &buffer); + } + +done: + free(buffer.mem); + if (result > 0) + result = 0; + fuse_session_reset(session); + return result; +} + +static char * uriDecode(const char * value, size_t length) +{ + static const char hex[] = "0123456789abcdef"; + char * result = malloc(length + 1U); + if (!result) + return NULL; + size_t out = 0; + for (size_t i = 0; i < length; ++i) + { + unsigned char ch = (unsigned char)value[i]; + if (ch == '%') + { + if (i + 2U >= length) + goto invalid; + const char * high = strchr(hex, (char)(value[i + 1U] | 0x20)); + const char * low = strchr(hex, (char)(value[i + 2U] | 0x20)); + if (!high || !low) + goto invalid; + ch = (unsigned char)(((high - hex) << 4) | (low - hex)); + i += 2U; + } + if (!ch) + goto invalid; + result[out++] = (char)ch; + } + result[out] = 0; + return result; + +invalid: + free(result); + return NULL; +} + +static bool localNodeStat(struct LocalNode * node, const struct stat * value) +{ + if (S_ISREG(value->st_mode)) + node->type = KVMFR_CLIPBOARD_FILE_TYPE_REGULAR; + else if (S_ISDIR(value->st_mode)) + node->type = KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY; + else + return false; + node->size = value->st_size < 0 ? 0 : (uint64_t)value->st_size; + node->modifiedNs = nsFromTimespec(value->st_mtim); + node->createdNs = 0; + node->device = value->st_dev; + node->inode = value->st_ino; + return true; +} + +static bool localStatSupported(const struct stat * value) +{ + return S_ISREG(value->st_mode) || S_ISDIR(value->st_mode); +} + +static bool localStatEqual(const struct stat * first, + const struct stat * second) +{ + return first->st_dev == second->st_dev && + first->st_ino == second->st_ino && + ((S_ISREG(first->st_mode) && S_ISREG(second->st_mode)) || + (S_ISDIR(first->st_mode) && S_ISDIR(second->st_mode))) && + first->st_size == second->st_size && + first->st_mtim.tv_sec == second->st_mtim.tv_sec && + first->st_mtim.tv_nsec == second->st_mtim.tv_nsec; +} + +static bool localNodeMatches(const struct LocalNode * node, + const struct stat * value) +{ + return node->device == value->st_dev && node->inode == value->st_ino && + ((node->type == KVMFR_CLIPBOARD_FILE_TYPE_REGULAR && + S_ISREG(value->st_mode)) || + (node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY && + S_ISDIR(value->st_mode))) && + node->size == (value->st_size < 0 ? 0 : (uint64_t)value->st_size) && + node->modifiedNs == nsFromTimespec(value->st_mtim); +} + +static int openAbsoluteLocal(const char * path, char ** name, + struct stat * value) +{ + if (!path || path[0] != '/' || !path[1]) + return -1; + + int current = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (current < 0) + return -1; + + const char * component = path + 1; + for (;;) + { + const char * slash = strchr(component, '/'); + const size_t length = slash ? (size_t)(slash - component) : + strlen(component); + if (!length || length > NAME_MAX || + (length == 1U && component[0] == '.') || + (length == 2U && component[0] == '.' && component[1] == '.')) + goto failed; + + char * part = strndup(component, length); + if (!part) + goto failed; + + struct stat before; + const bool final = !slash; + int next = -1; + if (fstatat(current, part, &before, AT_SYMLINK_NOFOLLOW) == 0 && + !S_ISLNK(before.st_mode) && + (final ? localStatSupported(&before) : S_ISDIR(before.st_mode))) + { + int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK; + if (!final || S_ISDIR(before.st_mode)) + flags |= O_DIRECTORY; + next = openat(current, part, flags); + } + if (next < 0) + { + free(part); + goto failed; + } + + struct stat after; + if (fstat(next, &after) < 0 || !localStatEqual(&before, &after)) + { + close(next); + free(part); + goto failed; + } + close(current); + current = next; + + if (final) + { + *name = part; + *value = after; + return current; + } + free(part); + component = slash + 1; + } + +failed: + close(current); + return -1; +} + +static int openLocalNode(const struct LocalNode * node, + struct stat * value) +{ + if (!node || !node->root || node->root->fd < 0) + return -1; + + int current; + if (node->root->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) + current = openat(node->root->fd, ".", + O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + else + current = fcntl(node->root->fd, F_DUPFD_CLOEXEC, 0); + if (current < 0) + return -1; + + const char * component = node->path; + while (*component) + { + const char * slash = strchr(component, '/'); + const size_t length = slash ? (size_t)(slash - component) : + strlen(component); + if (!length || length > NAME_MAX || + (length == 1U && component[0] == '.') || + (length == 2U && component[0] == '.' && component[1] == '.')) + goto failed; + char * part = strndup(component, length); + if (!part) + goto failed; + int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK; + if (slash || node->type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) + flags |= O_DIRECTORY; + const int next = openat(current, part, flags); + free(part); + if (next < 0) + goto failed; + close(current); + current = next; + component = slash ? slash + 1 : component + length; + } + + if (fstat(current, value) < 0) + goto failed; + if (!localNodeMatches(node, value)) + { + errno = ESTALE; + goto failed; + } + return current; + +failed: + close(current); + return -1; +} + +static void freeLocalDataset(struct LocalDataset * dataset) +{ + if (!dataset) + return; + struct LocalNode * node = dataset->nodes; + while (node) + { + struct LocalNode * next = node->next; + if (node->fd >= 0) + close(node->fd); + free(node->path); + free(node->name); + free(node); + node = next; + } + free(dataset); +} + +static struct LocalDataset * takeUnusedLocalDatasetNL( + struct LocalDataset * dataset) +{ + if (!dataset || dataset->current || dataset->references) + return NULL; + struct LocalDataset ** link = &files.localDatasets; + while (*link && *link != dataset) + link = &(*link)->next; + if (!*link) + return NULL; + *link = dataset->next; + dataset->next = NULL; + return dataset; +} + +static char * uriPath(const char * line, size_t length) +{ + if (length < strlen(FILE_URI_PREFIX) || + strncasecmp(line, FILE_URI_PREFIX, strlen(FILE_URI_PREFIX))) + return NULL; + line += strlen(FILE_URI_PREFIX); + length -= strlen(FILE_URI_PREFIX); + + if (length >= 10U && !strncasecmp(line, "localhost/", 10U)) + { + line += 9U; + length -= 9U; + } + else if (!length || line[0] != '/') + return NULL; + + static const char punctuation[] = "-._~!$&'()*+,;=:@/%"; + for (size_t i = 0; i < length; ++i) + { + const unsigned char ch = (unsigned char)line[i]; + if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || strchr(punctuation, ch))) + return NULL; + } + char * path = uriDecode(line, length); + if (!path) + return NULL; + size_t pathLength = strlen(path); + while (pathLength > 1U && path[pathLength - 1U] == '/') + path[--pathLength] = 0; + return path; +} + +static struct LocalNode * localNodeByIdNL(struct LocalDataset * dataset, + uint64_t node) +{ + for (struct LocalNode * item = dataset->nodes; item; item = item->next) + if (item->node == node) + return item; + return NULL; +} + +static struct LocalNode * localChildNL(struct LocalDataset * dataset, + uint64_t parent, const char * name) +{ + for (struct LocalNode * item = dataset->nodes; item; item = item->next) + if (item->parent == parent && !strcmp(item->name, name)) + return item; + return NULL; +} + +static struct LocalNode * addLocalNode(struct LocalDataset * dataset, + uint64_t parent, struct LocalNode * root, char * path, + const char * name, const struct stat * value) +{ + struct LocalNode * node = calloc(1, sizeof(*node)); + if (!node) + { + free(path); + return NULL; + } + node->fd = -1; + node->path = path; + node->name = strdup(name); + if (!node->name || !localNodeStat(node, value)) + { + free(node->path); + free(node->name); + free(node); + return NULL; + } + node->dataset = dataset; + node->root = root ? root : node; + node->parent = parent; + node->node = ++dataset->nodeSerial; + node->next = dataset->nodes; + dataset->nodes = node; + return node; +} + +bool lgClipboardFiles_setLocal(const char * mime, + const void * data, size_t size) +{ + if (!mime || (!data && size) || + (strcmp(mime, FILE_URI_MIME) && strcmp(mime, FILE_GNOME_MIME))) + return false; + const char * text = data; + size_t offset = 0; + if (!strcmp(mime, FILE_GNOME_MIME)) + { + const char * newline = size ? memchr(text, '\n', size) : NULL; + if (!newline) + return false; + const size_t operationLength = (size_t)(newline - text); + if ((operationLength != 4U || memcmp(text, "copy", 4U)) && + (operationLength != 3U || memcmp(text, "cut", 3U))) + return false; + offset = operationLength + 1U; + } + struct LocalDataset * dataset = calloc(1, sizeof(*dataset)); + if (!dataset) + return false; + while (offset < size) + { + const char * line = text + offset; + const char * newline = memchr(line, '\n', size - offset); + size_t length = newline ? (size_t)(newline - line) : size - offset; + if (length && line[length - 1U] == '\r') + --length; + offset += (newline ? (size_t)(newline - line) + 1U : size - offset); + if (!length || line[0] == '#') + continue; + char * path = uriPath(line, length); + if (!path) + goto invalid; + char * name = NULL; + struct stat value; + const int fd = openAbsoluteLocal(path, &name, &value); + free(path); + if (fd < 0) + { + free(name); + goto invalid; + } + char * relative = strdup(""); + if (!relative || !validUtf8((const uint8_t *)name, strlen(name)) || + localChildNL(dataset, KVMFR_CLIPBOARD_FILE_ROOT_NODE, name)) + { + close(fd); + free(relative); + free(name); + goto invalid; + } + struct LocalNode * node = addLocalNode(dataset, + KVMFR_CLIPBOARD_FILE_ROOT_NODE, NULL, relative, name, &value); + free(name); + if (!node) + { + close(fd); + goto invalid; + } + node->fd = fd; + } + if (!dataset->nodes) + goto invalid; + dataset->current = true; + LG_LOCK(files.lock); + dataset->dataset = nextDatasetNL(); + struct LocalDataset * retired = NULL; + if (files.localCurrent) + { + files.localCurrent->current = false; + retired = takeUnusedLocalDatasetNL(files.localCurrent); + } + dataset->next = files.localDatasets; + files.localDatasets = dataset; + files.localCurrent = dataset; + const uint64_t datasetId = dataset->dataset; + LG_UNLOCK(files.lock); + freeLocalDataset(retired); + lgClipboard_notifyFiles(datasetId); + return true; + +invalid: + freeLocalDataset(dataset); + return false; +} + +void lgClipboardFiles_clearLocal(void) +{ + LG_LOCK(files.lock); + struct LocalDataset * retired = NULL; + if (files.localCurrent) + { + files.localCurrent->current = false; + retired = takeUnusedLocalDatasetNL(files.localCurrent); + } + files.localCurrent = NULL; + LG_UNLOCK(files.lock); + freeLocalDataset(retired); +} + +static char * uriEncodePath(const char * path) +{ + static const char hex[] = "0123456789ABCDEF"; + const size_t length = strlen(path); + if (length > (SIZE_MAX - 1U) / 3U) + return NULL; + char * result = malloc(length * 3U + 1U); + if (!result) + return NULL; + size_t out = 0; + for (size_t i = 0; i < length; ++i) + { + const unsigned char ch = (unsigned char)path[i]; + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || ch == '/' || ch == '-' || + ch == '_' || ch == '.' || ch == '~') + result[out++] = (char)ch; + else + { + result[out++] = '%'; + result[out++] = hex[ch >> 4]; + result[out++] = hex[ch & 0xfU]; + } + } + result[out] = 0; + return result; +} + +static bool getRemoteDatasetNL(struct RemoteDataset * dataset, + const char * mime, char ** data, size_t * size) +{ + if (!strcmp(mime, FILE_KDE_CUT_MIME)) + { + *data = strdup("0"); + *size = *data ? 1U : 0U; + return *data != NULL; + } + if (strcmp(mime, FILE_URI_MIME) && strcmp(mime, FILE_GNOME_MIME)) + return false; + char * result = NULL; + size_t used = 0; + size_t capacity = 0; + if (!strcmp(mime, FILE_GNOME_MIME)) + { + if (!grow((void **)&result, &capacity, 6U)) + return false; + memcpy(result, "copy\n", 5U); + used = 5U; + } + for (struct RemoteNode * node = files.remoteNodes; node; node = node->next) + { + if (node->dataset != dataset || node->parent != dataset->ino) + continue; + const size_t rawLength = strlen(files.mountpoint) + 1U + + strlen(dataset->name) + 1U + strlen(node->name) + 1U; + char * raw = malloc(rawLength); + if (!raw) + goto error; + snprintf(raw, rawLength, "%s/%s/%s", + files.mountpoint, dataset->name, node->name); + char * encoded = uriEncodePath(raw); + free(raw); + if (!encoded) + goto error; + const size_t line = strlen(FILE_URI_PREFIX) + strlen(encoded) + 2U; + if (!grow((void **)&result, &capacity, used + line + 1U)) + { + free(encoded); + goto error; + } + used += (size_t)snprintf(result + used, capacity - used, + FILE_URI_PREFIX "%s\r\n", encoded); + free(encoded); + } + if (!result) + return false; + *data = result; + *size = used; + return true; + +error: + free(result); + return false; +} + +bool lgClipboardFiles_getRemote(const char * mime, + char ** data, size_t * size) +{ + if (!mime || !data || !size) + return false; + *data = NULL; + *size = 0; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = files.remoteCurrent; + const bool result = dataset && dataset->ready && !dataset->stale && + getRemoteDatasetNL(dataset, mime, data, size); + LG_UNLOCK(files.lock); + return result; +} + +uint64_t lgClipboardFiles_remotePresentationAcquire(void) +{ + LG_LOCK(files.lock); + struct RemoteDataset * dataset = files.remoteCurrent; + const uint64_t presentation = dataset && dataset->ready && + dataset->acquired && !dataset->stale ? dataset->acquisition : 0; + if (presentation) + ++dataset->presentationPins; + LG_UNLOCK(files.lock); + return presentation; +} + +bool lgClipboardFiles_getRemotePresentation(uint64_t presentation, + const char * mime, char ** data, size_t * size) +{ + if (!presentation || !mime || !data || !size) + return false; + *data = NULL; + *size = 0; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(presentation); + const bool result = dataset && dataset->presentationPins && + dataset->ready && !dataset->stale && + getRemoteDatasetNL(dataset, mime, data, size); + LG_UNLOCK(files.lock); + return result; +} + +void lgClipboardFiles_remotePresentationDelivered(uint64_t presentation) +{ + if (!presentation) + return; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = + remoteDatasetByAcquisitionNL(presentation); + const bool delivered = dataset && dataset->presentationPins && + dataset->ready && dataset->acquired && !dataset->stale; + int wakeFd = -1; + if (delivered) + { + const uint64_t now = microtime(); + dataset->deliveryDeadline = now > + UINT64_MAX - FILE_DELIVERY_GRACE_US ? UINT64_MAX : + now + FILE_DELIVERY_GRACE_US; + dataset->deliveryPending = true; + wakeFd = files.fuseStopFd; + } + LG_UNLOCK(files.lock); + if (wakeFd >= 0 && !wakeFuseThread(wakeFd)) + DEBUG_WARN("Failed to schedule clipboard file delivery expiry: %s", + strerror(errno)); + if (delivered) + reapRemoteDeliveries(true); +} + +void lgClipboardFiles_remotePresentationRelease(uint64_t presentation) +{ + if (!presentation) + return; + LG_LOCK(files.lock); + struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(presentation); + if (dataset && dataset->presentationPins) + --dataset->presentationPins; + const struct RemoteAction action = retireRemoteDatasetNL(dataset); + LG_UNLOCK(files.lock); + performRemoteAction(action); +} + +bool lgClipboardFiles_remoteReady(uint64_t dataset) +{ + LG_LOCK(files.lock); + struct RemoteDataset * found = remoteDatasetByIdNL(dataset); + const bool result = found && found->ready && found->current; + LG_UNLOCK(files.lock); + return result; +} + +static struct RemoteRequest * takeInitialRequestNL( + struct RemoteDataset * dataset) +{ + struct RemoteRequest * request = files.remoteRequests; + while (request && (request->dataset != dataset || + request->reply != REMOTE_REPLY_INITIAL)) + request = request->next; + return request ? remoteRequestTakePointerNL(request) : NULL; +} + +static void cancelRetiredInitialRequest(struct RemoteRequest * request) +{ + if (!request) + return; + const LG_ClipboardFileRequest descriptor = request->request; + if (request->transportStarted && + !lgClipboard_fileCancel(descriptor.dataset, descriptor.request, + LG_CLIPBOARD_FILE_ERROR_CANCELLED)) + DEBUG_WARN("Failed to cancel retired clipboard file request"); + unregisterRemoteInterrupt(request); + completeRemoteRequest(request); +} + +bool lgClipboardFiles_remoteOffer(uint64_t datasetId) +{ + DEBUG_INFO("Remote clipboard file offer received: dataset=%" PRIu64, + datasetId); + if (!datasetId) + return false; + struct RemoteDataset * dataset = calloc(1, sizeof(*dataset)); + struct RemoteNode * root = calloc(1, sizeof(*root)); + if (!dataset || !root) + { + free(dataset); + free(root); + return false; + } + LG_LOCK(files.lock); + if (files.remoteCurrent && files.remoteCurrent->dataset == datasetId && + !files.remoteCurrent->stale) + { + LG_UNLOCK(files.lock); + free(dataset); + free(root); + return true; + } + struct RemoteDataset * reusable = remoteDatasetByIdNL(datasetId); + if (reusable && reusable->stale) + reusable = NULL; + if (!reusable) + { + dataset->dataset = datasetId; + dataset->acquisition = nextAcquisitionNL(); + dataset->ino = nextInodeNL(); + dataset->current = true; + dataset->acquiring = true; + snprintf(dataset->name, sizeof(dataset->name), + "%016" PRIx64 "-%016" PRIx64, + datasetId, (uint64_t)dataset->ino); + *root = (struct RemoteNode) + { + .dataset = dataset, + .ino = dataset->ino, + .parent = FUSE_ROOT_ID, + .node = KVMFR_CLIPBOARD_FILE_ROOT_NODE, + .type = KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY, + .name = strdup(dataset->name), + }; + if (!root->name) + { + LG_UNLOCK(files.lock); + free(dataset); + free(root); + return false; + } + } + struct RemoteDataset * previous = files.remoteCurrent; + struct RemoteRequest * cancelled = NULL; + struct RemoteAction previousAction = { 0 }; + if (previous) + { + previous->current = false; + cancelled = takeInitialRequestNL(previous); + previousAction = retireRemoteDatasetNL(previous); + } + if (reusable) + { + reusable->current = true; + files.remoteCurrent = reusable; + LG_UNLOCK(files.lock); + cancelRetiredInitialRequest(cancelled); + performRemoteAction(previousAction); + reapRemoteDeliveries(true); + free(dataset); + free(root); + return true; + } + dataset->next = files.remoteDatasets; + files.remoteDatasets = dataset; + files.remoteCurrent = dataset; + root->next = files.remoteNodes; + files.remoteNodes = root; + const uint64_t acquisition = dataset->acquisition; + LG_UNLOCK(files.lock); + cancelRetiredInitialRequest(cancelled); + performRemoteAction(previousAction); + reapRemoteDeliveries(true); + if (!lgClipboard_fileAcquire(datasetId, acquisition)) + { + lgClipboardFiles_remoteCancel(datasetId, acquisition, + LG_CLIPBOARD_FILE_ERROR_DISCONNECTED); + return false; + } + return true; +} + +void lgClipboardFiles_remoteClear(void) +{ + LG_LOCK(files.lock); + struct RemoteDataset * previous = files.remoteCurrent; + struct RemoteRequest * cancelled = NULL; + struct RemoteAction action = { 0 }; + if (previous) + { + previous->current = false; + cancelled = takeInitialRequestNL(previous); + action = retireRemoteDatasetNL(previous); + } + files.remoteCurrent = NULL; + LG_UNLOCK(files.lock); + cancelRetiredInitialRequest(cancelled); + performRemoteAction(action); +} + +void lgClipboardFiles_providerUnavailable(void) +{ + if (!files.initialized) + return; + + LG_LOCK(files.lock); + files.remoteCurrent = NULL; + for (struct RemoteDataset * dataset = files.remoteDatasets; + dataset; dataset = dataset->next) + { + dataset->current = false; + dataset->acquiring = false; + dataset->acquired = false; + dataset->stale = true; + dataset->deliveryPending = false; + dataset->deliveryDeadline = 0; + } + struct RemoteRequest * remoteRequests = files.remoteRequests; + files.remoteRequests = NULL; + + struct LocalAcquisition * localAcquisitions = files.localAcquisitions; + files.localAcquisitions = NULL; + for (struct LocalAcquisition * acquisition = localAcquisitions; + acquisition; acquisition = acquisition->next) + if (acquisition->dataset->references) + --acquisition->dataset->references; + + struct LocalDataset * retired = NULL; + struct LocalDataset ** datasetLink = &files.localDatasets; + while (*datasetLink) + { + struct LocalDataset * dataset = *datasetLink; + if (dataset->current || dataset->references) + { + datasetLink = &dataset->next; + continue; + } + *datasetLink = dataset->next; + dataset->next = retired; + retired = dataset; + } + + for (struct LocalJob * job = files.jobsHead; job; job = job->next) + atomic_store_explicit(&job->cancelled, true, memory_order_release); + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + { + atomic_store_explicit(&job->cancelled, true, memory_order_release); + lgSignalEvent(job->ready); + } + LG_UNLOCK(files.lock); + if (files.jobEvent) + lgSignalEvent(files.jobEvent); + + while (remoteRequests) + { + struct RemoteRequest * next = remoteRequests->next; + unregisterRemoteInterrupt(remoteRequests); + if (remoteRequests->fuseRequest) + fuse_reply_err(remoteRequests->fuseRequest, EIO); + completeRemoteRequest(remoteRequests); + remoteRequests = next; + } + pruneUnusedRemoteDatasets(); + while (localAcquisitions) + { + struct LocalAcquisition * next = localAcquisitions->next; + free(localAcquisitions); + localAcquisitions = next; + } + while (retired) + { + struct LocalDataset * next = retired->next; + freeLocalDataset(retired); + retired = next; + } +} + +void lgClipboardFiles_remoteAcquired(uint64_t datasetId, + uint64_t acquisition, LG_ClipboardFileError error) +{ + LG_LOCK(files.lock); + struct RemoteDataset * dataset = remoteDatasetByAcquisitionNL(acquisition); + const bool valid = dataset && dataset->dataset == datasetId && + dataset->acquiring && !dataset->stale; + if (valid) + { + dataset->acquiring = false; + dataset->acquired = !error; + } + struct RemoteNode * root = valid ? datasetRootNL(dataset) : NULL; + const bool pinned = valid && !error && root && + pinRemoteOperationNL(dataset, false); + const uint64_t failedDataset = valid && (error || !pinned) ? + failCurrentRemoteDatasetNL(dataset) : 0; + const struct RemoteAction action = valid ? + retireRemoteDatasetNL(dataset) : (struct RemoteAction) { 0 }; + LG_UNLOCK(files.lock); + if (valid && error) + DEBUG_WARN("Remote clipboard file acquisition failed: " + "dataset=%" PRIu64 ", acquisition=%" PRIu64 ", error=%u (%s)", + datasetId, acquisition, (unsigned)error, fileErrorName(error)); + else if (valid) + DEBUG_INFO("Remote clipboard file acquisition succeeded: " + "dataset=%" PRIu64 ", acquisition=%" PRIu64, + datasetId, acquisition); + performRemoteAction(action); + if (failedDataset) + lgClipboard_fileRemoteFailed(failedDataset); + if (!pinned) + return; + if (!requestRemote(dataset, root, LG_CLIPBOARD_FILE_LIST, + 0, 0, REMOTE_REPLY_INITIAL, NULL, 0, 0, NULL)) + { + LG_LOCK(files.lock); + const uint64_t unsentDataset = + failCurrentRemoteDatasetNL(dataset); + const struct RemoteAction failed = retireRemoteDatasetNL(dataset); + LG_UNLOCK(files.lock); + performRemoteAction(failed); + if (unsentDataset) + lgClipboard_fileRemoteFailed(unsentDataset); + } + releaseRemoteOperation(dataset); +} + +static bool sameRemoteFileRequest( + const struct RemoteRequest * request, + const LG_ClipboardFileRequest * descriptor) +{ + return request->request.dataset == descriptor->dataset && + request->request.request == descriptor->request && + request->request.node == descriptor->node && + request->request.offset == descriptor->offset && + request->request.length == descriptor->length && + request->request.operation == descriptor->operation; +} + +LG_ClipboardResult lgClipboardFiles_remoteDataBegin( + const LG_ClipboardFileRequest * descriptor, uint64_t sizeHint) +{ + if (!descriptor) + return LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK(files.lock); + struct RemoteRequest * request = files.remoteRequests; + while (request && request->request.request != descriptor->request) + request = request->next; + const bool valid = request && !request->began && + sameRemoteFileRequest(request, descriptor) && + (descriptor->operation != LG_CLIPBOARD_FILE_READ || + sizeHint == descriptor->length); + if (valid) + { + request->began = true; + request->sizeHint = sizeHint; + } + struct RemoteRequest * failed = !valid && request ? + remoteRequestTakeNL(descriptor->request) : NULL; + LG_UNLOCK(files.lock); + failRemoteRequest(failed, EIO); + return valid ? LG_CLIPBOARD_RESULT_ACCEPTED : + LG_CLIPBOARD_RESULT_FAILED; +} + +LG_ClipboardResult lgClipboardFiles_remoteDataChunk( + const LG_ClipboardFileRequest * descriptor, uint64_t responseOffset, + const void * data, size_t size) +{ + if (!descriptor) + return LG_CLIPBOARD_RESULT_FAILED; + const bool envelopeValid = data && size && responseOffset <= SIZE_MAX && + size <= SIZE_MAX - (size_t)responseOffset; + LG_LOCK(files.lock); + struct RemoteRequest * request = files.remoteRequests; + while (request && request->request.request != descriptor->request) + request = request->next; + const size_t end = envelopeValid ? (size_t)responseOffset + size : 0; + const bool valid = envelopeValid && request && request->began && + sameRemoteFileRequest(request, descriptor) && + responseOffset == request->dataSize && + (request->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || + end <= request->sizeHint) && + (descriptor->operation != LG_CLIPBOARD_FILE_READ || + end <= descriptor->length) && + grow((void **)&request->data, &request->dataCapacity, end); + if (valid) + { + memcpy(request->data + request->dataSize, data, size); + request->dataSize = end; + } + struct RemoteRequest * failed = !valid && request ? + remoteRequestTakeNL(descriptor->request) : NULL; + LG_UNLOCK(files.lock); + failRemoteRequest(failed, EIO); + return valid ? LG_CLIPBOARD_RESULT_ACCEPTED : + LG_CLIPBOARD_RESULT_FAILED; +} + +LG_ClipboardResult lgClipboardFiles_remoteDataEnd( + const LG_ClipboardFileRequest * descriptor, uint64_t finalSize) +{ + if (!descriptor) + return LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK(files.lock); + struct RemoteRequest * request = remoteRequestTakeNL(descriptor->request); + const bool valid = finalSize <= SIZE_MAX && request && request->began && + sameRemoteFileRequest(request, descriptor) && + request->dataSize == (size_t)finalSize && + (request->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || + request->sizeHint == finalSize) && + (descriptor->operation != LG_CLIPBOARD_FILE_READ || + finalSize == descriptor->length); + LG_UNLOCK(files.lock); + if (!valid) + { + failRemoteRequest(request, EIO); + return LG_CLIPBOARD_RESULT_FAILED; + } + finishRemoteRequest(request); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +void lgClipboardFiles_remoteCancel(uint64_t dataset, + uint64_t requestId, LG_ClipboardFileError reason) +{ + LG_LOCK(files.lock); + struct RemoteRequest * request = remoteRequestTakeNL(requestId); + bool acquisition = false; + uint64_t failedDataset = 0; + struct RemoteAction action = { 0 }; + for (struct RemoteDataset * item = files.remoteDatasets; + item; item = item->next) + if (item->dataset == dataset && item->acquisition == requestId) + { + item->acquiring = false; + item->acquired = false; + failedDataset = failCurrentRemoteDatasetNL(item); + item->ready = false; + item->current = false; + item->stale = true; + item->deliveryPending = false; + item->deliveryDeadline = 0; + if (files.remoteCurrent == item) + files.remoteCurrent = NULL; + acquisition = true; + action = retireRemoteDatasetNL(item); + break; + } + LG_UNLOCK(files.lock); + if (acquisition) + DEBUG_WARN("Remote clipboard file acquisition failed: " + "dataset=%" PRIu64 ", acquisition=%" PRIu64 ", error=%u (%s)", + dataset, requestId, (unsigned)reason, fileErrorName(reason)); + if (acquisition && !request) + { + performRemoteAction(action); + if (failedDataset) + lgClipboard_fileRemoteFailed(failedDataset); + return; + } + if (!request || request->request.dataset != dataset) + { + failRemoteRequest(request, EIO); + performRemoteAction(action); + if (failedDataset) + lgClipboard_fileRemoteFailed(failedDataset); + return; + } + failRemoteRequest(request, errnoFromFileError(reason)); + performRemoteAction(action); + if (failedDataset) + lgClipboard_fileRemoteFailed(failedDataset); +} + +void lgClipboardFiles_localAcquire(uint64_t wireDataset, + uint64_t acquisition) +{ + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_STALE; + struct LocalAcquisition * item = calloc(1, sizeof(*item)); + LG_LOCK(files.lock); + struct LocalDataset * dataset = files.localDatasets; + while (dataset && dataset->dataset != wireDataset) + dataset = dataset->next; + if (dataset && item) + { + item->dataset = dataset; + item->wireDataset = wireDataset; + item->acquisition = acquisition; + item->next = files.localAcquisitions; + files.localAcquisitions = item; + ++dataset->references; + error = LG_CLIPBOARD_FILE_ERROR_NONE; + } + else if (!item) + error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + LG_UNLOCK(files.lock); + if (error) + free(item); + lgClipboard_fileAcquired(wireDataset, acquisition, error); +} + +void lgClipboardFiles_localRelease(uint64_t wireDataset, + uint64_t acquisition) +{ + LG_LOCK(files.lock); + struct LocalAcquisition ** link = &files.localAcquisitions; + while (*link && ((*link)->wireDataset != wireDataset || + (*link)->acquisition != acquisition)) + link = &(*link)->next; + struct LocalAcquisition * item = *link; + struct LocalDataset * retired = NULL; + if (item) + { + *link = item->next; + if (item->dataset->references) + --item->dataset->references; + retired = takeUnusedLocalDatasetNL(item->dataset); + } + LG_UNLOCK(files.lock); + free(item); + freeLocalDataset(retired); +} + +static struct LocalDataset * acquiredLocalDatasetNL(uint64_t wireDataset) +{ + for (struct LocalDataset * dataset = files.localDatasets; + dataset; dataset = dataset->next) + if (dataset->dataset == wireDataset) + return dataset; + return NULL; +} + +static bool sendJobCall(struct LocalJob * job, int operation, + uint64_t offset, const void * data, size_t size) +{ + for (;;) + { + if (atomic_load_explicit(&job->cancelled, memory_order_acquire)) + return false; + LG_ClipboardResult result; + switch (operation) + { + case 0: + result = lgClipboard_fileDataBegin(&job->request, offset); + break; + case 1: + result = lgClipboard_fileDataChunk( + &job->request, offset, data, size); + break; + default: + result = lgClipboard_fileDataEnd(&job->request, offset); + break; + } + if (result == LG_CLIPBOARD_RESULT_ACCEPTED) + return true; + if (result == LG_CLIPBOARD_RESULT_FAILED) + return false; + if (!lgWaitEvent(job->ready, TIMEOUT_INFINITE)) + return false; + } +} + +static bool sendBufferJob(struct LocalJob * job, + const uint8_t * data, size_t size) +{ + if (!sendJobCall(job, 0, size, NULL, 0)) + return false; + size_t offset = 0; + while (offset < size) + { + size_t chunk = size - offset; + if (chunk > FILE_STREAM_CHUNK) + chunk = FILE_STREAM_CHUNK; + if (!sendJobCall(job, 1, offset, data + offset, chunk)) + return false; + offset += chunk; + } + return sendJobCall(job, 2, size, NULL, 0); +} + +static LG_ClipboardFileError appendLocalEntry(uint8_t ** data, size_t * size, + size_t * capacity, const struct LocalNode * node) +{ + const size_t nameLength = strlen(node->name); + if (!nameLength || nameLength > NAME_MAX || + !validUtf8((const uint8_t *)node->name, nameLength)) + return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + const uint64_t bytes64 = KVMFR_CLIPBOARD_FILE_ENTRY_BYTES(nameLength); + if (bytes64 > SIZE_MAX || *size > SIZE_MAX - (size_t)bytes64) + return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + const size_t wanted = *size + (size_t)bytes64; + if (!wanted || !grow((void **)data, capacity, wanted) || !*data) + return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + KVMFRClipboardFileEntry entry = + { + .node = node->node, + .size = node->size, + .createdNs = node->createdNs, + .modifiedNs = node->modifiedNs, + .type = node->type, + .nameLength = (uint32_t)nameLength, + }; + memcpy(*data + *size, &entry, sizeof(entry)); + memcpy(*data + *size + sizeof(entry), node->name, nameLength); + memset(*data + *size + sizeof(entry) + nameLength, 0, + (size_t)bytes64 - sizeof(entry) - nameLength); + *size += (size_t)bytes64; + return LG_CLIPBOARD_FILE_ERROR_NONE; +} + +static bool localJobCancelled(const struct LocalJob * job) +{ + return atomic_load_explicit(&job->cancelled, memory_order_acquire); +} + +static LG_ClipboardFileError localList(struct LocalJob * job) +{ + uint8_t * data = NULL; + size_t size = 0; + size_t capacity = 0; + struct LocalNode ** listed = NULL; + size_t listedCount = 0; + size_t listedCapacity = 0; + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_IO; + LG_LOCK(files.lock); + struct LocalNode * directory = job->request.node ? + localNodeByIdNL(job->dataset, job->request.node) : NULL; + if (job->request.node && !directory) + { + LG_UNLOCK(files.lock); + return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND; + } + if (directory && + directory->type != KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY) + { + LG_UNLOCK(files.lock); + return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY; + } + if (!job->request.node) + { + struct LocalNode ** roots = NULL; + size_t rootCount = 0; + size_t rootCapacity = 0; + for (struct LocalNode * node = job->dataset->nodes; + node; node = node->next) + if (!node->parent) + { + if (rootCount == SIZE_MAX / sizeof(*roots)) + { + LG_UNLOCK(files.lock); + free(roots); + return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + } + if (!grow((void **)&roots, &rootCapacity, + (rootCount + 1U) * sizeof(*roots))) + { + LG_UNLOCK(files.lock); + free(roots); + return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + } + roots[rootCount++] = node; + } + LG_UNLOCK(files.lock); + + for (size_t i = 0; i < rootCount; ++i) + { + if (localJobCancelled(job)) + { + free(roots); + free(data); + return LG_CLIPBOARD_FILE_ERROR_CANCELLED; + } + struct stat value; + const int fd = openLocalNode(roots[i], &value); + if (fd < 0) + { + error = fileErrorFromErrno(errno); + free(roots); + free(data); + return error; + } + close(fd); + error = appendLocalEntry(&data, &size, &capacity, roots[i]); + if (error) + { + free(roots); + free(data); + return error; + } + } + for (size_t i = 0; i < rootCount; ++i) + { + if (localJobCancelled(job)) + { + free(roots); + free(data); + return LG_CLIPBOARD_FILE_ERROR_CANCELLED; + } + struct stat value; + const int fd = openLocalNode(roots[i], &value); + if (fd < 0) + { + error = fileErrorFromErrno(errno); + free(roots); + free(data); + return error; + } + close(fd); + } + free(roots); + const bool result = sendBufferJob(job, data, size); + free(data); + return result ? LG_CLIPBOARD_FILE_ERROR_NONE : + localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED : + LG_CLIPBOARD_FILE_ERROR_DISCONNECTED; + } + LG_UNLOCK(files.lock); + + if (localJobCancelled(job)) + return LG_CLIPBOARD_FILE_ERROR_CANCELLED; + struct stat directoryBefore; + const int directoryFd = openLocalNode(directory, &directoryBefore); + if (directoryFd < 0) + return fileErrorFromErrno(errno); + DIR * dir = fdopendir(directoryFd); + if (!dir) + { + error = fileErrorFromErrno(errno); + close(directoryFd); + return error; + } + + for (;;) + { + if (localJobCancelled(job)) + { + error = LG_CLIPBOARD_FILE_ERROR_CANCELLED; + goto failed; + } + errno = 0; + struct dirent * entry = readdir(dir); + if (!entry) + { + if (errno) + { + error = fileErrorFromErrno(errno); + goto failed; + } + break; + } + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) + continue; + + struct stat before; + if (fstatat(directoryFd, entry->d_name, &before, + AT_SYMLINK_NOFOLLOW) < 0) + { + error = fileErrorFromErrno(errno); + goto failed; + } + if (S_ISLNK(before.st_mode)) + { + error = LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + goto failed; + } + if (!localStatSupported(&before)) + { + error = LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED; + goto failed; + } + + int flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK; + if (S_ISDIR(before.st_mode)) + flags |= O_DIRECTORY; + const int childFd = openat(directoryFd, entry->d_name, flags); + if (childFd < 0) + { + error = fileErrorFromErrno(errno); + goto failed; + } + struct stat value; + if (fstat(childFd, &value) < 0) + { + error = fileErrorFromErrno(errno); + close(childFd); + goto failed; + } + const bool stable = localStatEqual(&before, &value); + close(childFd); + if (!stable) + { + error = LG_CLIPBOARD_FILE_ERROR_STALE; + goto failed; + } + + const size_t parentLength = strlen(directory->path); + const size_t nameLength = strlen(entry->d_name); + if (parentLength > SIZE_MAX - nameLength - 2U) + { + error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + goto failed; + } + const size_t pathLength = parentLength + (parentLength ? 1U : 0U) + + nameLength + 1U; + char * childPath = malloc(pathLength); + if (!childPath) + { + error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + goto failed; + } + if (parentLength) + snprintf(childPath, pathLength, "%s/%s", + directory->path, entry->d_name); + else + memcpy(childPath, entry->d_name, nameLength + 1U); + + LG_LOCK(files.lock); + struct LocalNode * node = localChildNL( + job->dataset, directory->node, entry->d_name); + if (!node) + node = addLocalNode(job->dataset, directory->node, + directory->root, childPath, entry->d_name, &value); + else + free(childPath); + if (!node) + error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + else if (!localNodeMatches(node, &value)) + error = LG_CLIPBOARD_FILE_ERROR_STALE; + else + error = appendLocalEntry(&data, &size, &capacity, node); + LG_UNLOCK(files.lock); + if (error) + goto failed; + if (listedCount == SIZE_MAX / sizeof(*listed) || + !grow((void **)&listed, &listedCapacity, + (listedCount + 1U) * sizeof(*listed))) + { + error = LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + goto failed; + } + listed[listedCount++] = node; + } + + for (size_t i = 0; i < listedCount; ++i) + { + if (localJobCancelled(job)) + { + error = LG_CLIPBOARD_FILE_ERROR_CANCELLED; + goto failed; + } + struct stat value; + const int fd = openLocalNode(listed[i], &value); + if (fd < 0) + { + error = fileErrorFromErrno(errno); + goto failed; + } + close(fd); + } + struct stat directoryAfter; + if (localJobCancelled(job)) + { + error = LG_CLIPBOARD_FILE_ERROR_CANCELLED; + goto failed; + } + if (fstat(directoryFd, &directoryAfter) < 0) + { + error = fileErrorFromErrno(errno); + goto failed; + } + if (!localNodeMatches(directory, &directoryAfter)) + { + error = LG_CLIPBOARD_FILE_ERROR_STALE; + goto failed; + } + if (closedir(dir) < 0) + { + const int closeError = errno; + free(listed); + free(data); + return fileErrorFromErrno(closeError); + } + free(listed); + const bool result = sendBufferJob(job, data, size); + free(data); + return result ? LG_CLIPBOARD_FILE_ERROR_NONE : + localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED : + LG_CLIPBOARD_FILE_ERROR_DISCONNECTED; + +failed: + closedir(dir); + free(listed); + free(data); + return error; +} + +static LG_ClipboardFileError localRead(struct LocalJob * job) +{ + LG_LOCK(files.lock); + struct LocalNode * node = localNodeByIdNL( + job->dataset, job->request.node); + LG_UNLOCK(files.lock); + if (!node) + return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND; + if (node->type != KVMFR_CLIPBOARD_FILE_TYPE_REGULAR) + return LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY; + if (job->request.offset > node->size || + job->request.length > node->size - job->request.offset) + return LG_CLIPBOARD_FILE_ERROR_INVALID; + if (localJobCancelled(job)) + return LG_CLIPBOARD_FILE_ERROR_CANCELLED; + + struct stat before; + const int fd = openLocalNode(node, &before); + if (fd < 0) + return fileErrorFromErrno(errno); + uint8_t * buffer = malloc(job->request.length ? job->request.length : 1U); + if (!buffer) + { + close(fd); + return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY; + } + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_NONE; + size_t received = 0; + while (received < job->request.length) + { + if (localJobCancelled(job)) + { + error = LG_CLIPBOARD_FILE_ERROR_CANCELLED; + goto failed; + } +#ifdef ENABLE_TESTS + const bool forceEof = atomic_exchange_explicit( + &forceLocalEof, false, memory_order_acq_rel); + const ssize_t bytes = forceEof ? 0 : + pread(fd, buffer + received, job->request.length - received, + (off_t)(job->request.offset + received)); +#else + const ssize_t bytes = pread(fd, buffer + received, + job->request.length - received, + (off_t)(job->request.offset + received)); +#endif + if (bytes < 0 && errno == EINTR) + continue; + if (bytes < 0) + { + error = fileErrorFromErrno(errno); + goto failed; + } + if (!bytes) + { + error = LG_CLIPBOARD_FILE_ERROR_IO; + goto failed; + } + received += (size_t)bytes; + } + + struct stat after; + if (localJobCancelled(job)) + { + error = LG_CLIPBOARD_FILE_ERROR_CANCELLED; + goto failed; + } + if (fstat(fd, &after) < 0) + { + error = fileErrorFromErrno(errno); + goto failed; + } + if (!localNodeMatches(node, &after) || + !localStatEqual(&before, &after)) + { + error = LG_CLIPBOARD_FILE_ERROR_STALE; + goto failed; + } + if (received != job->request.length) + { + error = LG_CLIPBOARD_FILE_ERROR_IO; + goto failed; + } + close(fd); + const bool result = sendBufferJob(job, buffer, received); + free(buffer); + return result ? LG_CLIPBOARD_FILE_ERROR_NONE : + localJobCancelled(job) ? LG_CLIPBOARD_FILE_ERROR_CANCELLED : + LG_CLIPBOARD_FILE_ERROR_DISCONNECTED; + +failed: + close(fd); + free(buffer); + return error; +} + +static int localWorker(void * opaque) +{ + (void)opaque; + for (;;) + { + LG_LOCK(files.lock); + struct LocalJob * job = files.jobsHead; + if (job) + { + files.jobsHead = job->next; + if (!files.jobsHead) + files.jobsTail = &files.jobsHead; + job->next = files.activeJobs; + files.activeJobs = job; + } + const bool stopping = files.stopping; + if (!job && !stopping) + lgResetEvent(files.jobEvent); + LG_UNLOCK(files.lock); + if (!job) + { + if (stopping) + return 0; + lgWaitEvent(files.jobEvent, TIMEOUT_INFINITE); + continue; + } + LG_ClipboardFileError error = LG_CLIPBOARD_FILE_ERROR_CANCELLED; + if (!atomic_load_explicit(&job->cancelled, memory_order_acquire)) + error = job->request.operation == LG_CLIPBOARD_FILE_LIST ? + localList(job) : localRead(job); + if (error && error != LG_CLIPBOARD_FILE_ERROR_CANCELLED && + !atomic_load_explicit(&job->cancelled, memory_order_acquire)) + lgClipboard_fileCancel(job->request.dataset, + job->request.request, error); + LG_LOCK(files.lock); + struct LocalJob ** link = &files.activeJobs; + while (*link && *link != job) + link = &(*link)->next; + if (*link) + *link = job->next; + if (job->dataset->references) + --job->dataset->references; + struct LocalDataset * retired = + takeUnusedLocalDatasetNL(job->dataset); + LG_UNLOCK(files.lock); + freeLocalDataset(retired); + lgFreeEvent(job->ready); + free(job); + } +} + +void lgClipboardFiles_localRequest( + const LG_ClipboardFileRequest * request) +{ + if (!request || !request->request || + (request->operation != LG_CLIPBOARD_FILE_LIST && + request->operation != LG_CLIPBOARD_FILE_READ) || + (request->operation == LG_CLIPBOARD_FILE_LIST && + (request->offset || request->length)) || + (request->operation == LG_CLIPBOARD_FILE_READ && + (!request->length || request->length > FILE_REQUEST_MAX || + request->offset > INT64_MAX || + request->length > (uint64_t)INT64_MAX - request->offset))) + { + if (request) + lgClipboard_fileCancel(request->dataset, request->request, + LG_CLIPBOARD_FILE_ERROR_INVALID); + return; + } + struct LocalJob * job = calloc(1, sizeof(*job)); + if (!job) + { + lgClipboard_fileCancel(request->dataset, request->request, + LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return; + } + job->ready = lgCreateEvent(true, 0); + if (!job->ready) + { + free(job); + lgClipboard_fileCancel(request->dataset, request->request, + LG_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return; + } + atomic_init(&job->cancelled, false); + job->request = *request; + LG_LOCK(files.lock); + job->dataset = acquiredLocalDatasetNL(request->dataset); + if (job->dataset && !files.stopping) + { + ++job->dataset->references; + *files.jobsTail = job; + files.jobsTail = &job->next; + } + const bool queued = job->dataset && !files.stopping; + LG_UNLOCK(files.lock); + if (queued) + { + lgSignalEvent(files.jobEvent); + return; + } + lgFreeEvent(job->ready); + free(job); + lgClipboard_fileCancel(request->dataset, request->request, + LG_CLIPBOARD_FILE_ERROR_STALE); +} + +void lgClipboardFiles_localCancel(uint64_t dataset, + uint64_t request, LG_ClipboardFileError reason) +{ + (void)reason; + LG_LOCK(files.lock); + struct LocalAcquisition ** acquisition = &files.localAcquisitions; + struct LocalDataset * retired = NULL; + while (*acquisition && ((*acquisition)->wireDataset != dataset || + (*acquisition)->acquisition != request)) + acquisition = &(*acquisition)->next; + if (*acquisition) + { + struct LocalAcquisition * cancelled = *acquisition; + *acquisition = cancelled->next; + if (cancelled->dataset->references) + --cancelled->dataset->references; + retired = takeUnusedLocalDatasetNL(cancelled->dataset); + free(cancelled); + } + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + if (job->request.dataset == dataset && job->request.request == request) + { + atomic_store_explicit(&job->cancelled, true, memory_order_release); + lgSignalEvent(job->ready); + } + for (struct LocalJob * job = files.jobsHead; job; job = job->next) + if (job->request.dataset == dataset && job->request.request == request) + atomic_store_explicit(&job->cancelled, true, memory_order_release); + LG_UNLOCK(files.lock); + freeLocalDataset(retired); +} + +void lgClipboardFiles_localReady(uint64_t request) +{ + LG_LOCK(files.lock); + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + if (job->request.request == request) + lgSignalEvent(job->ready); + LG_UNLOCK(files.lock); +} + +static void freeState(void) +{ + struct RemoteRequest * request = files.remoteRequests; + while (request) + { + struct RemoteRequest * next = request->next; + unregisterRemoteInterrupt(request); + if (request->fuseRequest) + fuse_reply_err(request->fuseRequest, EIO); + freeRemoteRequest(request); + request = next; + } + struct RemoteNode * remoteNode = files.remoteNodes; + while (remoteNode) + { + struct RemoteNode * next = remoteNode->next; + free(remoteNode->name); + free(remoteNode); + remoteNode = next; + } + struct RemoteDataset * remote = files.remoteDatasets; + while (remote) + { + struct RemoteDataset * next = remote->next; + if (remote->acquired) + lgClipboard_fileRelease(remote->dataset, remote->acquisition); + else if (remote->acquiring) + lgClipboard_fileCancel(remote->dataset, remote->acquisition, + LG_CLIPBOARD_FILE_ERROR_CANCELLED); + free(remote); + remote = next; + } + struct LocalAcquisition * acquisition = files.localAcquisitions; + while (acquisition) + { + struct LocalAcquisition * next = acquisition->next; + free(acquisition); + acquisition = next; + } + struct LocalDataset * local = files.localDatasets; + while (local) + { + struct LocalDataset * next = local->next; + freeLocalDataset(local); + local = next; + } +} + +bool lgClipboardFiles_init(void) +{ + if (filesLockInitialized) + return false; + memset(&files, 0, sizeof(files)); + files.fuseStopFd = -1; + LG_LOCK_INIT(files.lock); + filesLockInitialized = true; + uint64_t nonce = 0; + if (!randomBytes(&nonce, sizeof(nonce))) + { + DEBUG_ERROR("Failed to seed clipboard file transfer identifiers: %s", + strerror(errno)); + goto failed; + } + nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!nonce) + nonce = 1; + files.requestSerial = nonce; + files.datasetSerial = (nonce ^ UINT64_C(0x4c474346494c4553)) & + ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!files.datasetSerial) + files.datasetSerial = 1; + files.inodeSerial = FUSE_ROOT_ID; + files.jobsTail = &files.jobsHead; + files.jobEvent = lgCreateEvent(false, 0); + if (!files.jobEvent) + goto failed; + files.fuseStopFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (files.fuseStopFd < 0) + { + DEBUG_ERROR("Failed to create the clipboard FUSE stop event: %s", + strerror(errno)); + goto failed; + } + const char * runtime = getenv("XDG_RUNTIME_DIR"); + if (!runtime || !*runtime) + { + DEBUG_ERROR("XDG_RUNTIME_DIR is required for clipboard file transfer"); + goto failed; + } + const int length = snprintf(files.mountpoint, sizeof(files.mountpoint), + "%s/looking-glass-clipboard-XXXXXX", runtime); + if (length < 0 || (size_t)length >= sizeof(files.mountpoint) || + !mkdtemp(files.mountpoint)) + { + DEBUG_ERROR("Failed to create the clipboard FUSE mountpoint: %s", + strerror(errno)); + goto failed; + } + chmod(files.mountpoint, 0700); + struct fuse_args args = FUSE_ARGS_INIT(0, NULL); + if (fuse_opt_add_arg(&args, "looking-glass-client") < 0 || + fuse_opt_add_arg(&args, "-o") < 0 || + fuse_opt_add_arg(&args, + "ro,nodev,nosuid,noexec,default_permissions,auto_unmount") < 0) + { + fuse_opt_free_args(&args); + goto failed; + } + files.session = fuse_session_new(&args, &fuseOps, + sizeof(fuseOps), NULL); + fuse_opt_free_args(&args); + if (!files.session || fuse_session_mount( + files.session, files.mountpoint) < 0) + { + DEBUG_ERROR("Failed to mount the clipboard FUSE filesystem"); + goto failed; + } + if (!lgCreateThread("clipboard-fuse", fuseThread, + files.session, &files.fuseThread)) + goto failed; + unsigned created = 0; + for (; created < FILE_WORKERS; ++created) + if (!lgCreateThread("clipboard-file", localWorker, + NULL, &files.workers[created])) + break; + if (created != FILE_WORKERS) + goto failed; + files.initialized = true; + DEBUG_INFO("Clipboard files mounted at %s", files.mountpoint); + return true; + +failed: + lgClipboardFiles_free(); + return false; +} + +#ifdef ENABLE_TESTS +bool lgClipboardFiles_testFuseStopWake(void) +{ + const int stopFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (stopFd < 0) + return false; + const bool result = wakeFuseThread(stopFd) && + waitFuseEvent(-1, stopFd, -1) == FUSE_WAIT_WAKE; + close(stopFd); + return result; +} + +bool lgClipboardFiles_testUnsentRemoteOwnership(void) +{ + return finishUnsentRemoteRequest(0); +} + +bool lgClipboardFiles_testInit(uint64_t nonce) +{ + if (filesLockInitialized) + return false; + nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!nonce) + return false; + memset(&files, 0, sizeof(files)); + files.fuseStopFd = -1; + LG_LOCK_INIT(files.lock); + filesLockInitialized = true; + files.requestSerial = nonce; + files.datasetSerial = (nonce ^ UINT64_C(0x4c474346494c4553)) & + ~KVMFR_CLIPBOARD_TRANSFER_HELPER; + if (!files.datasetSerial) + files.datasetSerial = 1; + files.inodeSerial = FUSE_ROOT_ID; + files.jobsTail = &files.jobsHead; + files.initialized = true; + atomic_store_explicit(&forceLocalEof, false, memory_order_release); + return true; +} + +size_t lgClipboardFiles_testRemoteDatasetCount(void) +{ + if (!filesLockInitialized) + return 0; + size_t count = 0; + LG_LOCK(files.lock); + for (struct RemoteDataset * dataset = files.remoteDatasets; + dataset; dataset = dataset->next) + ++count; + LG_UNLOCK(files.lock); + return count; +} + +void lgClipboardFiles_testExpireRemoteDeliveries(void) +{ + if (!filesLockInitialized) + return; + LG_LOCK(files.lock); + for (struct RemoteDataset * dataset = files.remoteDatasets; + dataset; dataset = dataset->next) + if (dataset->deliveryPending) + dataset->deliveryDeadline = 0; + LG_UNLOCK(files.lock); + reapRemoteDeliveries(false); +} + +bool lgClipboardFiles_testBeginRemoteLookup(uint64_t presentation) +{ + LG_LOCK(files.lock); + struct RemoteDataset * dataset = + remoteDatasetByAcquisitionNL(presentation); + const bool result = dataset && dataset->ready && + pinRemoteOperationNL(dataset, true); + LG_UNLOCK(files.lock); + return result; +} + +void lgClipboardFiles_testEndRemoteLookup(uint64_t presentation) +{ + LG_LOCK(files.lock); + struct RemoteDataset * dataset = + remoteDatasetByAcquisitionNL(presentation); + if (dataset && dataset->operationPins) + --dataset->operationPins; + const struct RemoteAction action = retireRemoteDatasetNL(dataset); + LG_UNLOCK(files.lock); + performRemoteAction(action); +} + +bool lgClipboardFiles_testRemoteRead(uint64_t presentation, + uint64_t nodeId, uint64_t offset, uint32_t length) +{ + LG_LOCK(files.lock); + struct RemoteDataset * dataset = + remoteDatasetByAcquisitionNL(presentation); + struct RemoteNode * node = dataset ? + remoteNodeByWireNL(dataset, nodeId) : NULL; + const bool pinned = node && length && + pinRemoteOperationNL(dataset, false); + LG_UNLOCK(files.lock); + if (!pinned) + return false; + const bool result = requestRemote(dataset, node, + LG_CLIPBOARD_FILE_READ, offset, length, REMOTE_REPLY_READ, + NULL, 0, 0, NULL); + releaseRemoteOperation(dataset); + return result; +} + +void lgClipboardFiles_testForceLocalEof(void) +{ + atomic_store_explicit(&forceLocalEof, true, memory_order_release); +} + +bool lgClipboardFiles_testLocalRequest( + const LG_ClipboardFileRequest * request) +{ + if (!request || + (request->operation != LG_CLIPBOARD_FILE_LIST && + request->operation != LG_CLIPBOARD_FILE_READ)) + return false; + + struct LocalJob job = { .request = *request }; + atomic_init(&job.cancelled, false); + LG_LOCK(files.lock); + job.dataset = acquiredLocalDatasetNL(request->dataset); + if (job.dataset) + ++job.dataset->references; + LG_UNLOCK(files.lock); + if (!job.dataset) + return false; + + const LG_ClipboardFileError error = + request->operation == LG_CLIPBOARD_FILE_LIST ? + localList(&job) : localRead(&job); + if (error && error != LG_CLIPBOARD_FILE_ERROR_CANCELLED) + lgClipboard_fileCancel(request->dataset, request->request, error); + LG_LOCK(files.lock); + if (job.dataset->references) + --job.dataset->references; + LG_UNLOCK(files.lock); + return error == LG_CLIPBOARD_FILE_ERROR_NONE; +} +#endif + +void lgClipboardFiles_free(void) +{ + if (!filesLockInitialized) + return; + LG_LOCK(files.lock); + files.stopping = true; + for (struct LocalJob * job = files.jobsHead; job; job = job->next) + atomic_store_explicit(&job->cancelled, true, memory_order_release); + for (struct LocalJob * job = files.activeJobs; job; job = job->next) + { + atomic_store_explicit(&job->cancelled, true, memory_order_release); + if (job->ready) + lgSignalEvent(job->ready); + } + LG_UNLOCK(files.lock); + if (files.jobEvent) + lgSignalEvent(files.jobEvent); + for (unsigned i = 0; i < FILE_WORKERS; ++i) + if (files.workers[i]) + { + lgSignalEvent(files.jobEvent); + lgJoinThread(files.workers[i], NULL); + files.workers[i] = NULL; + } + if (files.session) + fuse_session_exit(files.session); + if (files.fuseThread) + { + if (!wakeFuseThread(files.fuseStopFd)) + DEBUG_WARN("Failed to wake the clipboard FUSE thread: %s", + strerror(errno)); + lgJoinThread(files.fuseThread, NULL); + files.fuseThread = NULL; + } + if (files.fuseStopFd >= 0) + { + close(files.fuseStopFd); + files.fuseStopFd = -1; + } + /* Pending requests belong to the stopped but still-live session. */ + freeState(); + if (files.session) + { + fuse_session_unmount(files.session); + fuse_session_destroy(files.session); + files.session = NULL; + } + if (files.jobEvent) + { + lgFreeEvent(files.jobEvent); + files.jobEvent = NULL; + } + if (files.mountpoint[0] && rmdir(files.mountpoint) < 0 && errno != ENOENT) + DEBUG_WARN("Failed to remove clipboard mountpoint: %s", strerror(errno)); + files.mountpoint[0] = 0; + files.initialized = false; + LG_LOCK_FREE(files.lock); + filesLockInitialized = false; +} diff --git a/client/src/clipboard_files.h b/client/src/clipboard_files.h new file mode 100644 index 00000000..87415972 --- /dev/null +++ b/client/src/clipboard_files.h @@ -0,0 +1,86 @@ +/** + * 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 + */ + +#ifndef _H_LG_CLIENT_CLIPBOARD_FILES_ +#define _H_LG_CLIENT_CLIPBOARD_FILES_ + +#include "interface/clipboard.h" + +bool lgClipboardFiles_init(void); +void lgClipboardFiles_free(void); + +/* Desktop backends install a local file selection after reading one of the + * standard URI clipboard representations. The payload is copied. */ +bool lgClipboardFiles_setLocal(const char * mime, + const void * data, size_t size); +void lgClipboardFiles_clearLocal(void); + +/* Returns an allocated desktop representation for the current guest offer. */ +bool lgClipboardFiles_getRemote(const char * mime, + char ** data, size_t * size); +uint64_t lgClipboardFiles_remotePresentationAcquire(void); +bool lgClipboardFiles_getRemotePresentation(uint64_t presentation, + const char * mime, char ** data, size_t * size); +void lgClipboardFiles_remotePresentationDelivered(uint64_t presentation); +void lgClipboardFiles_remotePresentationRelease(uint64_t presentation); +bool lgClipboardFiles_remoteReady(uint64_t dataset); + +/* Events delivered by the active clipboard transport. */ +bool lgClipboardFiles_remoteOffer(uint64_t dataset); +void lgClipboardFiles_remoteClear(void); +void lgClipboardFiles_providerUnavailable(void); +void lgClipboardFiles_remoteAcquired(uint64_t dataset, + uint64_t acquisition, LG_ClipboardFileError error); +LG_ClipboardResult lgClipboardFiles_remoteDataBegin( + const LG_ClipboardFileRequest * request, uint64_t sizeHint); +LG_ClipboardResult lgClipboardFiles_remoteDataChunk( + const LG_ClipboardFileRequest * request, uint64_t responseOffset, + const void * data, size_t size); +LG_ClipboardResult lgClipboardFiles_remoteDataEnd( + const LG_ClipboardFileRequest * request, uint64_t finalSize); +void lgClipboardFiles_remoteCancel(uint64_t dataset, + uint64_t request, LG_ClipboardFileError reason); + +void lgClipboardFiles_localAcquire(uint64_t dataset, + uint64_t acquisition); +void lgClipboardFiles_localRelease(uint64_t dataset, + uint64_t acquisition); +void lgClipboardFiles_localRequest( + const LG_ClipboardFileRequest * request); +void lgClipboardFiles_localCancel(uint64_t dataset, + uint64_t request, LG_ClipboardFileError reason); +void lgClipboardFiles_localReady(uint64_t request); + +#ifdef ENABLE_TESTS +bool lgClipboardFiles_testInit(uint64_t nonce); +bool lgClipboardFiles_testFuseStopWake(void); +bool lgClipboardFiles_testUnsentRemoteOwnership(void); +size_t lgClipboardFiles_testRemoteDatasetCount(void); +void lgClipboardFiles_testExpireRemoteDeliveries(void); +bool lgClipboardFiles_testBeginRemoteLookup(uint64_t presentation); +void lgClipboardFiles_testEndRemoteLookup(uint64_t presentation); +bool lgClipboardFiles_testRemoteRead(uint64_t presentation, + uint64_t node, uint64_t offset, uint32_t length); +void lgClipboardFiles_testForceLocalEof(void); +bool lgClipboardFiles_testLocalRequest( + const LG_ClipboardFileRequest * request); +#endif + +#endif diff --git a/client/src/main.c b/client/src/main.c index e742fcfd..3c716a32 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -3322,7 +3322,8 @@ static int lg_run(void) frameTimingInit(); lgInput_init(); lgAudio_init(); - lgClipboard_init(); + if (!lgClipboard_init()) + return -1; #ifdef ENABLE_TESTS memset(&l_testCapture, 0, sizeof(l_testCapture)); @@ -4018,8 +4019,6 @@ static void lg_shutdown(void) e_startup = NULL; } - lgClipboard_free(); - app_releaseAllKeybinds(); ll_free(g_state.bindings); @@ -4029,6 +4028,10 @@ static void lg_shutdown(void) g_state.ds->free(); } + /* Clipboard sources own FUSE dataset references and are released by the + * display server before the filesystem is unmounted. */ + lgClipboard_free(); + // Input callbacks have stopped; the evdev state and overlays can go evdev_free(); diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 2099a6ad..81e208cb 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -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 ) diff --git a/client/tests/clipboard_files_stub.c b/client/tests/clipboard_files_stub.c new file mode 100644 index 00000000..3dba5b80 --- /dev/null +++ b/client/tests/clipboard_files_stub.c @@ -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; } diff --git a/client/tests/clipboard_files_test.c b/client/tests/clipboard_files_test.c new file mode 100644 index 00000000..9d161bbe --- /dev/null +++ b/client/tests/clipboard_files_test.c @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/client/tests/clipboard_test.c b/client/tests/clipboard_test.c index 1c5c5268..fb81c8a4 100644 --- a/client/tests/clipboard_test.c +++ b/client/tests/clipboard_test.c @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -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 }, diff --git a/client/tests/spice_clipboard_test.c b/client/tests/spice_clipboard_test.c index d93a3a6d..db270796 100644 --- a/client/tests/spice_clipboard_test.c +++ b/client/tests/spice_clipboard_test.c @@ -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; diff --git a/client/tests/wayland_clipboard_test.c b/client/tests/wayland_clipboard_test.c index 1b01f9da..3c41bd87 100644 --- a/client/tests/wayland_clipboard_test.c +++ b/client/tests/wayland_clipboard_test.c @@ -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 }, diff --git a/client/tests/x11_clipboard_test.c b/client/tests/x11_clipboard_test.c index 52f9a043..82fc6138 100644 --- a/client/tests/x11_clipboard_test.c +++ b/client/tests/x11_clipboard_test.c @@ -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) diff --git a/client/transports/SPICE/clipboard.c b/client/transports/SPICE/clipboard.c index e2ccb519..5d697ec1 100644 --- a/client/transports/SPICE/clipboard.c +++ b/client/transports/SPICE/clipboard.c @@ -103,6 +103,7 @@ static bool lgType(LG_ClipboardData source, PSDataType * type) case LG_CLIPBOARD_DATA_BMP : *type = SPICE_DATA_BMP ; return true; case LG_CLIPBOARD_DATA_TIFF : *type = SPICE_DATA_TIFF; return true; case LG_CLIPBOARD_DATA_JPEG : *type = SPICE_DATA_JPEG; return true; + case LG_CLIPBOARD_DATA_FILES: return false; case LG_CLIPBOARD_DATA_NONE : *type = SPICE_DATA_NONE; return true; }