/** * 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 "wayland.h" #include #include #include #include #include #include #include #include "app.h" #include "common/debug.h" struct DataOffer { bool isSelfCopy; char * mimetypes[LG_CLIPBOARD_DATA_NONE]; }; static const char * textMimetypes[] = { "text/plain", "text/plain;charset=utf-8", "TEXT", "STRING", "UTF8_STRING", NULL, }; static const char * pngMimetypes[] = { "image/png", NULL, }; static const char * bmpMimetypes[] = { "image/bmp", "image/x-bmp", "image/x-MS-bmp", "image/x-win-bitmap", NULL, }; static const char * tiffMimetypes[] = { "image/tiff", NULL, }; static const char * jpegMimetypes[] = { "image/jpeg", NULL, }; static const char ** cbTypeToMimetypes(enum LG_ClipboardData type) { switch (type) { case LG_CLIPBOARD_DATA_TEXT: return textMimetypes; case LG_CLIPBOARD_DATA_PNG: return pngMimetypes; case LG_CLIPBOARD_DATA_BMP: return bmpMimetypes; case LG_CLIPBOARD_DATA_TIFF: return tiffMimetypes; case LG_CLIPBOARD_DATA_JPEG: return jpegMimetypes; default: DEBUG_ERROR("invalid clipboard type"); abort(); } } static bool containsMimetype(const char ** mimetypes, const char * needle) { for (const char ** mimetype = mimetypes; *mimetype; mimetype++) if (!strcmp(needle, *mimetype)) return true; return false; } static bool mimetypeEndswith(const char * mimetype, const char * what) { size_t mimetypeLen = strlen(mimetype); size_t whatLen = strlen(what); if (mimetypeLen < whatLen) return false; return !strcmp(mimetype + mimetypeLen - whatLen, what); } static bool isTextMimetype(const char * mimetype) { if (containsMimetype(textMimetypes, mimetype)) return true; if (!strcmp(mimetype, "text/ico")) return false; char * text = "text/"; if (!strncmp(mimetype, text, strlen(text))) return true; if (mimetypeEndswith(mimetype, "script") || mimetypeEndswith(mimetype, "xml") || mimetypeEndswith(mimetype, "yaml")) return true; if (strstr(mimetype, "json")) return true; return false; } static enum LG_ClipboardData mimetypeToCbType(const char * mimetype) { if (isTextMimetype(mimetype)) return LG_CLIPBOARD_DATA_TEXT; if (containsMimetype(pngMimetypes, mimetype)) return LG_CLIPBOARD_DATA_PNG; if (containsMimetype(bmpMimetypes, mimetype)) return LG_CLIPBOARD_DATA_BMP; if (containsMimetype(tiffMimetypes, mimetype)) return LG_CLIPBOARD_DATA_TIFF; if (containsMimetype(jpegMimetypes, mimetype)) return LG_CLIPBOARD_DATA_JPEG; return LG_CLIPBOARD_DATA_NONE; } static bool isImageCbtype(enum LG_ClipboardData type) { switch (type) { case LG_CLIPBOARD_DATA_TEXT: return false; case LG_CLIPBOARD_DATA_PNG: case LG_CLIPBOARD_DATA_BMP: case LG_CLIPBOARD_DATA_TIFF: case LG_CLIPBOARD_DATA_JPEG: return true; default: DEBUG_ERROR("invalid clipboard type"); abort(); } } static bool hasAnyMimetype(char ** mimetypes) { for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) if (mimetypes[i]) return true; return false; } static bool hasImageMimetype(char ** mimetypes) { for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) if (isImageCbtype(i) && mimetypes[i]) return true; return false; } /* wlCb.lock must be held. */ static struct ClipboardRead * clipboardReadTakeCurrentNL(void) { struct ClipboardRead * data = wlCb.currentRead; if (!data) return NULL; wlCb.currentRead = NULL; return data; } static void clipboardReadRetire(struct ClipboardRead * data) { if (data) waylandPollUnregister(data->fd); } static bool clipboardReadClaim(struct ClipboardRead * data) { LG_LOCK(wlCb.lock); const bool current = wlCb.currentRead == data; if (current) clipboardReadTakeCurrentNL(); LG_UNLOCK(wlCb.lock); if (current) clipboardReadRetire(data); return current; } static void clipboardReadCleanup(void * opaque) { struct ClipboardRead * data = opaque; close(data->fd); free(data->buf); free(data); } // Destination client handlers. static void dataOfferHandleOffer(void * opaque, struct wl_data_offer * offer, const char * mimetype) { struct DataOffer * data = opaque; if (!strcmp(mimetype, wlCb.lgMimetype)) { data->isSelfCopy = true; return; } enum LG_ClipboardData type = mimetypeToCbType(mimetype); if (type == LG_CLIPBOARD_DATA_NONE) return; // text/html represents rich text format, which is almost never desirable when // and should not be used when a plain text or image format is available. if ((isImageCbtype(type) || containsMimetype(textMimetypes, mimetype)) && data->mimetypes[LG_CLIPBOARD_DATA_TEXT] && strstr(data->mimetypes[LG_CLIPBOARD_DATA_TEXT], "html")) { free(data->mimetypes[LG_CLIPBOARD_DATA_TEXT]); data->mimetypes[LG_CLIPBOARD_DATA_TEXT] = NULL; } if (strstr(mimetype, "html") && hasImageMimetype(data->mimetypes)) return; if (data->mimetypes[type]) return; data->mimetypes[type] = strdup(mimetype); } static void dataOfferHandleSourceActions(void * data, struct wl_data_offer * offer, uint32_t sourceActions) { // Do nothing. } static void dataOfferHandleAction(void * data, struct wl_data_offer * offer, uint32_t dndAction) { // Do nothing. } static const struct wl_data_offer_listener dataOfferListener = { .offer = dataOfferHandleOffer, .source_actions = dataOfferHandleSourceActions, .action = dataOfferHandleAction, }; static void dataDeviceHandleDataOffer(void * data, struct wl_data_device * dataDevice, struct wl_data_offer * offer) { struct DataOffer * extra = calloc(1, sizeof(struct DataOffer)); if (!extra) { DEBUG_ERROR("Out of memory while handling clipboard"); abort(); } wl_data_offer_set_user_data(offer, extra); wl_data_offer_add_listener(offer, &dataOfferListener, extra); } static void dataDeviceHandleSelection(void * opaque, struct wl_data_device * dataDevice, struct wl_data_offer * offer) { if (!offer) { waylandCBInvalidate(); return; } struct DataOffer * extra = wl_data_offer_get_user_data(offer); if (!hasAnyMimetype(extra->mimetypes) || extra->isSelfCopy) { waylandCBInvalidate(); wl_data_offer_destroy(offer); return; } int idx = 0; enum LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) if (extra->mimetypes[i]) types[idx++] = i; char * oldMimetypes[LG_CLIPBOARD_DATA_NONE]; LG_LOCK(wlCb.lock); struct ClipboardRead * read = clipboardReadTakeCurrentNL(); struct wl_data_offer * oldOffer = wlCb.offer; memcpy(oldMimetypes, wlCb.mimetypes, sizeof(oldMimetypes)); wlCb.offer = offer; memcpy(wlCb.mimetypes, extra->mimetypes, sizeof(wlCb.mimetypes)); LG_UNLOCK(wlCb.lock); wl_data_offer_set_user_data(offer, NULL); memset(extra->mimetypes, 0, sizeof(extra->mimetypes)); free(extra); if (read) { const LG_ClipboardRequest request = read->request; clipboardReadRetire(read); app_clipboardAbort(request); } if (oldOffer && oldOffer != offer) wl_data_offer_destroy(oldOffer); for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) free(oldMimetypes[i]); app_clipboardNotifyTypes(types, idx); } static void dataDeviceHandleEnter(void * data, struct wl_data_device * device, uint32_t serial, struct wl_surface * surface, wl_fixed_t sxW, wl_fixed_t syW, struct wl_data_offer * offer) { DEBUG_ASSERT(wlCb.dndOffer == NULL); wlCb.dndOffer = offer; struct DataOffer * extra = wl_data_offer_get_user_data(offer); for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) free(extra->mimetypes[i]); free(extra); wl_data_offer_set_user_data(offer, NULL); wl_data_offer_set_actions(offer, WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE, WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE); } static void dataDeviceHandleMotion(void * data, struct wl_data_device * device, uint32_t time, wl_fixed_t sxW, wl_fixed_t syW) { // Do nothing. } static void dataDeviceHandleLeave(void * data, struct wl_data_device * device) { wl_data_offer_destroy(wlCb.dndOffer); wlCb.dndOffer = NULL; } static void dataDeviceHandleDrop(void * data, struct wl_data_device * device) { wl_data_offer_destroy(wlCb.dndOffer); wlCb.dndOffer = NULL; } static const struct wl_data_device_listener dataDeviceListener = { .data_offer = dataDeviceHandleDataOffer, .selection = dataDeviceHandleSelection, .enter = dataDeviceHandleEnter, .motion = dataDeviceHandleMotion, .leave = dataDeviceHandleLeave, .drop = dataDeviceHandleDrop, }; bool waylandCBInit(void) { if (!wlWm.seat) { DEBUG_WARN("Clipboard unavailable without wl_seat"); return false; } if (!wlWm.dataDeviceManager) { DEBUG_ERROR("Missing wl_data_device_manager interface (version 3+)"); return false; } wlCb.dataDevice = wl_data_device_manager_get_data_device( wlWm.dataDeviceManager, wlWm.seat); if (!wlCb.dataDevice) { DEBUG_ERROR("Failed to get data device"); return false; } wl_data_device_add_listener(wlCb.dataDevice, &dataDeviceListener, NULL); snprintf(wlCb.lgMimetype, sizeof(wlCb.lgMimetype), "application/x-looking-glass-copy;pid=%d", getpid()); return true; } static void clipboardReadCallback(uint32_t events, void * opaque) { struct ClipboardRead * data = opaque; LG_LOCK(wlCb.lock); const bool active = wlCb.currentRead == data; LG_UNLOCK(wlCb.lock); if (!active) return; if (events & EPOLLERR) { if (clipboardReadClaim(data)) app_clipboardAbort(data->request); return; } ssize_t result = read(data->fd, data->buf + data->numRead, data->size - data->numRead); if (result < 0) { DEBUG_ERROR("Failed to read from clipboard: %s", strerror(errno)); if (clipboardReadClaim(data)) app_clipboardAbort(data->request); return; } if (result == 0) { if (clipboardReadClaim(data)) app_clipboardData( data->request, data->type, data->buf, data->numRead); return; } data->numRead += result; if (data->numRead >= data->size) { data->size *= 2; void * nbuf = realloc(data->buf, data->size); if (!nbuf) { DEBUG_ERROR("Failed to realloc clipboard buffer: %s", strerror(errno)); if (clipboardReadClaim(data)) app_clipboardAbort(data->request); return; } data->buf = nbuf; } } void waylandCBInvalidate(void) { char * mimetypes[LG_CLIPBOARD_DATA_NONE]; LG_LOCK(wlCb.lock); struct ClipboardRead * read = clipboardReadTakeCurrentNL(); struct wl_data_offer * offer = wlCb.offer; wlCb.offer = NULL; memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes)); memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes)); LG_UNLOCK(wlCb.lock); if (read) { const LG_ClipboardRequest request = read->request; clipboardReadRetire(read); app_clipboardAbort(request); } app_clipboardRelease(); if (offer) wl_data_offer_destroy(offer); for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) free(mimetypes[i]); } void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) { if (request == LG_CLIPBOARD_REQUEST_INVALID || type < LG_CLIPBOARD_DATA_TEXT || type >= LG_CLIPBOARD_DATA_NONE) return; int fds[2]; if (pipe(fds) < 0) { DEBUG_ERROR("Failed to get a clipboard pipe: %s", strerror(errno)); app_clipboardAbort(request); return; } struct ClipboardRead * data = malloc(sizeof(*data)); if (!data) { DEBUG_ERROR("Failed to allocate memory to read clipboard"); close(fds[0]); close(fds[1]); app_clipboardAbort(request); return; } data->fd = fds[0]; data->size = 4096; data->numRead = 0; data->buf = malloc(data->size); data->request = request; data->type = type; if (!data->buf) { DEBUG_ERROR("Failed to allocate memory to receive clipboard data"); close(data->fd); close(fds[1]); free(data); app_clipboardAbort(request); return; } LG_LOCK(wlCb.lock); struct ClipboardRead * old = clipboardReadTakeCurrentNL(); const bool available = wlCb.offer && wlCb.mimetypes[type]; if (available) { wl_data_offer_receive( wlCb.offer, wlCb.mimetypes[type], fds[1]); } close(fds[1]); const bool registered = available && waylandPollRegisterWithCleanup(data->fd, clipboardReadCallback, data, clipboardReadCleanup, EPOLLIN); if (registered) wlCb.currentRead = data; LG_UNLOCK(wlCb.lock); if (old) { const LG_ClipboardRequest oldRequest = old->request; clipboardReadRetire(old); app_clipboardAbort(oldRequest); } if (!registered) { if (available) DEBUG_ERROR("Failed to register clipboard read into epoll: %s", strerror(errno)); clipboardReadCleanup(data); app_clipboardAbort(request); return; } } struct ClipboardWrite { int fd; size_t pos; struct CountedBuffer * buffer; }; static void clipboardWriteCleanup(void * opaque) { struct ClipboardWrite * data = opaque; close(data->fd); countedBufferRelease(&data->buffer); free(data); } static void clipboardWriteCallback(uint32_t events, void * opaque) { struct ClipboardWrite * data = opaque; if (events & EPOLLERR) goto error; ssize_t written = write(data->fd, data->buffer->data + data->pos, data->buffer->size - data->pos); if (written < 0) { if (errno != EPIPE) DEBUG_ERROR("Failed to write clipboard data: %s", strerror(errno)); goto error; } data->pos += written; if (data->pos < data->buffer->size) return; error: waylandPollUnregister(data->fd); } static void dataSourceHandleTarget(void * data, struct wl_data_source * source, const char * mimetype) { // Certain Wayland clients send this for copy-paste operations even though // it only makes sense for drag-and-drop. We just do nothing. } static void dataSourceHandleSend(void * data, struct wl_data_source * source, const char * mimetype, int fd) { struct WCBTransfer * transfer = (struct WCBTransfer *) data; if (containsMimetype(transfer->mimetypes, mimetype)) { struct ClipboardWrite * data = malloc(sizeof(*data)); if (!data) { DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite"); goto error; } data->fd = fd; data->pos = 0; data->buffer = transfer->data; countedBufferAddRef(transfer->data); if (waylandPollRegisterWithCleanup(fd, clipboardWriteCallback, data, clipboardWriteCleanup, EPOLLOUT)) return; countedBufferRelease(&data->buffer); free(data); } error: close(fd); } static void dataSourceHandleCancelled(void * data, struct wl_data_source * source) { struct WCBTransfer * transfer = (struct WCBTransfer *) data; countedBufferRelease(&transfer->data); free(transfer); wl_data_source_destroy(source); } static const struct wl_data_source_listener dataSourceListener = { .target = dataSourceHandleTarget, .send = dataSourceHandleSend, .cancelled = dataSourceHandleCancelled, }; static void waylandCBReplyFn(void * opaque, LG_ClipboardData type, const uint8_t * data, uint32_t size) { if (type == LG_CLIPBOARD_DATA_NONE) return; struct WCBTransfer * transfer = malloc(sizeof(*transfer)); if (!transfer) { DEBUG_ERROR("Out of memory when allocating WCBTransfer"); return; } transfer->mimetypes = cbTypeToMimetypes(type); transfer->data = countedBufferNew(size); if (!transfer->data) { DEBUG_ERROR("Out of memory when allocating clipboard buffer"); free(transfer); return; } if (size) memcpy(transfer->data->data, data, size); struct wl_data_source * source = wl_data_device_manager_create_data_source(wlWm.dataDeviceManager); wl_data_source_add_listener(source, &dataSourceListener, transfer); for (const char ** mimetype = transfer->mimetypes; *mimetype; mimetype++) wl_data_source_offer(source, *mimetype); wl_data_source_offer(source, wlCb.lgMimetype); wl_data_device_set_selection(wlCb.dataDevice, source, wlWm.keyboardEnterSerial); } void waylandCBNotice(LG_ClipboardData type) { if (!app_clipboardRequest(type, waylandCBReplyFn, NULL)) DEBUG_ERROR("Failed to request remote clipboard data"); } void waylandCBRelease(void) { } void waylandCBFree(void) { char * mimetypes[LG_CLIPBOARD_DATA_NONE]; LG_LOCK(wlCb.lock); struct ClipboardRead * read = clipboardReadTakeCurrentNL(); struct wl_data_offer * offer = wlCb.offer; struct wl_data_offer * dndOffer = wlCb.dndOffer; struct wl_data_device * dataDevice = wlCb.dataDevice; wlCb.offer = NULL; wlCb.dndOffer = NULL; wlCb.dataDevice = NULL; memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes)); memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes)); LG_UNLOCK(wlCb.lock); clipboardReadRetire(read); if (offer) wl_data_offer_destroy(offer); if (dndOffer && dndOffer != offer) wl_data_offer_destroy(dndOffer); if (dataDevice) wl_data_device_release(dataDevice); for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) free(mimetypes[i]); LG_LOCK_FREE(wlCb.lock); }