/**
 * 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 "test.h"
#include "core/clipboard.h"
#include "core/clipboard_files.h"

#include "common/debug.h"

#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <unistd.h>

#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0]))
#define MAX_ABORT    8U
#define MAX_DATA     4U
#define MAX_MIME    12U
#define MAX_NOTICE   8U
#define MAX_OFFER    8U
#define MAX_POLL     8U
#define MAX_SOURCE   4U
#define MAX_TRANSFER 8192U

enum ProxyKind
{
  PROXY_MANAGER,
  PROXY_DEVICE,
  PROXY_OFFER,
  PROXY_SOURCE,
};

struct Proxy
{
  enum ProxyKind kind;
  void (**listener)(void);
  void           * data;
  void           * user;
  bool             dead;
  char             mime[MAX_MIME][80];
  unsigned int     mimeN;
};

struct Proto
{
  struct Proxy   manager;
  struct Proxy   device;
  struct Proxy   offer[MAX_OFFER];
  struct Proxy   source[MAX_SOURCE];
  unsigned int   offerN;
  unsigned int   sourceN;
  unsigned int   offerDestroyN;
  unsigned int   sourceDestroyN;
  unsigned int   deviceReleaseN;
  unsigned int   dirtyDestroyN;
  unsigned int   receiveN;
  unsigned int   setActionsN;
  unsigned int   selectionSetN;
  char           receiveMime[80];
  const void   * receiveData;
  size_t         receiveSize;
  int            receiveFd;
  bool           keepReceiveOpen;
  struct Proxy * selection;
  uint32_t       serial;
};

struct Poll
{
  int                 fd;
  WaylandPollCallback callback;
  WaylandPollCleanup  cleanup;
  void              * opaque;
  uint32_t            events;
  bool                active;
  bool                retired;
};

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 StreamNotice
{
  LG_ClipboardRequest request;
  LG_ClipboardData    type;
  uint64_t            sizeHint;
  uint64_t            finalSize;
  uint8_t             data[MAX_TRANSFER];
  size_t              size;
  unsigned int        beginN;
  unsigned int        chunkN;
  unsigned int        endN;
};

struct Log
{
  struct Poll         poll[MAX_POLL];
  struct TypeNotice   notice[MAX_NOTICE];
  struct DataNotice   data[MAX_DATA];
  struct StreamNotice streamData;
  LG_ClipboardRequest abort[MAX_ABORT];
  unsigned int        pollN;
  unsigned int        pollUnregisterN;
  unsigned int        pollCleanupN;
  unsigned int        noticeN;
  unsigned int        dataN;
  unsigned int        abortN;
  unsigned int        releaseN;
  unsigned int        requestN;
  unsigned int        requestReadyN;
  LG_ClipboardData    requestType;
  const LG_ClipboardStreamOps * requestStream;
  void               * requestOpaque;
  LG_ClipboardRequest requestId;
  LG_ClipboardResult  beginResult;
  LG_ClipboardResult  chunkResult;
  LG_ClipboardResult  endResult;
  LG_ClipboardResult  requestBeginResult;
  bool                 readyDuringChunk;
  bool                 readyDuringEnd;
  bool                 pollOK;
  bool                 requestOK;
  bool                 requestCancelSync;
  bool                 requestBeginSync;
  bool                 expectReleaseLocked;
  bool                 firing;
  unsigned int         releaseLockedN;
  unsigned int         fileSetN;
  unsigned int         fileClearN;
  unsigned int         presentationAcquireN;
  unsigned int         presentationDeliveredN;
  unsigned int         presentationReleaseN;
  unsigned int         presentationMimeN;
  uint64_t             presentationDelivered[MAX_SOURCE];
  uint64_t             presentationReleased[MAX_SOURCE];
  char                 presentationMime[MAX_MIME][80];
  char                 fileMime[80];
  uint8_t              fileData[MAX_TRANSFER];
  size_t               fileSize;
  bool                 fileSetOK;
  bool                 presentationOK;
};

struct WaylandDSState wlWm;
struct WCBState       wlCb;

static struct Proto proto;
static struct Log   rec;

const struct wl_interface wl_data_device_interface =
{
  .name    = "wl_data_device",
  .version = 3,
};

const struct wl_interface wl_data_source_interface =
{
  .name    = "wl_data_source",
  .version = 3,
};

uint32_t wl_proxy_get_version(struct wl_proxy * proxy)
{
  CHECK(!((struct Proxy *)proxy)->dead);
  return 3;
}

void wl_proxy_set_user_data(struct wl_proxy * proxy, void * data)
{
  struct Proxy * p = (struct Proxy *)proxy;
  CHECK(!p->dead);
  p->user = data;
}

void * wl_proxy_get_user_data(struct wl_proxy * proxy)
{
  struct Proxy * p = (struct Proxy *)proxy;
  CHECK(!p->dead);
  return p->user;
}

int wl_proxy_add_listener(struct wl_proxy * proxy,
    void (**listener)(void), void * data)
{
  struct Proxy * p = (struct Proxy *)proxy;
  CHECK(!p->dead);
  CHECK(!p->listener);
  p->listener = listener;
  p->data     = data;
  return 0;
}

static void destroy(struct Proxy * p)
{
  CHECK(p);
  CHECK(!p->dead);
  p->dead = true;
  if (p->user)
    ++proto.dirtyDestroyN;

  switch (p->kind)
  {
    case PROXY_OFFER:
      ++proto.offerDestroyN;
      break;
    case PROXY_SOURCE:
      ++proto.sourceDestroyN;
      break;
    case PROXY_DEVICE:
      ++proto.deviceReleaseN;
      break;
    default:
      CHECK(false);
  }
}

static void copyMime(struct Proxy * p, const char * mime)
{
  CHECK(p->mimeN < ARRAY_LENGTH(p->mime));
  const int n = snprintf(p->mime[p->mimeN], sizeof(p->mime[p->mimeN]),
      "%s", mime);
  CHECK(n >= 0);
  CHECK((size_t)n < sizeof(p->mime[p->mimeN]));
  ++p->mimeN;
}

static void sendData(int fd, const void * data, size_t size)
{
  const uint8_t * pos = data;
  while (size)
  {
    const ssize_t n = write(fd, pos, size);
    CHECK(n > 0);
    pos  += n;
    size -= n;
  }
}

struct wl_proxy * wl_proxy_marshal_flags(struct wl_proxy * proxy,
    uint32_t opcode, const struct wl_interface * interface,
    uint32_t version, uint32_t flags, ...)
{
  struct Proxy * p = (struct Proxy *)proxy;
  CHECK(p);
  CHECK(!p->dead);
  CHECK(version == 3);

  if (flags & WL_MARSHAL_FLAG_DESTROY)
  {
    CHECK(!interface);
    switch (p->kind)
    {
      case PROXY_DEVICE:
        CHECK(opcode == WL_DATA_DEVICE_RELEASE);
        break;
      case PROXY_OFFER:
        CHECK(opcode == WL_DATA_OFFER_DESTROY);
        break;
      case PROXY_SOURCE:
        CHECK(opcode == WL_DATA_SOURCE_DESTROY);
        break;
      default:
        CHECK(false);
    }
    destroy(p);
    return NULL;
  }

  va_list ap;
  va_start(ap, flags);
  switch (p->kind)
  {
    case PROXY_MANAGER:
      if (opcode == WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE)
      {
        CHECK(interface == &wl_data_device_interface);
        p = &proto.device;
      }
      else
      {
        CHECK(opcode == WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE);
        CHECK(interface == &wl_data_source_interface);
        CHECK(proto.sourceN < ARRAY_LENGTH(proto.source));
        p       = &proto.source[proto.sourceN++];
        p->kind = PROXY_SOURCE;
      }
      break;

    case PROXY_DEVICE:
      CHECK(opcode == WL_DATA_DEVICE_SET_SELECTION);
      CHECK(!interface);
      proto.selection = (struct Proxy *)va_arg(ap, struct wl_data_source *);
      proto.serial    = va_arg(ap, unsigned int);
      ++proto.selectionSetN;
      p               = NULL;
      break;

    case PROXY_OFFER:
      CHECK(!interface);
      if (opcode == WL_DATA_OFFER_RECEIVE)
      {
        copyMime(p, va_arg(ap, const char *));
        const int fd = va_arg(ap, int);
        ++proto.receiveN;
        snprintf(proto.receiveMime, sizeof(proto.receiveMime),
            "%s", p->mime[p->mimeN - 1]);
        if (proto.keepReceiveOpen)
        {
          CHECK(proto.receiveFd < 0);
          proto.receiveFd = dup(fd);
          CHECK(proto.receiveFd >= 0);
        }
        if (proto.receiveSize)
          sendData(fd, proto.receiveData, proto.receiveSize);
      }
      else
      {
        CHECK(opcode == WL_DATA_OFFER_SET_ACTIONS);
        CHECK(va_arg(ap, unsigned int) ==
            WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE);
        CHECK(va_arg(ap, unsigned int) ==
            WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE);
        ++proto.setActionsN;
      }
      p = NULL;
      break;

    case PROXY_SOURCE:
      CHECK(opcode == WL_DATA_SOURCE_OFFER);
      CHECK(!interface);
      copyMime(p, va_arg(ap, const char *));
      p = NULL;
      break;
  }
  va_end(ap);
  return (struct wl_proxy *)p;
}

static void pollCleanup(struct Poll * poll)
{
  CHECK(poll->retired);
  poll->retired = false;
  ++rec.pollCleanupN;
  if (poll->cleanup)
    poll->cleanup(poll->opaque);
  poll->cleanup = NULL;
  poll->opaque  = NULL;
}

bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback,
    void * opaque, WaylandPollCleanup cleanup, uint32_t events)
{
  if (!rec.pollOK)
    return false;

  CHECK(rec.pollN < ARRAY_LENGTH(rec.poll));
  struct Poll * poll = &rec.poll[rec.pollN++];
  poll->fd       = fd;
  poll->callback = callback;
  poll->cleanup  = cleanup;
  poll->opaque   = opaque;
  poll->events   = events;
  poll->active   = true;
  return true;
}

bool waylandPollUnregister(int fd)
{
  for (unsigned int i = 0; i < rec.pollN; ++i)
  {
    struct Poll * poll = &rec.poll[i];
    if (!poll->active || poll->fd != fd)
      continue;

    poll->active  = false;
    poll->retired = true;
    ++rec.pollUnregisterN;
    if (!rec.firing)
      pollCleanup(poll);
    return true;
  }
  return false;
}

bool waylandPollUpdate(int fd, uint32_t events)
{
  for (unsigned int i = 0; i < rec.pollN; ++i)
  {
    struct Poll * poll = &rec.poll[i];
    if (!poll->active || poll->fd != fd)
      continue;

    poll->events = events;
    return true;
  }
  return false;
}

static void pollFire(unsigned int no, uint32_t events)
{
  CHECK(no < rec.pollN);
  struct Poll * poll = &rec.poll[no];
  CHECK(poll->active);
  CHECK(events & (poll->events | EPOLLERR));

  rec.firing = true;
  poll->callback(events, poll->opaque);
  rec.firing = false;
  for (unsigned int i = 0; i < rec.pollN; ++i)
    if (rec.poll[i].retired)
      pollCleanup(&rec.poll[i]);
}

void clipboard_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++];
  memcpy(notice->type, types, count * sizeof(*types));
  notice->count = count;
}

void clipboard_release(void)
{
  if (rec.expectReleaseLocked)
  {
    CHECK(atomic_flag_test_and_set_explicit(
        &wlCb.lock, memory_order_acquire));
    ++rec.releaseLockedN;
  }
  ++rec.releaseN;
}

void clipboard_abort(LG_ClipboardRequest request)
{
  CHECK(rec.abortN < ARRAY_LENGTH(rec.abort));
  rec.abort[rec.abortN++] = request;
}

void clipboard_data(LG_ClipboardRequest request,
    LG_ClipboardData type, const void * data, size_t size)
{
  CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
  CHECK(size <= MAX_TRANSFER);
  struct DataNotice * notice = &rec.data[rec.dataN++];
  notice->request = request;
  notice->type    = type;
  notice->size    = size;
  memcpy(notice->data, data, size);
}

LG_ClipboardResult clipboard_dataBegin(LG_ClipboardRequest request,
    LG_ClipboardData type, uint64_t sizeHint)
{
  struct StreamNotice * stream = &rec.streamData;
  ++stream->beginN;
  stream->request  = request;
  stream->type     = type;
  stream->sizeHint = sizeHint;
  return rec.beginResult;
}

LG_ClipboardResult clipboard_dataChunk(LG_ClipboardRequest request,
    uint64_t offset, const void * data, size_t size)
{
  const LG_ClipboardResult result = rec.chunkResult;
  struct StreamNotice * stream = &rec.streamData;
  ++stream->chunkN;
  CHECK(request == stream->request);
  CHECK(offset == stream->size);
  CHECK(data);
  CHECK(size);
  CHECK(size <= sizeof(stream->data) - stream->size);
  if (rec.readyDuringChunk)
  {
    rec.readyDuringChunk = false;
    rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
    waylandCBRequestReady(request);
  }
  if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
  {
    memcpy(stream->data + stream->size, data, size);
    stream->size += size;
  }
  return result;
}

LG_ClipboardResult clipboard_dataEnd(LG_ClipboardRequest request,
    uint64_t finalSize)
{
  const LG_ClipboardResult result = rec.endResult;
  struct StreamNotice * stream = &rec.streamData;
  ++stream->endN;
  CHECK(request == stream->request);
  stream->finalSize = finalSize;
  if (rec.readyDuringEnd)
  {
    rec.readyDuringEnd = false;
    rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
    waylandCBRequestReady(request);
  }
  return result;
}

bool clipboard_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);
    const uint8_t byte = 0xa5;
    CHECK(stream->chunk(opaque, 0, &byte, 1) ==
        LG_CLIPBOARD_RESULT_ACCEPTED);
    CHECK(stream->chunk(opaque, 1, &byte, 1) ==
        LG_CLIPBOARD_RESULT_BLOCKED);
    pollFire(rec.pollN - 1, EPOLLOUT);
  }
  if (rec.requestCancelSync)
    stream->cancel(opaque, LG_CLIPBOARD_CANCEL_REPLACED);
  return rec.requestOK;
}

bool clipboard_requestReady(LG_ClipboardRequest request)
{
  CHECK(request == rec.requestId);
  ++rec.requestReadyN;
  return true;
}

bool clipboardFiles_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 clipboardFiles_clearLocal(void)
{
  ++rec.fileClearN;
}

uint64_t clipboardFiles_remotePresentationAcquire(void)
{
  ++rec.presentationAcquireN;
  return rec.presentationOK ? 1000U + rec.presentationAcquireN : 0;
}

bool clipboardFiles_getRemotePresentation(uint64_t presentation,
    const char * mime, char ** data, size_t * size)
{
  CHECK(presentation >= 1001U);
  CHECK(rec.presentationMimeN < MAX_MIME);
  snprintf(rec.presentationMime[rec.presentationMimeN++],
      sizeof(rec.presentationMime[0]), "%s", mime);
  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") ||
      !strcmp(mime, "x-special/mate-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 clipboardFiles_remotePresentationDelivered(uint64_t presentation)
{
  CHECK(presentation);
  CHECK(rec.presentationDeliveredN <
      ARRAY_LENGTH(rec.presentationDelivered));
  rec.presentationDelivered[rec.presentationDeliveredN++] = presentation;
}

void clipboardFiles_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);
  return (const struct wl_data_device_listener *)proto.device.listener;
}

static struct Proxy * newOffer(void)
{
  CHECK(proto.offerN < ARRAY_LENGTH(proto.offer));
  struct Proxy * offer = &proto.offer[proto.offerN++];
  offer->kind = PROXY_OFFER;
  deviceListener()->data_offer(proto.device.data,
      (struct wl_data_device *)&proto.device,
      (struct wl_data_offer *)offer);
  CHECK(offer->listener);
  CHECK(offer->user);
  return offer;
}

static struct Proxy * rawOffer(void)
{
  CHECK(proto.offerN < ARRAY_LENGTH(proto.offer));
  struct Proxy * offer = &proto.offer[proto.offerN++];
  offer->kind = PROXY_OFFER;
  return offer;
}

static void offerMime(struct Proxy * offer, const char * mime)
{
  const struct wl_data_offer_listener * listener =
    (const struct wl_data_offer_listener *)offer->listener;
  listener->offer(offer->data, (struct wl_data_offer *)offer, mime);
}

static void selectOffer(struct Proxy * offer)
{
  proto.selection = offer;
  deviceListener()->selection(proto.device.data,
      (struct wl_data_device *)&proto.device,
      (struct wl_data_offer *)offer);
}

static const struct wl_data_source_listener * sourceListener(
    struct Proxy * source)
{
  CHECK(source);
  CHECK(source->listener);
  return (const struct wl_data_source_listener *)source->listener;
}

static void start(void)
{
  memset(&wlWm, 0, sizeof(wlWm));
  memset(&wlCb, 0, sizeof(wlCb));
  memset(&proto, 0, sizeof(proto));
  memset(&rec, 0, sizeof(rec));
  proto.receiveFd = -1;
  LG_LOCK_INIT(wlCb.lock);

  rec.pollOK             = true;
  rec.requestOK          = 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;
  proto.manager.kind     = PROXY_MANAGER;
  proto.device.kind      = PROXY_DEVICE;
  wlWm.dataDeviceManager =
    (struct wl_data_device_manager *)&proto.manager;
  wlWm.seat                = (struct wl_seat *)(uintptr_t)1;
  wlWm.keyboardEnterSerial = 77;

  CHECK(waylandCBInit());
  CHECK(wlCb.dataDevice == (struct wl_data_device *)&proto.device);
  CHECK(proto.device.listener);
  CHECK(strstr(wlCb.lgMimetype, "application/x-looking-glass-copy;pid=") ==
      wlCb.lgMimetype);
}

static void finish(void)
{
  if (proto.receiveFd >= 0)
  {
    CHECK(close(proto.receiveFd) == 0);
    proto.receiveFd = -1;
  }
  waylandCBFree();
  CHECK(!wlCb.dataDevice);
  CHECK(!wlCb.offer);
  CHECK(!wlCb.dndOffer);
  CHECK(!wlCb.currentRead);
  CHECK(proto.device.dead);
  CHECK(proto.deviceReleaseN == 1);
  for (unsigned int i = 0; i < rec.pollN; ++i)
    CHECK(!rec.poll[i].active);
}

static struct Proxy * textOffer(void)
{
  struct Proxy * offer = newOffer();
  offerMime(offer, "text/plain;charset=utf-8");
  selectOffer(offer);
  return offer;
}

static void checkClosed(int fd)
{
  errno = 0;
  CHECK(fcntl(fd, F_GETFD) == -1);
  CHECK(errno == EBADF);
}

static void testMime(void)
{
  start();
  struct Proxy * offer = newOffer();
  offerMime(offer, "text/html");
  offerMime(offer, "text/ico");
  offerMime(offer, "image/png");
  offerMime(offer, "application/xhtml+xml");
  offerMime(offer, "text/plain;charset=utf-8");
  offerMime(offer, "image/x-MS-bmp");
  offerMime(offer, "image/bmp");
  selectOffer(offer);

  CHECK(rec.noticeN == 1);
  CHECK(rec.notice[0].count == 3);
  CHECK(rec.notice[0].type[0] == LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.notice[0].type[1] == LG_CLIPBOARD_DATA_PNG);
  CHECK(rec.notice[0].type[2] == LG_CLIPBOARD_DATA_BMP);
  CHECK(strcmp(wlCb.mimetypes[LG_CLIPBOARD_DATA_TEXT],
      "text/plain;charset=utf-8") == 0);
  CHECK(strcmp(wlCb.mimetypes[LG_CLIPBOARD_DATA_PNG], "image/png") == 0);
  CHECK(strcmp(wlCb.mimetypes[LG_CLIPBOARD_DATA_BMP],
      "image/x-MS-bmp") == 0);
  CHECK(!offer->user);

  finish();
  CHECK(proto.offerDestroyN == 1);
  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 testFileImportMate(void)
{
  start();
  const char payload[] =
    "copy\nfile:///home/user/first.txt\r\n"
    "file:///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/mate-copied-files");
  selectOffer(offer);

  CHECK(rec.noticeN == 0);
  CHECK(proto.receiveN == 1);
  CHECK(strcmp(proto.receiveMime,
      "x-special/mate-copied-files") == 0);
  CHECK(rec.pollN == 1);
  pollFire(0, EPOLLIN);
  CHECK(rec.fileSetN == 1);
  CHECK(strcmp(rec.fileMime,
      "x-special/mate-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(rec.presentationMimeN == 3);
  CHECK(strcmp(rec.presentationMime[0], "text/uri-list") == 0);
  CHECK(strcmp(rec.presentationMime[1],
      "x-special/gnome-copied-files") == 0);
  CHECK(strcmp(rec.presentationMime[2],
      "application/x-kde-cutselection") == 0);
  CHECK(proto.sourceN == 1);
  struct Proxy * source = &proto.source[0];
  CHECK(source->mimeN == 5);
  CHECK(strcmp(source->mime[0],
      "x-special/gnome-copied-files") == 0);
  CHECK(strcmp(source->mime[1],
      "x-special/mate-copied-files") == 0);
  CHECK(strcmp(source->mime[2], "text/uri-list") == 0);
  CHECK(strcmp(source->mime[3],
      "application/x-kde-cutselection") == 0);
  CHECK(strcmp(source->mime[4], wlCb.lgMimetype) == 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 mate[2];
  CHECK(pipe(mate) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source,
      "x-special/mate-copied-files", mate[1]);
  CHECK(rec.pollN == 2);
  pollFire(1, EPOLLOUT);
  CHECK(rec.presentationDeliveredN == 2);
  CHECK(rec.presentationDelivered[1] == 1001U);
  const char mateExpected[] =
    "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n";
  char mateActual[sizeof(mateExpected)] = { 0 };
  CHECK(read(mate[0], mateActual, sizeof(mateActual)) ==
      (ssize_t)(sizeof(mateExpected) - 1U));
  CHECK(memcmp(mateActual,
      mateExpected, sizeof(mateExpected) - 1U) == 0);
  CHECK(read(mate[0], mateActual, 1) == 0);
  CHECK(close(mate[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 == 3);
  pollFire(2, EPOLLOUT);
  CHECK(rec.presentationDeliveredN == 2);
  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();
  struct Proxy * local = textOffer();
  CHECK(rec.noticeN == 1);

  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];
  CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
  CHECK(wlCb.offer == (struct wl_data_offer *)local);
  CHECK(!local->dead);

  /* A remote notice and its self echo must not erase the Wayland offer for a
   * local clipboard generation that Windows can still request. */
  const uint8_t localData[] = "local clipboard";
  proto.receiveData = localData;
  proto.receiveSize = sizeof(localData) - 1;

  /* Losing focus before the compositor echoes our source must not release
   * the remote clipboard. */
  waylandCBInvalidate();
  CHECK(rec.releaseN == 0);
  CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
  CHECK(wlCb.offer == (struct wl_data_offer *)local);
  CHECK(!local->dead);

  struct Proxy * echo = newOffer();
  offerMime(echo, "text/plain");
  offerMime(echo, wlCb.lgMimetype);
  selectOffer(echo);

  CHECK(rec.noticeN == 1);
  CHECK(rec.releaseN == 0);
  CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
  CHECK(wlCb.offer == (struct wl_data_offer *)local);
  CHECK(wlCb.mimetypes[LG_CLIPBOARD_DATA_TEXT]);
  CHECK(!local->dead);
  CHECK(echo->dead);
  CHECK(proto.offerDestroyN == 1);
  CHECK(!echo->user);

  waylandCBInvalidate();
  CHECK(rec.releaseN == 0);
  CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
  CHECK(wlCb.offer == (struct wl_data_offer *)local);

  waylandCBRequest(102, LG_CLIPBOARD_DATA_TEXT);
  CHECK(proto.receiveN == 1);
  CHECK(rec.pollN == 1);
  pollFire(0, EPOLLIN);
  pollFire(0, EPOLLIN);
  CHECK(rec.streamData.request == 102);
  CHECK(rec.streamData.size == sizeof(localData) - 1);
  CHECK(rec.streamData.endN == 1);
  CHECK(memcmp(rec.streamData.data, localData,
      sizeof(localData) - 1) == 0);

  int fds[2];
  CHECK(pipe(fds) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source, "text/plain", fds[1]);
  CHECK(rec.requestN == 1);
  CHECK(rec.requestType == LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.requestStream);
  CHECK(rec.requestOpaque);
  CHECK(rec.requestStream->begin(rec.requestOpaque,
      LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.requestStream->end(rec.requestOpaque, 0) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  uint8_t value;
  CHECK(read(fds[0], &value, 1) == 0);
  CHECK(close(fds[0]) == 0);
  checkClosed(fds[1]);

  selectOffer(NULL);
  CHECK(rec.releaseN == 1);
  CHECK(!wlCb.selectionSource);
  CHECK(local->dead);
  CHECK(proto.offerDestroyN == 2);

  CHECK(proto.dirtyDestroyN == 0);
  finish();
}

static void testLateSelfCopy(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];
  CHECK(wlCb.selectionSource == (struct wl_data_source *)source);

  struct Proxy * local = textOffer();
  CHECK(rec.noticeN == 1);
  CHECK(!wlCb.selectionSource);
  CHECK(wlCb.offer == (struct wl_data_offer *)local);
  CHECK(!local->dead);

  /* The compositor may deliver our marked echo after a newer external
   * selection. The stale echo must not discard that local selection. */
  struct Proxy * echo = newOffer();
  offerMime(echo, "text/plain");
  offerMime(echo, wlCb.lgMimetype);
  selectOffer(echo);

  CHECK(rec.noticeN == 1);
  CHECK(rec.releaseN == 0);
  CHECK(!wlCb.selectionSource);
  CHECK(wlCb.offer == (struct wl_data_offer *)local);
  CHECK(wlCb.mimetypes[LG_CLIPBOARD_DATA_TEXT]);
  CHECK(!local->dead);
  CHECK(echo->dead);

  const uint8_t data[] = "new local selection";
  proto.receiveData = data;
  proto.receiveSize = sizeof(data) - 1;
  waylandCBRequest(102, LG_CLIPBOARD_DATA_TEXT);
  CHECK(proto.receiveN == 1);
  CHECK(rec.pollN == 1);
  pollFire(0, EPOLLIN);
  pollFire(0, EPOLLIN);
  CHECK(rec.streamData.request == 102);
  CHECK(rec.streamData.size == sizeof(data) - 1);
  CHECK(rec.streamData.endN == 1);
  CHECK(memcmp(rec.streamData.data, data, sizeof(data) - 1) == 0);

  CHECK(proto.dirtyDestroyN == 0);
  finish();
}

static void testIgnoredOffer(void)
{
  start();
  struct Proxy * offer = newOffer();
  offerMime(offer, "application/x-unsupported");
  selectOffer(offer);

  CHECK(rec.noticeN == 0);
  CHECK(rec.releaseN == 1);
  CHECK(offer->dead);
  CHECK(proto.offerDestroyN == 1);
  CHECK(!offer->user);
  CHECK(proto.dirtyDestroyN == 0);
  finish();
}

static void testInvalidateSerialization(void)
{
  start();
  struct Proxy * offer = textOffer();
  rec.expectReleaseLocked = true;

  waylandCBInvalidate();

  CHECK(rec.releaseN == 1);
  CHECK(rec.releaseLockedN == 1);
  CHECK(offer->dead);
  CHECK(!wlCb.offer);
  finish();
}

static void testReplace(void)
{
  start();
  struct Proxy * first = textOffer();
  waylandCBRequest(101, LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.pollN == 1);
  const int fd = rec.poll[0].fd;

  struct Proxy * second = newOffer();
  offerMime(second, "image/png");
  selectOffer(second);
  CHECK(first->dead);
  CHECK(!second->dead);
  CHECK(rec.abortN == 1);
  CHECK(rec.abort[0] == 101);
  CHECK(rec.pollUnregisterN == 1);
  CHECK(rec.pollCleanupN == 1);
  CHECK(!rec.poll[0].active);
  checkClosed(fd);
  CHECK(rec.noticeN == 2);
  CHECK(rec.notice[1].count == 1);
  CHECK(rec.notice[1].type[0] == LG_CLIPBOARD_DATA_PNG);

  selectOffer(NULL);
  CHECK(second->dead);
  CHECK(rec.releaseN == 1);
  CHECK(proto.offerDestroyN == 2);
  finish();
  CHECK(proto.dirtyDestroyN == 0);
}

static void testReadEOF(void)
{
  start();
  textOffer();
  uint8_t data[4096];
  for (size_t i = 0; i < sizeof(data); ++i)
    data[i] = i;
  proto.receiveData = data;
  proto.receiveSize = sizeof(data);

  waylandCBRequest(201, LG_CLIPBOARD_DATA_TEXT);
  CHECK(proto.receiveN == 1);
  CHECK(strcmp(proto.receiveMime, "text/plain;charset=utf-8") == 0);
  CHECK(rec.pollN == 1);
  CHECK(rec.poll[0].events == EPOLLIN);
  const int fd = rec.poll[0].fd;

  pollFire(0, EPOLLIN);
  CHECK(rec.poll[0].active);
  CHECK(rec.streamData.beginN == 1);
  CHECK(rec.streamData.chunkN == 1);
  CHECK(rec.streamData.endN == 0);
  pollFire(0, EPOLLIN);
  CHECK(!rec.poll[0].active);
  CHECK(rec.pollCleanupN == 1);
  CHECK(rec.streamData.request == 201);
  CHECK(rec.streamData.type == LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.streamData.sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN);
  CHECK(rec.streamData.size == sizeof(data));
  CHECK(rec.streamData.finalSize == sizeof(data));
  CHECK(rec.streamData.endN == 1);
  CHECK(memcmp(rec.streamData.data, data, sizeof(data)) == 0);
  checkClosed(fd);

  finish();
  CHECK(rec.abortN == 0);
}

static void testReadBlocked(void)
{
  start();
  textOffer();
  const uint8_t data[] = "blocked stream";
  proto.receiveData = data;
  proto.receiveSize = sizeof(data) - 1;
  rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;

  waylandCBRequest(202, LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.pollN == 1);
  pollFire(0, EPOLLIN);
  CHECK(rec.streamData.beginN == 1);
  CHECK(rec.streamData.chunkN == 1);
  CHECK(rec.streamData.size == 0);
  CHECK(rec.poll[0].events == 0);

  rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
  waylandCBRequestReady(202);
  CHECK(rec.streamData.chunkN == 2);
  CHECK(rec.streamData.size == sizeof(data) - 1);
  CHECK(rec.streamData.endN == 1);
  CHECK(rec.streamData.finalSize == sizeof(data) - 1);
  CHECK(memcmp(rec.streamData.data, data, sizeof(data) - 1) == 0);
  CHECK(!rec.poll[0].active);
  CHECK(rec.abortN == 0);
  finish();
}

static void testReadBlockedEmpty(void)
{
  start();
  textOffer();
  const uint8_t data[] = "buffered while source stays open";
  proto.receiveData     = data;
  proto.receiveSize     = sizeof(data) - 1;
  proto.keepReceiveOpen = true;
  rec.chunkResult       = LG_CLIPBOARD_RESULT_BLOCKED;

  waylandCBRequest(204, LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.pollN == 1);
  pollFire(0, EPOLLIN);
  CHECK(rec.streamData.chunkN == 1);
  CHECK(rec.streamData.size == 0);
  CHECK(rec.poll[0].events == 0);

  /* The read drained the pipe, whose duplicated write end remains open.
   * Ready must retry the buffered chunk without another poll callback. */
  rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
  waylandCBRequestReady(204);
  CHECK(rec.streamData.chunkN == 2);
  CHECK(rec.streamData.size == sizeof(data) - 1);
  CHECK(memcmp(rec.streamData.data, data, sizeof(data) - 1) == 0);
  CHECK(rec.streamData.endN == 0);
  CHECK(rec.poll[0].active);
  CHECK(rec.poll[0].events == EPOLLIN);

  CHECK(close(proto.receiveFd) == 0);
  proto.receiveFd = -1;
  pollFire(0, EPOLLIN);
  CHECK(rec.streamData.endN == 1);
  CHECK(rec.streamData.finalSize == sizeof(data) - 1);
  CHECK(!rec.poll[0].active);
  CHECK(rec.abortN == 0);
  finish();
}

static void testReadReadyRace(void)
{
  start();
  textOffer();
  const uint8_t data[] = "ready while returning blocked";
  proto.receiveData = data;
  proto.receiveSize = sizeof(data) - 1;
  rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
  rec.readyDuringChunk = true;

  waylandCBRequest(203, LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.pollN == 1);
  pollFire(0, EPOLLIN);
  CHECK(rec.streamData.beginN == 1);
  CHECK(rec.streamData.chunkN == 2);
  CHECK(rec.streamData.size == sizeof(data) - 1);
  CHECK(rec.poll[0].events == EPOLLIN);

  rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED;
  rec.readyDuringEnd = true;
  pollFire(0, EPOLLIN);
  CHECK(rec.streamData.endN == 2);
  CHECK(rec.streamData.finalSize == sizeof(data) - 1);
  CHECK(!rec.poll[0].active);
  CHECK(rec.abortN == 0);
  finish();
}

static void testReadError(void)
{
  start();
  textOffer();
  waylandCBRequest(301, LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.pollN == 1);
  const int firstFd = rec.poll[0].fd;
  CHECK(close(firstFd) == 0);
  pollFire(0, EPOLLIN);
  CHECK(rec.abortN == 1);
  CHECK(rec.abort[0] == 301);
  CHECK(rec.pollCleanupN == 1);
  checkClosed(firstFd);

  waylandCBRequest(302, LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.pollN == 2);
  const int secondFd = rec.poll[1].fd;
  pollFire(1, EPOLLERR);
  CHECK(rec.abortN == 2);
  CHECK(rec.abort[1] == 302);
  CHECK(rec.pollCleanupN == 2);
  checkClosed(secondFd);

  finish();
}

static void testSource(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  CHECK(proto.sourceN == 1);
  struct Proxy * source = &proto.source[0];
  CHECK(proto.selection == source);
  CHECK(proto.serial == 77);
  CHECK(source->mimeN == 3);
  CHECK(strcmp(source->mime[0], "text/plain;charset=utf-8") == 0);
  CHECK(strcmp(source->mime[1], "text/plain") == 0);
  CHECK(strcmp(source->mime[2], wlCb.lgMimetype) == 0);

  int bad[2];
  CHECK(pipe(bad) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source, "image/png", bad[1]);
  CHECK(rec.pollN == 0);
  uint8_t value;
  CHECK(read(bad[0], &value, 1) == 0);
  CHECK(close(bad[0]) == 0);

  int good[2];
  CHECK(pipe(good) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source, "text/plain", good[1]);
  CHECK(rec.pollN == 1);
  CHECK(rec.poll[0].fd == good[1]);
  CHECK(rec.poll[0].events == 0);
  CHECK(rec.requestN == 1);
  CHECK(rec.requestType == LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.requestStream);
  CHECK(rec.requestOpaque);

  int queued[2];
  CHECK(pipe(queued) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source,
      "text/plain;charset=utf-8", queued[1]);
  CHECK(rec.pollN == 1);
  CHECK(rec.requestN == 1);

  const uint8_t first [] = "source ";
  const uint8_t second[] = "data";
  CHECK(rec.requestStream->begin(rec.requestOpaque,
      LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
      first, sizeof(first) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.poll[0].events == EPOLLOUT);
  CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1,
      second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_BLOCKED);
  pollFire(0, EPOLLOUT);
  CHECK(rec.requestReadyN == 1);
  CHECK(rec.poll[0].events == 0);

  CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1,
      second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.requestStream->end(rec.requestOpaque,
      sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_BLOCKED);
  pollFire(0, EPOLLOUT);
  CHECK(rec.requestReadyN == 2);
  CHECK(rec.requestStream->end(rec.requestOpaque,
      sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(!rec.poll[0].active);
  CHECK(rec.pollN == 2);
  CHECK(rec.poll[1].fd == queued[1]);
  CHECK(rec.poll[1].events == EPOLLOUT);
  pollFire(1, EPOLLOUT);
  checkClosed(good[1]);
  checkClosed(queued[1]);

  int cached[2];
  CHECK(pipe(cached) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source, "text/plain", cached[1]);
  CHECK(rec.pollN == 3);
  CHECK(rec.poll[2].fd == cached[1]);
  CHECK(rec.poll[2].events == EPOLLOUT);
  CHECK(rec.requestN == 1);
  pollFire(2, EPOLLOUT);
  checkClosed(cached[1]);

  sourceListener(source)->cancelled(source->data,
      (struct wl_data_source *)source);
  CHECK(source->dead);
  CHECK(proto.sourceDestroyN == 1);
  CHECK(rec.pollCleanupN == 3);
  const size_t expected = sizeof(first) + sizeof(second) - 2;
  const int    reads[]  = { good[0], queued[0], cached[0] };
  for (size_t i = 0; i < ARRAY_LENGTH(reads); ++i)
  {
    uint8_t actual[sizeof(first) + sizeof(second)] = {};
    CHECK(read(reads[i], actual, sizeof(actual)) == (ssize_t)expected);
    CHECK(memcmp(actual, "source data", expected) == 0);
    CHECK(read(reads[i], &value, 1) == 0);
    CHECK(close(reads[i]) == 0);
  }

  finish();
}

static void testSourceRelease(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];
  CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
  CHECK(wlCb.sources);
  CHECK(proto.selection == source);
  CHECK(proto.selectionSetN == 1);

  waylandCBRelease();
  CHECK(!wlCb.selectionSource);
  CHECK(wlCb.sources);
  CHECK(!proto.selection);
  CHECK(proto.selectionSetN == 2);

  waylandCBRelease();
  CHECK(proto.selectionSetN == 2);

  sourceListener(source)->cancelled(source->data,
      (struct wl_data_source *)source);
  CHECK(source->dead);
  CHECK(!wlCb.sources);
  finish();
}

static void testSourceExternalOwner(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];
  CHECK(proto.selectionSetN == 1);

  struct Proxy * external = newOffer();
  offerMime(external, "image/png");
  selectOffer(external);
  CHECK(!wlCb.selectionSource);
  CHECK(proto.selection == external);

  waylandCBRelease();
  CHECK(proto.selectionSetN == 1);
  CHECK(proto.selection == external);

  sourceListener(source)->cancelled(source->data,
      (struct wl_data_source *)source);
  CHECK(source->dead);
  finish();
}

static void testSourceReplacement(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * first = &proto.source[0];
  waylandCBNotice(LG_CLIPBOARD_DATA_PNG);
  struct Proxy * second = &proto.source[1];
  CHECK(proto.selection == second);
  CHECK(proto.selectionSetN == 2);
  CHECK(wlCb.selectionSource == (struct wl_data_source *)second);

  sourceListener(first)->cancelled(first->data,
      (struct wl_data_source *)first);
  CHECK(first->dead);
  CHECK(wlCb.selectionSource == (struct wl_data_source *)second);

  waylandCBRelease();
  CHECK(!proto.selection);
  CHECK(proto.selectionSetN == 3);
  CHECK(!wlCb.selectionSource);

  sourceListener(second)->cancelled(second->data,
      (struct wl_data_source *)second);
  CHECK(second->dead);
  CHECK(!wlCb.sources);
  finish();
}

static void testSourceCancelDuringRequest(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];
  rec.requestCancelSync = true;

  int fds[2];
  CHECK(pipe(fds) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source, "text/plain", fds[1]);
  CHECK(rec.requestN == 1);
  CHECK(rec.pollN == 1);
  CHECK(!rec.poll[0].active);
  CHECK(rec.pollCleanupN == 1);
  checkClosed(fds[1]);
  uint8_t value;
  CHECK(read(fds[0], &value, 1) == 0);
  CHECK(close(fds[0]) == 0);

  sourceListener(source)->cancelled(source->data,
      (struct wl_data_source *)source);
  finish();
}

static void testSourceBeginDuringRequest(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];
  rec.requestBeginSync = true;

  int fds[2];
  CHECK(pipe(fds) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source, "text/plain", fds[1]);
  CHECK(rec.requestN == 1);
  CHECK(rec.requestBeginResult == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.requestOpaque);
  CHECK(rec.requestReadyN == 1);

  CHECK(rec.requestStream->end(rec.requestOpaque, 1) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.pollCleanupN == 1);
  uint8_t value = 0;
  CHECK(read(fds[0], &value, 1) == 1);
  CHECK(value == 0xa5);
  CHECK(read(fds[0], &value, 1) == 0);
  CHECK(close(fds[0]) == 0);
  checkClosed(fds[1]);

  sourceListener(source)->cancelled(source->data,
      (struct wl_data_source *)source);
  finish();
}

static void testSourceError(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];

  int fds[2];
  CHECK(pipe(fds) == 0);
  sourceListener(source)->send(source->data,
      (struct wl_data_source *)source, "text/plain", fds[1]);
  CHECK(rec.requestStream->begin(rec.requestOpaque,
      LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) ==
      LG_CLIPBOARD_RESULT_ACCEPTED);

  const uint8_t first[] = "first";
  const uint8_t second[] = "second";
  CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
      first, sizeof(first) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1,
      second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_BLOCKED);
  CHECK(close(fds[0]) == 0);
  pollFire(0, EPOLLERR);
  CHECK(rec.requestReadyN == 1);
  CHECK(!rec.poll[0].active);
  CHECK(rec.pollCleanupN == 1);

  CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1,
      second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
  CHECK(rec.requestStream->end(rec.requestOpaque,
      sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED);
  checkClosed(fds[1]);

  sourceListener(source)->cancelled(source->data,
      (struct wl_data_source *)source);
  finish();
}

static void testTeardown(void)
{
  start();
  struct Proxy * offer = textOffer();
  waylandCBRequest(401, LG_CLIPBOARD_DATA_TEXT);
  CHECK(rec.pollN == 1);
  const int fd = rec.poll[0].fd;
  struct Proxy * dnd = rawOffer();
  wlCb.dndOffer = (struct wl_data_offer *)dnd;
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];

  finish();
  CHECK(offer->dead);
  CHECK(dnd->dead);
  CHECK(source->dead);
  CHECK(proto.offerDestroyN == 2);
  CHECK(proto.sourceDestroyN == 1);
  CHECK(proto.dirtyDestroyN == 0);
  CHECK(proto.selectionSetN == 2);
  CHECK(!proto.selection);
  CHECK(rec.pollUnregisterN == 1);
  CHECK(rec.pollCleanupN == 1);
  checkClosed(fd);
}

static void testTeardownExternalOwner(void)
{
  start();
  waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
  struct Proxy * source = &proto.source[0];
  struct Proxy * external = newOffer();
  offerMime(external, "text/plain");
  selectOffer(external);
  CHECK(!wlCb.selectionSource);

  finish();
  CHECK(source->dead);
  CHECK(external->dead);
  CHECK(proto.selectionSetN == 1);
  CHECK(proto.selection == external);
}

static void testDnd(void)
{
  start();
  struct Proxy * leave = rawOffer();
  wlCb.dndOffer = (struct wl_data_offer *)leave;
  deviceListener()->leave(proto.device.data,
      (struct wl_data_device *)&proto.device);
  CHECK(!wlCb.dndOffer);
  CHECK(leave->dead);
  CHECK(proto.offerDestroyN == 1);

  struct Proxy * drop = rawOffer();
  wlCb.dndOffer = (struct wl_data_offer *)drop;
  deviceListener()->drop(proto.device.data,
      (struct wl_data_device *)&proto.device);
  CHECK(!wlCb.dndOffer);
  CHECK(drop->dead);
  CHECK(proto.offerDestroyN == 2);

  struct Proxy * enter = newOffer();
  offerMime(enter, "text/plain");
  deviceListener()->enter(proto.device.data,
      (struct wl_data_device *)&proto.device, 1, wlWm.surface, 0, 0,
      (struct wl_data_offer *)enter);
  CHECK(wlCb.dndOffer == (struct wl_data_offer *)enter);
  CHECK(!enter->user);
  CHECK(proto.setActionsN == 1);

  deviceListener()->leave(proto.device.data,
      (struct wl_data_device *)&proto.device);
  CHECK(!wlCb.dndOffer);
  CHECK(enter->dead);
  CHECK(proto.offerDestroyN == 3);
  finish();
}

struct Test
{
  const char * name;
  void (*run)(void);
};

static const struct Test tests[] =
{
  { "mime"      , testMime      },
  { "file-import", testFileImport },
  { "file-mate-import", testFileImportMate },
  { "file-source", testFileSource },
  { "file-replace", testFileSourceReplacement },
  { "replace"   , testReplace   },
  { "read-eof"  , testReadEOF   },
  { "read-block", testReadBlocked },
  { "read-empty", testReadBlockedEmpty },
  { "read-ready", testReadReadyRace },
  { "read-error", testReadError },
  { "source"    , testSource    },
  { "source-release", testSourceRelease },
  { "source-external", testSourceExternalOwner },
  { "source-replace", testSourceReplacement },
  { "source-cancel", testSourceCancelDuringRequest },
  { "source-early", testSourceBeginDuringRequest },
  { "source-error", testSourceError },
  { "teardown"  , testTeardown  },
  { "teardown-external", testTeardownExternalOwner },
  { "self-copy" , testSelfCopy  },
  { "self-copy-late", testLateSelfCopy },
  { "ignored"   , testIgnoredOffer },
  { "invalidate-lock", testInvalidateSerialization },
  { "dnd"       , testDnd       },
};

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;
}
