mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 07:01:30 +00:00
Explorer file copies were invisible to the Helper because the service launched its child with a duplicate of the LocalSystem token and changed only TokenSessionId. The child therefore remained a System-integrity process. Windows filtered Explorer's file clipboard formats across that integrity boundary. Basic text and bitmap formats continued to work. Keep only the SCM service privileged. Obtain the active session user's primary token with WTSQueryUserToken. For elevated accounts, prefer the linked limited token. Validate its session and security properties. Build the user environment and launch the interactive Helper on WinSta0\Default. Gate Helper activation until the service has rechecked the active session and registered the clipboard authority. Use random lifetime, stop, and activation objects owned by the service. Give the target logon SID synchronization access only. Recheck the active console session and service state before activation. Make the lifetime mutex terminate the Helper if the service exits unexpectedly. Restart it when the active session, IDD host, or authority changes. Replace the old process-handle mapping transfer with a device-bound authority protocol on the LGIdd device interface. The service verifies the exact driver host instance, duplicates only section map rights into that process, and registers the session, mapping identifier, and handle. Bind authority lifetime to its WDF file object, revoke it synchronously on cleanup, and poll the driver host identity while the child is active. Restrict the device stack to SYSTEM and isolate LGIdd in a unique UMDF device group. Restrict the shared section to SYSTEM and the target logon SID. Apply a medium mandatory label that prevents low-integrity readers and writers. Map it with read/write rights instead of all access. Give each clipboard mapping a second random authority identifier. Store it only inside the logon-SID-protected mapping and send it in the mandatory HELLO. LGIdd matches it against the service-injected mapping. This authenticates the user Helper without the unsupported UMDF call to GetNamedPipeClientSessionId. Have the Helper verify that its pipe server is in session zero. Extend the pipe endpoint with bounded authentication reads, cancellable overlapped I/O, periodic authorization checks, and explicit disconnects. Serialize authority changes with clipboard attach and detach. Prevent stale cleanup from tearing down a replacement mapping. Disconnect the user pipe immediately when its owning authority is revoked. Run clipboard, OLE, display, configuration, and file access in the user's interactive process. Retain its process token for worker-thread file operations instead of querying and impersonating the desktop user from a System process. Store Helper logs in LocalAppData and grant only the registry rights needed by interactive configuration and UMDF. Keep immediate, stage-specific Win32 and HRESULT diagnostics throughout clipboard capture. Probe CF_HDROP while holding the Win32 clipboard and enumerate the OLE object's advertised file formats. Validate returned storage and fall back to Shell item paths when direct retrieval fails. Validate clipboard sequence changes and defer retries during contention without publishing incomplete clipboard state. Complete the 1 MiB transfer work with full-sized Windows copy buffers. Use full-sized FUSE reads and retain the named 64 KiB X11 chunk limit. Validate the user-writable mapping with CClipboardRing before attaching. The pipe, mapping, and authority protocols change together. LGIdd.dll, the INF, and LGIddHelper.exe must be rebuilt and installed as one matching set.
1475 lines
41 KiB
C
1475 lines
41 KiB
C
/**
|
|
* Looking Glass
|
|
* Copyright © 2017-2026 The Looking Glass Authors
|
|
* https://looking-glass.io
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#include "atoms.h"
|
|
#include "clipboard.h"
|
|
#include "test.h"
|
|
#include "x11.h"
|
|
#include "core/clipboard.h"
|
|
#include "core/clipboard_files.h"
|
|
|
|
#include "common/debug.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <pthread.h>
|
|
|
|
#include <X11/Xatom.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/extensions/Xfixes.h>
|
|
|
|
#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0]))
|
|
#define MAX_ABORT 12U
|
|
#define MAX_CHANGE 8U
|
|
#define MAX_CONVERT 16U
|
|
#define MAX_DATA 4U
|
|
#define MAX_NOTICE 8U
|
|
#define MAX_PROP 16U
|
|
#define MAX_SEND 8U
|
|
#define MAX_TRANSFER (128U * 1024U)
|
|
#define MAX_WINDOW 16U
|
|
|
|
#define A_CLIPBOARD 10UL
|
|
#define A_TARGETS 11UL
|
|
#define A_SEL_DATA 12UL
|
|
#define A_INCR 13UL
|
|
#define A_TEXT 100UL
|
|
#define A_PNG 101UL
|
|
#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
|
|
{
|
|
Window window;
|
|
bool dead;
|
|
};
|
|
|
|
struct ConvertLog
|
|
{
|
|
Atom selection;
|
|
Atom target;
|
|
Atom property;
|
|
Window requestor;
|
|
};
|
|
|
|
struct ChangeLog
|
|
{
|
|
Window window;
|
|
Atom property;
|
|
Atom type;
|
|
int format;
|
|
int count;
|
|
unsigned char data[MAX_TRANSFER];
|
|
size_t size;
|
|
};
|
|
|
|
struct Property
|
|
{
|
|
int status;
|
|
Atom type;
|
|
int format;
|
|
unsigned long count;
|
|
unsigned long after;
|
|
unsigned char data[MAX_TRANSFER];
|
|
size_t size;
|
|
bool null;
|
|
};
|
|
|
|
struct TypeNotice
|
|
{
|
|
LG_ClipboardData type[LG_CLIPBOARD_DATA_NONE];
|
|
size_t count;
|
|
};
|
|
|
|
struct DataNotice
|
|
{
|
|
LG_ClipboardRequest request;
|
|
LG_ClipboardData type;
|
|
uint8_t data[MAX_TRANSFER];
|
|
size_t size;
|
|
};
|
|
|
|
struct Log
|
|
{
|
|
struct WindowLog window[MAX_WINDOW];
|
|
struct ConvertLog convert[MAX_CONVERT];
|
|
struct ChangeLog change[MAX_CHANGE];
|
|
struct Property prop[MAX_PROP];
|
|
struct TypeNotice notice[MAX_NOTICE];
|
|
struct DataNotice data[MAX_DATA];
|
|
XEvent send[MAX_SEND];
|
|
LG_ClipboardRequest abort[MAX_ABORT];
|
|
unsigned int windowN;
|
|
unsigned int convertN;
|
|
unsigned int changeN;
|
|
unsigned int propN;
|
|
unsigned int propPos;
|
|
unsigned int noticeN;
|
|
unsigned int dataN;
|
|
unsigned int sendN;
|
|
unsigned int abortN;
|
|
unsigned int releaseN;
|
|
unsigned int requestN;
|
|
unsigned int requestReadyN;
|
|
unsigned int destroyN;
|
|
unsigned int freeN;
|
|
unsigned int flushN;
|
|
unsigned int grabN;
|
|
unsigned int ungrabN;
|
|
unsigned int selectN;
|
|
unsigned int structureSelectN;
|
|
LG_ClipboardData requestType;
|
|
const LG_ClipboardStreamOps * requestStream;
|
|
void * requestOpaque;
|
|
LG_ClipboardRequest requestId;
|
|
LG_ClipboardRequest streamRequest;
|
|
LG_ClipboardData streamType;
|
|
uint64_t streamSizeHint;
|
|
unsigned int streamBeginN;
|
|
unsigned int streamChunkN;
|
|
unsigned int streamEndN;
|
|
size_t streamMaxChunk;
|
|
LG_ClipboardResult beginResult;
|
|
LG_ClipboardResult chunkResult;
|
|
LG_ClipboardResult endResult;
|
|
LG_ClipboardResult requestBeginResult;
|
|
bool readyDuringChunk;
|
|
bool readyDuringEnd;
|
|
Window owner;
|
|
Window nextWindow;
|
|
bool requestOK;
|
|
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;
|
|
struct X11DSAtoms x11atoms;
|
|
|
|
static struct Log rec;
|
|
|
|
static pthread_mutex_t readRaceLock = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_cond_t readRaceCond = PTHREAD_COND_INITIALIZER;
|
|
static bool readRacePause;
|
|
static bool readRaceEntered;
|
|
static bool readRaceResume;
|
|
|
|
static size_t itemSize(int format, unsigned long count)
|
|
{
|
|
switch (format)
|
|
{
|
|
case 8:
|
|
return count;
|
|
case 16:
|
|
return count * sizeof(short);
|
|
case 32:
|
|
return count * sizeof(long);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
Atom XInternAtom(Display * display, const char * name, Bool onlyIfExists)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(!onlyIfExists);
|
|
if (!strcmp(name, "UTF8_STRING"))
|
|
return A_TEXT;
|
|
if (!strcmp(name, "image/png"))
|
|
return A_PNG;
|
|
if (!strcmp(name, "image/bmp"))
|
|
return A_BMP;
|
|
if (!strcmp(name, "image/tiff"))
|
|
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;
|
|
}
|
|
|
|
Bool XFixesQueryExtension(Display * display, int * eventBase, int * errorBase)
|
|
{
|
|
CHECK(display == x11.display);
|
|
if (!rec.fixesOK)
|
|
return False;
|
|
*eventBase = 80;
|
|
*errorBase = 81;
|
|
return True;
|
|
}
|
|
|
|
void XFixesSelectSelectionInput(Display * display, Window window,
|
|
Atom selection, unsigned long mask)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(window == x11.window);
|
|
CHECK(selection == x11atoms.CLIPBOARD);
|
|
CHECK(mask == XFixesSetSelectionOwnerNotifyMask);
|
|
++rec.selectN;
|
|
}
|
|
|
|
Window XCreateSimpleWindow(Display * display, Window parent,
|
|
int x, int y, unsigned int width, unsigned int height,
|
|
unsigned int borderWidth, unsigned long border,
|
|
unsigned long background)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(parent == x11.window);
|
|
CHECK(x == 0);
|
|
CHECK(y == 0);
|
|
CHECK(width == 1);
|
|
CHECK(height == 1);
|
|
CHECK(borderWidth == 0);
|
|
CHECK(border == 0);
|
|
CHECK(background == 0);
|
|
CHECK(rec.windowN < ARRAY_LENGTH(rec.window));
|
|
struct WindowLog * window = &rec.window[rec.windowN++];
|
|
window->window = ++rec.nextWindow;
|
|
return window->window;
|
|
}
|
|
|
|
int XDestroyWindow(Display * display, Window window)
|
|
{
|
|
CHECK(display == x11.display);
|
|
for (unsigned int i = 0; i < rec.windowN; ++i)
|
|
if (rec.window[i].window == window)
|
|
{
|
|
CHECK(!rec.window[i].dead);
|
|
rec.window[i].dead = true;
|
|
++rec.destroyN;
|
|
return Success;
|
|
}
|
|
CHECK(false);
|
|
return BadWindow;
|
|
}
|
|
|
|
int XConvertSelection(Display * display, Atom selection, Atom target,
|
|
Atom property, Window requestor, Time time)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(time == CurrentTime);
|
|
CHECK(rec.convertN < ARRAY_LENGTH(rec.convert));
|
|
struct ConvertLog * convert = &rec.convert[rec.convertN++];
|
|
convert->selection = selection;
|
|
convert->target = target;
|
|
convert->property = property;
|
|
convert->requestor = requestor;
|
|
return Success;
|
|
}
|
|
|
|
int XSelectInput(Display * display, Window window, long mask)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(mask == PropertyChangeMask ||
|
|
mask == (PropertyChangeMask | StructureNotifyMask));
|
|
if (mask & StructureNotifyMask)
|
|
++rec.structureSelectN;
|
|
CHECK(window != None);
|
|
return Success;
|
|
}
|
|
|
|
int XSetSelectionOwner(Display * display, Atom selection,
|
|
Window owner, Time time)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(selection == x11atoms.CLIPBOARD);
|
|
CHECK(time == CurrentTime);
|
|
rec.owner = owner;
|
|
return Success;
|
|
}
|
|
|
|
Window XGetSelectionOwner(Display * display, Atom selection)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(selection == x11atoms.CLIPBOARD);
|
|
return rec.owner;
|
|
}
|
|
|
|
int XGrabServer(Display * display)
|
|
{
|
|
CHECK(display == x11.display);
|
|
++rec.grabN;
|
|
return Success;
|
|
}
|
|
|
|
int XUngrabServer(Display * display)
|
|
{
|
|
CHECK(display == x11.display);
|
|
++rec.ungrabN;
|
|
return Success;
|
|
}
|
|
|
|
int XFlush(Display * display)
|
|
{
|
|
CHECK(display == x11.display);
|
|
++rec.flushN;
|
|
return Success;
|
|
}
|
|
|
|
int XChangeProperty(Display * display, Window window, Atom property,
|
|
Atom type, int format, int mode, const unsigned char * data, int count)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(mode == PropModeReplace);
|
|
CHECK(count >= 0);
|
|
CHECK(rec.changeN < ARRAY_LENGTH(rec.change));
|
|
struct ChangeLog * change = &rec.change[rec.changeN++];
|
|
change->window = window;
|
|
change->property = property;
|
|
change->type = type;
|
|
change->format = format;
|
|
change->count = count;
|
|
change->size = itemSize(format, count);
|
|
CHECK(change->size <= sizeof(change->data));
|
|
CHECK(!change->size || data);
|
|
if (change->size)
|
|
memcpy(change->data, data, change->size);
|
|
return Success;
|
|
}
|
|
|
|
Status XSendEvent(Display * display, Window window, Bool propagate,
|
|
long mask, XEvent * event)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(!propagate);
|
|
CHECK(mask == 0);
|
|
CHECK(rec.sendN < ARRAY_LENGTH(rec.send));
|
|
CHECK(event->xselection.requestor == window);
|
|
rec.send[rec.sendN++] = *event;
|
|
return True;
|
|
}
|
|
|
|
int XGetWindowProperty(Display * display, Window window, Atom property,
|
|
long offset, long length, Bool delete, Atom requestedType,
|
|
Atom * actualType, int * actualFormat, unsigned long * count,
|
|
unsigned long * after, unsigned char ** data)
|
|
{
|
|
CHECK(display == x11.display);
|
|
CHECK(window != None);
|
|
CHECK(property != None);
|
|
CHECK(offset >= 0);
|
|
CHECK(length == ~0L || length ==
|
|
(KVMFR_CLIPBOARD_REPRESENTATION_BYTES + 3U) / 4U);
|
|
CHECK(delete);
|
|
CHECK(requestedType == AnyPropertyType);
|
|
CHECK(rec.propPos < rec.propN);
|
|
const struct Property * prop = &rec.prop[rec.propPos];
|
|
*actualType = prop->type;
|
|
*actualFormat = prop->format;
|
|
size_t start = 0;
|
|
size_t size = prop->size;
|
|
if (prop->format == 8 && length != ~0L)
|
|
{
|
|
start = (size_t)offset * 4U;
|
|
CHECK(start <= prop->size);
|
|
const size_t maximum = (size_t)length * 4U;
|
|
size = prop->size - start;
|
|
if (size > maximum)
|
|
size = maximum;
|
|
*count = size;
|
|
*after = prop->size - start - size;
|
|
}
|
|
else
|
|
{
|
|
*count = prop->count;
|
|
*after = prop->after;
|
|
}
|
|
if (prop->null)
|
|
*data = NULL;
|
|
else
|
|
{
|
|
CHECK(start + size <= sizeof(prop->data));
|
|
*data = malloc(size ? size : 1);
|
|
CHECK(*data);
|
|
if (size)
|
|
memcpy(*data, prop->data + start, size);
|
|
}
|
|
if (*after == 0)
|
|
++rec.propPos;
|
|
return prop->status;
|
|
}
|
|
|
|
int XFree(void * data)
|
|
{
|
|
CHECK(data);
|
|
++rec.freeN;
|
|
free(data);
|
|
return Success;
|
|
}
|
|
|
|
void lgClipboard_notifyTypes(
|
|
const LG_ClipboardData types[], size_t count)
|
|
{
|
|
CHECK(rec.noticeN < ARRAY_LENGTH(rec.notice));
|
|
CHECK(count <= ARRAY_LENGTH(rec.notice[0].type));
|
|
struct TypeNotice * notice = &rec.notice[rec.noticeN++];
|
|
notice->count = count;
|
|
memcpy(notice->type, types, count * sizeof(*types));
|
|
}
|
|
|
|
void lgClipboard_data(LG_ClipboardRequest request,
|
|
LG_ClipboardData type, const void * data, size_t size)
|
|
{
|
|
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
|
|
CHECK(size <= MAX_TRANSFER);
|
|
CHECK(!size || data);
|
|
struct DataNotice * notice = &rec.data[rec.dataN++];
|
|
notice->request = request;
|
|
notice->type = type;
|
|
notice->size = size;
|
|
if (size)
|
|
memcpy(notice->data, data, size);
|
|
}
|
|
|
|
LG_ClipboardResult lgClipboard_dataBegin(LG_ClipboardRequest request,
|
|
LG_ClipboardData type, uint64_t sizeHint)
|
|
{
|
|
++rec.streamBeginN;
|
|
rec.streamRequest = request;
|
|
rec.streamType = type;
|
|
rec.streamSizeHint = sizeHint;
|
|
if (rec.beginResult == LG_CLIPBOARD_RESULT_ACCEPTED)
|
|
{
|
|
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
|
|
rec.data[rec.dataN] = (struct DataNotice)
|
|
{
|
|
.request = request,
|
|
.type = type,
|
|
};
|
|
}
|
|
return rec.beginResult;
|
|
}
|
|
|
|
LG_ClipboardResult lgClipboard_dataChunk(LG_ClipboardRequest request,
|
|
uint64_t offset, const void * data, size_t size)
|
|
{
|
|
const LG_ClipboardResult result = rec.chunkResult;
|
|
++rec.streamChunkN;
|
|
CHECK(request == rec.streamRequest);
|
|
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
|
|
struct DataNotice * notice = &rec.data[rec.dataN];
|
|
CHECK(offset == notice->size);
|
|
CHECK(data);
|
|
CHECK(size);
|
|
CHECK(size <= sizeof(notice->data) - notice->size);
|
|
if (size > rec.streamMaxChunk)
|
|
rec.streamMaxChunk = size;
|
|
pthread_mutex_lock(&readRaceLock);
|
|
if (readRacePause)
|
|
{
|
|
readRacePause = false;
|
|
readRaceEntered = true;
|
|
pthread_cond_signal(&readRaceCond);
|
|
while (!readRaceResume)
|
|
pthread_cond_wait(&readRaceCond, &readRaceLock);
|
|
}
|
|
pthread_mutex_unlock(&readRaceLock);
|
|
if (rec.readyDuringChunk)
|
|
{
|
|
rec.readyDuringChunk = false;
|
|
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
|
|
x11CBRequestReady(request);
|
|
}
|
|
if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
|
|
{
|
|
memcpy(notice->data + notice->size, data, size);
|
|
notice->size += size;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest request,
|
|
uint64_t finalSize)
|
|
{
|
|
const LG_ClipboardResult result = rec.endResult;
|
|
++rec.streamEndN;
|
|
CHECK(request == rec.streamRequest);
|
|
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
|
|
if (rec.readyDuringEnd)
|
|
{
|
|
rec.readyDuringEnd = false;
|
|
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
|
|
x11CBRequestReady(request);
|
|
}
|
|
if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
|
|
{
|
|
CHECK(finalSize == rec.data[rec.dataN].size);
|
|
++rec.dataN;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void lgClipboard_abort(LG_ClipboardRequest request)
|
|
{
|
|
CHECK(rec.abortN < ARRAY_LENGTH(rec.abort));
|
|
rec.abort[rec.abortN++] = request;
|
|
}
|
|
|
|
void lgClipboard_release(void)
|
|
{
|
|
++rec.releaseN;
|
|
}
|
|
|
|
bool lgClipboard_requestStream(LG_ClipboardData type,
|
|
const LG_ClipboardStreamOps * stream, void * opaque,
|
|
LG_ClipboardRequest * request)
|
|
{
|
|
++rec.requestN;
|
|
rec.requestType = type;
|
|
rec.requestStream = stream;
|
|
rec.requestOpaque = opaque;
|
|
rec.requestId = 9000 + rec.requestN;
|
|
if (request)
|
|
*request = rec.requestId;
|
|
if (rec.requestBeginSync)
|
|
rec.requestBeginResult = stream->begin(
|
|
opaque, type, LG_CLIPBOARD_SIZE_UNKNOWN);
|
|
if (rec.requestCancelSync)
|
|
stream->cancel(opaque, LG_CLIPBOARD_CANCEL_REPLACED);
|
|
return rec.requestOK;
|
|
}
|
|
|
|
bool lgClipboard_requestReady(LG_ClipboardRequest request)
|
|
{
|
|
CHECK(request == rec.requestId);
|
|
++rec.requestReadyN;
|
|
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));
|
|
struct Property * prop = &rec.prop[rec.propN++];
|
|
prop->status = Success;
|
|
return prop;
|
|
}
|
|
|
|
static void prop8(Atom type, const void * data, size_t size)
|
|
{
|
|
struct Property * value = prop();
|
|
CHECK(size <= sizeof(value->data));
|
|
value->type = type;
|
|
value->format = 8;
|
|
value->count = size;
|
|
value->size = size;
|
|
if (size)
|
|
memcpy(value->data, data, size);
|
|
}
|
|
|
|
static void prop32(Atom type, const unsigned long * data, size_t count)
|
|
{
|
|
struct Property * value = prop();
|
|
const size_t size = count * sizeof(*data);
|
|
CHECK(size <= sizeof(value->data));
|
|
value->type = type;
|
|
value->format = 32;
|
|
value->count = count;
|
|
value->size = size;
|
|
if (size)
|
|
memcpy(value->data, data, size);
|
|
}
|
|
|
|
static void propNull(Atom type, int format, unsigned long count)
|
|
{
|
|
struct Property * value = prop();
|
|
value->type = type;
|
|
value->format = format;
|
|
value->count = count;
|
|
value->null = true;
|
|
}
|
|
|
|
static void selection(Window requestor, Atom target, Atom property)
|
|
{
|
|
XEvent event = {};
|
|
event.xselection.type = SelectionNotify;
|
|
event.xselection.display = x11.display;
|
|
event.xselection.requestor = requestor;
|
|
event.xselection.selection = x11atoms.CLIPBOARD;
|
|
event.xselection.target = target;
|
|
event.xselection.property = property;
|
|
CHECK(x11CBEventThread(&event));
|
|
}
|
|
|
|
static void propertyState(Window window, Atom atom, int state)
|
|
{
|
|
XEvent event = {};
|
|
event.xproperty.type = PropertyNotify;
|
|
event.xproperty.display = x11.display;
|
|
event.xproperty.window = window;
|
|
event.xproperty.atom = atom;
|
|
event.xproperty.state = state;
|
|
CHECK(x11CBEventThread(&event));
|
|
}
|
|
|
|
static void property(Window window)
|
|
{
|
|
propertyState(window, x11atoms.SEL_DATA, PropertyNewValue);
|
|
}
|
|
|
|
static void owner(Window owner)
|
|
{
|
|
rec.owner = owner;
|
|
XFixesSelectionNotifyEvent event =
|
|
{
|
|
.type = x11.eventBase + XFixesSelectionNotify,
|
|
.display = x11.display,
|
|
.window = x11.window,
|
|
.owner = owner,
|
|
.selection = x11atoms.CLIPBOARD,
|
|
};
|
|
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)
|
|
{
|
|
const unsigned int no = rec.convertN;
|
|
owner(ownerWindow);
|
|
CHECK(rec.convertN == no + 1);
|
|
const struct ConvertLog * convert = &rec.convert[no];
|
|
CHECK(convert->selection == x11atoms.CLIPBOARD);
|
|
CHECK(convert->target == x11atoms.TARGETS);
|
|
CHECK(convert->property == x11atoms.TARGETS);
|
|
prop32(XA_ATOM, targets, count);
|
|
selection(convert->requestor, x11atoms.TARGETS, x11atoms.TARGETS);
|
|
}
|
|
|
|
static Window request(LG_ClipboardRequest id, LG_ClipboardData type)
|
|
{
|
|
const unsigned int no = rec.convertN;
|
|
x11CBRequest(id, type);
|
|
CHECK(rec.convertN == no + 1);
|
|
const struct ConvertLog * convert = &rec.convert[no];
|
|
CHECK(convert->selection == x11atoms.CLIPBOARD);
|
|
CHECK(convert->property == x11atoms.SEL_DATA);
|
|
return convert->requestor;
|
|
}
|
|
|
|
static void selectionRequest(Atom target, Atom property)
|
|
{
|
|
XEvent event = {};
|
|
event.xselectionrequest.type = SelectionRequest;
|
|
event.xselectionrequest.display = x11.display;
|
|
event.xselectionrequest.owner = x11.window;
|
|
event.xselectionrequest.requestor = 900;
|
|
event.xselectionrequest.selection = x11atoms.CLIPBOARD;
|
|
event.xselectionrequest.target = target;
|
|
event.xselectionrequest.property = property;
|
|
CHECK(x11CBEventThread(&event));
|
|
}
|
|
|
|
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.fileSetOK = true;
|
|
rec.presentationOK = true;
|
|
|
|
pthread_mutex_lock(&readRaceLock);
|
|
readRacePause = false;
|
|
readRaceEntered = false;
|
|
readRaceResume = false;
|
|
pthread_mutex_unlock(&readRaceLock);
|
|
|
|
x11.display = (Display *)(uintptr_t)1;
|
|
x11.window = 50;
|
|
|
|
x11atoms.CLIPBOARD = A_CLIPBOARD;
|
|
x11atoms.TARGETS = A_TARGETS;
|
|
x11atoms.SEL_DATA = A_SEL_DATA;
|
|
x11atoms.INCR = A_INCR;
|
|
|
|
CHECK(x11CBInit());
|
|
CHECK(x11.eventBase == 80);
|
|
CHECK(x11.errorBase == 81);
|
|
CHECK(rec.selectN == 1);
|
|
}
|
|
|
|
static void finish(void)
|
|
{
|
|
x11CBRelease();
|
|
owner(None);
|
|
CHECK(rec.owner == None);
|
|
CHECK(rec.grabN == rec.ungrabN);
|
|
for (unsigned int i = 0; i < rec.windowN; ++i)
|
|
CHECK(rec.window[i].dead);
|
|
}
|
|
|
|
static void testTargets(void)
|
|
{
|
|
start();
|
|
const unsigned long targets[] = { A_PNG, 999, A_TEXT, A_PNG };
|
|
discover(60, targets, ARRAY_LENGTH(targets));
|
|
CHECK(rec.noticeN == 1);
|
|
CHECK(rec.notice[0].count == 2);
|
|
CHECK(rec.notice[0].type[0] == LG_CLIPBOARD_DATA_TEXT);
|
|
CHECK(rec.notice[0].type[1] == LG_CLIPBOARD_DATA_PNG);
|
|
CHECK(rec.destroyN == 1);
|
|
CHECK(rec.freeN == 1);
|
|
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();
|
|
const unsigned long targets[] = { A_TEXT };
|
|
discover(61, targets, ARRAY_LENGTH(targets));
|
|
const Window window = request(101, LG_CLIPBOARD_DATA_TEXT);
|
|
const uint8_t data[] = "normal data";
|
|
prop8(A_TEXT, data, sizeof(data) - 1);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
|
|
CHECK(rec.dataN == 1);
|
|
CHECK(rec.data[0].request == 101);
|
|
CHECK(rec.data[0].type == LG_CLIPBOARD_DATA_TEXT);
|
|
CHECK(rec.data[0].size == sizeof(data) - 1);
|
|
CHECK(memcmp(rec.data[0].data, data, sizeof(data) - 1) == 0);
|
|
CHECK(rec.abortN == 0);
|
|
CHECK(rec.destroyN == 2);
|
|
CHECK(rec.freeN == 2);
|
|
finish();
|
|
}
|
|
|
|
static void testIncr(void)
|
|
{
|
|
start();
|
|
const unsigned long targets[] = { A_TEXT };
|
|
discover(62, targets, ARRAY_LENGTH(targets));
|
|
const Window window = request(201, LG_CLIPBOARD_DATA_TEXT);
|
|
const unsigned long capacity = 2;
|
|
prop32(x11atoms.INCR, &capacity, 1);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
CHECK(rec.dataN == 0);
|
|
CHECK(rec.abortN == 0);
|
|
|
|
const uint8_t first[] = "abc";
|
|
const uint8_t second[] = "defg";
|
|
prop8(A_TEXT, first, sizeof(first) - 1);
|
|
property(window);
|
|
prop8(A_TEXT, second, sizeof(second) - 1);
|
|
property(window);
|
|
propNull(A_TEXT, 8, 0);
|
|
property(window);
|
|
|
|
CHECK(rec.dataN == 1);
|
|
CHECK(rec.data[0].request == 201);
|
|
CHECK(rec.data[0].type == LG_CLIPBOARD_DATA_TEXT);
|
|
CHECK(rec.data[0].size == 7);
|
|
CHECK(memcmp(rec.data[0].data, "abcdefg", 7) == 0);
|
|
CHECK(rec.abortN == 0);
|
|
CHECK(rec.destroyN == 2);
|
|
CHECK(rec.freeN == 4);
|
|
finish();
|
|
}
|
|
|
|
static void testIncrBlocked(void)
|
|
{
|
|
start();
|
|
const unsigned long targets[] = { A_TEXT };
|
|
discover(620, targets, ARRAY_LENGTH(targets));
|
|
const Window window = request(211, LG_CLIPBOARD_DATA_TEXT);
|
|
const unsigned long capacity = 9;
|
|
rec.beginResult = LG_CLIPBOARD_RESULT_BLOCKED;
|
|
prop32(x11atoms.INCR, &capacity, 1);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
CHECK(rec.streamBeginN == 1);
|
|
CHECK(rec.dataN == 0);
|
|
|
|
const uint8_t first[] = "abc";
|
|
prop8(A_TEXT, first, sizeof(first) - 1);
|
|
property(window);
|
|
CHECK(rec.streamChunkN == 0);
|
|
|
|
rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED;
|
|
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
|
|
x11CBRequestReady(211);
|
|
CHECK(rec.streamBeginN == 2);
|
|
CHECK(rec.streamSizeHint == capacity);
|
|
CHECK(rec.streamChunkN == 1);
|
|
CHECK(rec.data[0].size == 0);
|
|
|
|
const uint8_t second[] = "defghi";
|
|
prop8(A_TEXT, second, sizeof(second) - 1);
|
|
property(window);
|
|
CHECK(rec.streamChunkN == 1);
|
|
|
|
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
|
|
x11CBRequestReady(211);
|
|
CHECK(rec.streamChunkN == 3);
|
|
CHECK(rec.data[0].size == 9);
|
|
CHECK(memcmp(rec.data[0].data, "abcdefghi", 9) == 0);
|
|
|
|
rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED;
|
|
propNull(A_TEXT, 8, 0);
|
|
property(window);
|
|
CHECK(rec.streamEndN == 1);
|
|
CHECK(rec.dataN == 0);
|
|
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
|
|
x11CBRequestReady(211);
|
|
CHECK(rec.streamEndN == 2);
|
|
CHECK(rec.dataN == 1);
|
|
CHECK(rec.abortN == 0);
|
|
finish();
|
|
}
|
|
|
|
static void testIncrReadyRace(void)
|
|
{
|
|
start();
|
|
const unsigned long targets[] = { A_TEXT };
|
|
discover(622, targets, ARRAY_LENGTH(targets));
|
|
const Window window = request(213, LG_CLIPBOARD_DATA_TEXT);
|
|
const unsigned long capacity = 4;
|
|
prop32(x11atoms.INCR, &capacity, 1);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
|
|
const uint8_t data[] = "race";
|
|
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
|
|
rec.readyDuringChunk = true;
|
|
prop8(A_TEXT, data, sizeof(data) - 1);
|
|
property(window);
|
|
CHECK(rec.streamChunkN == 2);
|
|
CHECK(rec.data[0].size == sizeof(data) - 1);
|
|
|
|
rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED;
|
|
rec.readyDuringEnd = true;
|
|
propNull(A_TEXT, 8, 0);
|
|
property(window);
|
|
CHECK(rec.streamEndN == 2);
|
|
CHECK(rec.dataN == 1);
|
|
CHECK(rec.abortN == 0);
|
|
finish();
|
|
}
|
|
|
|
static void * requestReadyThread(void * opaque)
|
|
{
|
|
x11CBRequestReady(*(const LG_ClipboardRequest *)opaque);
|
|
return NULL;
|
|
}
|
|
|
|
static void testIncrCancelRace(void)
|
|
{
|
|
start();
|
|
const unsigned long targets[] = { A_TEXT };
|
|
discover(623, targets, ARRAY_LENGTH(targets));
|
|
const LG_ClipboardRequest id = 214;
|
|
const Window window = request(id, LG_CLIPBOARD_DATA_TEXT);
|
|
const unsigned long capacity = 6;
|
|
prop32(x11atoms.INCR, &capacity, 1);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
|
|
const uint8_t data[] = "cancel";
|
|
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
|
|
prop8(A_TEXT, data, sizeof(data) - 1);
|
|
property(window);
|
|
CHECK(rec.streamChunkN == 1);
|
|
|
|
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
|
|
pthread_mutex_lock(&readRaceLock);
|
|
readRacePause = true;
|
|
pthread_mutex_unlock(&readRaceLock);
|
|
|
|
pthread_t readyThread;
|
|
CHECK(pthread_create(&readyThread, NULL, requestReadyThread,
|
|
(void *)&id) == 0);
|
|
|
|
pthread_mutex_lock(&readRaceLock);
|
|
while (!readRaceEntered)
|
|
pthread_cond_wait(&readRaceCond, &readRaceLock);
|
|
pthread_mutex_unlock(&readRaceLock);
|
|
|
|
/* The callback is outside x11cb.lock and still consuming its chunk. */
|
|
x11CBRequestCancel(id, LG_CLIPBOARD_CANCEL_REPLACED);
|
|
|
|
pthread_mutex_lock(&readRaceLock);
|
|
readRaceResume = true;
|
|
pthread_cond_signal(&readRaceCond);
|
|
pthread_mutex_unlock(&readRaceLock);
|
|
|
|
CHECK(pthread_join(readyThread, NULL) == 0);
|
|
CHECK(rec.streamChunkN == 2);
|
|
CHECK(rec.data[0].size == sizeof(data) - 1);
|
|
CHECK(memcmp(rec.data[0].data, data, sizeof(data) - 1) == 0);
|
|
CHECK(rec.abortN == 0);
|
|
finish();
|
|
}
|
|
|
|
static void testIncrLargeProperty(void)
|
|
{
|
|
start();
|
|
const unsigned long targets[] = { A_TEXT };
|
|
discover(621, targets, ARRAY_LENGTH(targets));
|
|
const Window window = request(212, LG_CLIPBOARD_DATA_TEXT);
|
|
const unsigned long capacity = 70U * 1024U;
|
|
prop32(x11atoms.INCR, &capacity, 1);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
|
|
const size_t size = 70U * 1024U;
|
|
uint8_t * data = malloc(size);
|
|
CHECK(data);
|
|
for (size_t i = 0; i < size; ++i)
|
|
data[i] = (uint8_t)i;
|
|
prop8(A_TEXT, data, size);
|
|
property(window);
|
|
propNull(A_TEXT, 8, 0);
|
|
property(window);
|
|
|
|
CHECK(rec.streamChunkN == 2);
|
|
CHECK(rec.streamMaxChunk == KVMFR_CLIPBOARD_REPRESENTATION_BYTES);
|
|
CHECK(rec.dataN == 1);
|
|
CHECK(rec.data[0].size == size);
|
|
CHECK(memcmp(rec.data[0].data, data, size) == 0);
|
|
free(data);
|
|
finish();
|
|
}
|
|
|
|
static void testMalformed(void)
|
|
{
|
|
start();
|
|
const unsigned long targets[] = { A_TEXT };
|
|
discover(63, targets, ARRAY_LENGTH(targets));
|
|
|
|
Window window = request(301, LG_CLIPBOARD_DATA_TEXT);
|
|
const unsigned long bad = 1;
|
|
prop32(A_TEXT, &bad, 1);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
CHECK(rec.abortN == 1);
|
|
CHECK(rec.abort[0] == 301);
|
|
|
|
window = request(302, LG_CLIPBOARD_DATA_TEXT);
|
|
const unsigned long capacity = 8;
|
|
prop32(x11atoms.INCR, &capacity, 1);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
prop32(A_TEXT, &bad, 1);
|
|
property(window);
|
|
CHECK(rec.abortN == 2);
|
|
CHECK(rec.abort[1] == 302);
|
|
|
|
window = request(303, LG_CLIPBOARD_DATA_TEXT);
|
|
propNull(A_TEXT, 8, 3);
|
|
selection(window, A_TEXT, x11atoms.SEL_DATA);
|
|
CHECK(rec.abortN == 3);
|
|
CHECK(rec.abort[2] == 303);
|
|
CHECK(rec.dataN == 0);
|
|
finish();
|
|
}
|
|
|
|
static void testReplace(void)
|
|
{
|
|
start();
|
|
const unsigned long targets[] = { A_TEXT };
|
|
discover(64, targets, ARRAY_LENGTH(targets));
|
|
const Window first = request(401, LG_CLIPBOARD_DATA_TEXT);
|
|
|
|
const unsigned int convertN = rec.convertN;
|
|
owner(65);
|
|
CHECK(rec.convertN == convertN + 1);
|
|
CHECK(rec.abortN == 1);
|
|
CHECK(rec.abort[0] == 401);
|
|
CHECK(rec.window[1].window == first);
|
|
CHECK(rec.window[1].dead);
|
|
|
|
const Window second = request(402, LG_CLIPBOARD_DATA_TEXT);
|
|
owner(None);
|
|
CHECK(rec.abortN == 2);
|
|
CHECK(rec.abort[1] == 402);
|
|
CHECK(rec.releaseN == 1);
|
|
bool dead = false;
|
|
for (unsigned int i = 0; i < rec.windowN; ++i)
|
|
if (rec.window[i].window == second)
|
|
dead = rec.window[i].dead;
|
|
CHECK(dead);
|
|
|
|
x11CBRequest(403, LG_CLIPBOARD_DATA_TEXT);
|
|
CHECK(rec.abortN == 3);
|
|
CHECK(rec.abort[2] == 403);
|
|
finish();
|
|
}
|
|
|
|
static void testSource(void)
|
|
{
|
|
start();
|
|
x11CBNotice(LG_CLIPBOARD_DATA_PNG);
|
|
CHECK(rec.owner == x11.window);
|
|
|
|
selectionRequest(x11atoms.TARGETS, 700);
|
|
CHECK(rec.changeN == 1);
|
|
CHECK(rec.change[0].window == 900);
|
|
CHECK(rec.change[0].property == 700);
|
|
CHECK(rec.change[0].type == XA_ATOM);
|
|
CHECK(rec.change[0].format == 32);
|
|
CHECK(rec.change[0].count == 2);
|
|
const Atom * values = (const Atom *)rec.change[0].data;
|
|
CHECK(values[0] == x11atoms.TARGETS);
|
|
CHECK(values[1] == A_PNG);
|
|
CHECK(rec.sendN == 1);
|
|
CHECK(rec.send[0].xselection.property == 700);
|
|
|
|
selectionRequest(A_PNG, 701);
|
|
CHECK(rec.requestN == 1);
|
|
CHECK(rec.requestType == LG_CLIPBOARD_DATA_PNG);
|
|
CHECK(rec.requestStream);
|
|
CHECK(rec.sendN == 1);
|
|
const uint8_t data[] = { 1, 2, 3, 4 };
|
|
CHECK(rec.requestStream->begin(rec.requestOpaque,
|
|
LG_CLIPBOARD_DATA_PNG, LG_CLIPBOARD_SIZE_UNKNOWN) ==
|
|
LG_CLIPBOARD_RESULT_ACCEPTED);
|
|
CHECK(rec.changeN == 2);
|
|
CHECK(rec.change[1].property == 701);
|
|
CHECK(rec.change[1].type == x11atoms.INCR);
|
|
CHECK(rec.change[1].format == 32);
|
|
CHECK(rec.change[1].count == 1);
|
|
CHECK(rec.sendN == 2);
|
|
CHECK(rec.send[1].xselection.property == 701);
|
|
|
|
CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
|
|
data, ARRAY_LENGTH(data)) == LG_CLIPBOARD_RESULT_BLOCKED);
|
|
propertyState(900, 701, PropertyDelete);
|
|
CHECK(rec.requestReadyN == 1);
|
|
CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
|
|
data, ARRAY_LENGTH(data)) == LG_CLIPBOARD_RESULT_ACCEPTED);
|
|
CHECK(rec.changeN == 3);
|
|
CHECK(rec.change[2].property == 701);
|
|
CHECK(rec.change[2].type == A_PNG);
|
|
CHECK(rec.change[2].format == 8);
|
|
CHECK(rec.change[2].count == 4);
|
|
CHECK(memcmp(rec.change[2].data, data, sizeof(data)) == 0);
|
|
CHECK(rec.requestStream->end(rec.requestOpaque, ARRAY_LENGTH(data)) ==
|
|
LG_CLIPBOARD_RESULT_BLOCKED);
|
|
propertyState(900, 701, PropertyDelete);
|
|
CHECK(rec.requestReadyN == 2);
|
|
CHECK(rec.requestStream->end(rec.requestOpaque, ARRAY_LENGTH(data)) ==
|
|
LG_CLIPBOARD_RESULT_ACCEPTED);
|
|
CHECK(rec.changeN == 4);
|
|
CHECK(rec.change[3].property == 701);
|
|
CHECK(rec.change[3].type == A_PNG);
|
|
CHECK(rec.change[3].format == 8);
|
|
CHECK(rec.change[3].count == 0);
|
|
|
|
rec.requestCancelSync = true;
|
|
selectionRequest(A_PNG, 704);
|
|
CHECK(rec.requestN == 2);
|
|
CHECK(rec.sendN == 3);
|
|
CHECK(rec.send[2].xselection.property == None);
|
|
rec.requestCancelSync = false;
|
|
|
|
selectionRequest(A_TEXT, 702);
|
|
CHECK(rec.requestN == 2);
|
|
CHECK(rec.sendN == 4);
|
|
CHECK(rec.send[3].xselection.property == None);
|
|
|
|
rec.requestOK = false;
|
|
selectionRequest(A_PNG, 703);
|
|
CHECK(rec.requestN == 3);
|
|
CHECK(rec.sendN == 5);
|
|
CHECK(rec.send[4].xselection.property == None);
|
|
|
|
x11CBRelease();
|
|
CHECK(rec.owner == None);
|
|
finish();
|
|
}
|
|
|
|
static void testSourceBeginDuringRequest(void)
|
|
{
|
|
start();
|
|
x11CBNotice(LG_CLIPBOARD_DATA_PNG);
|
|
rec.requestBeginSync = true;
|
|
selectionRequest(A_PNG, 705);
|
|
CHECK(rec.requestN == 1);
|
|
CHECK(rec.requestBeginResult == LG_CLIPBOARD_RESULT_ACCEPTED);
|
|
CHECK(rec.requestOpaque);
|
|
CHECK(rec.changeN == 1);
|
|
CHECK(rec.change[0].type == x11atoms.INCR);
|
|
CHECK(rec.sendN == 1);
|
|
|
|
propertyState(900, 705, PropertyDelete);
|
|
CHECK(rec.requestStream->end(rec.requestOpaque, 0) ==
|
|
LG_CLIPBOARD_RESULT_ACCEPTED);
|
|
CHECK(rec.changeN == 2);
|
|
CHECK(rec.change[1].count == 0);
|
|
finish();
|
|
}
|
|
|
|
static void testTeardown(void)
|
|
{
|
|
start();
|
|
const unsigned int convertN = rec.convertN;
|
|
owner(66);
|
|
CHECK(rec.convertN == convertN + 1);
|
|
const Window targets = rec.convert[convertN].requestor;
|
|
const Window read = request(501, LG_CLIPBOARD_DATA_TEXT);
|
|
x11CBNotice(LG_CLIPBOARD_DATA_TEXT);
|
|
CHECK(rec.abortN == 1);
|
|
CHECK(rec.abort[0] == 501);
|
|
CHECK(rec.owner == x11.window);
|
|
CHECK(rec.destroyN == 2);
|
|
|
|
bool targetsDead = false;
|
|
bool readDead = false;
|
|
for (unsigned int i = 0; i < rec.windowN; ++i)
|
|
{
|
|
if (rec.window[i].window == targets)
|
|
targetsDead = rec.window[i].dead;
|
|
if (rec.window[i].window == read)
|
|
readDead = rec.window[i].dead;
|
|
}
|
|
CHECK(targetsDead);
|
|
CHECK(readDead);
|
|
|
|
x11CBRelease();
|
|
CHECK(rec.owner == None);
|
|
finish();
|
|
}
|
|
|
|
struct Test
|
|
{
|
|
const char * name;
|
|
void (*run)(void);
|
|
};
|
|
|
|
static const struct Test tests[] =
|
|
{
|
|
{ "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)
|
|
{
|
|
debug_init();
|
|
|
|
if (argc == 2)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i)
|
|
if (strcmp(argv[1], tests[i].name) == 0)
|
|
{
|
|
tests[i].run();
|
|
return 0;
|
|
}
|
|
|
|
fprintf(stderr, "unknown test: %s\n", argv[1]);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (argc != 1)
|
|
return EXIT_FAILURE;
|
|
|
|
for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i)
|
|
tests[i].run();
|
|
return 0;
|
|
}
|